Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1595f93da | ||
|
|
a5c548bbee | ||
|
|
1375b3c53a | ||
|
|
72ede4a03f | ||
|
|
96076bf7c2 | ||
|
|
a3f7043f45 | ||
|
|
67c01b02ec | ||
|
|
ab1c6892df | ||
|
|
84c8ac4f59 | ||
|
|
faab9648a7 | ||
|
|
a30ad38a53 | ||
|
|
c0b186b8c1 | ||
|
|
01e761aecd | ||
|
|
55a99054fa | ||
|
|
2c5078cd7f | ||
|
|
82348adef2 | ||
|
|
71aad211c6 | ||
|
|
a40f976fd1 | ||
|
|
382f6f99c6 | ||
|
|
95c4aa8ae9 | ||
|
|
a6a47fde53 | ||
|
|
e08f51b4f2 |
@@ -13,6 +13,15 @@ debugpy:
|
||||
wait: false
|
||||
port: 5678
|
||||
|
||||
versatile_thermostat:
|
||||
auto_regulation_expert:
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
|
||||
input_number:
|
||||
fake_temperature_sensor1:
|
||||
name: Temperature
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"ms-python.python",
|
||||
"github.vscode-pull-request-github",
|
||||
"ryanluker.vscode-coverage-gutters",
|
||||
"ms-python.vscode-pylance"
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.pylint",
|
||||
"ferrierbenjamin.fold-unfold-all-icone"
|
||||
],
|
||||
// "mounts": [
|
||||
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached",
|
||||
@@ -40,8 +42,7 @@
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.autoSearchPaths": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"pylint.lintOnChange": false,
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"editor.formatOnPaste": false,
|
||||
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.python"
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"pylint.lintOnChange": false,
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
},
|
||||
|
||||
99
README-fr.md
99
README-fr.md
@@ -22,6 +22,7 @@
|
||||
- [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
|
||||
- [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate)
|
||||
- [L'auto-régulation](#lauto-régulation)
|
||||
- [L'auto-régulation en mode Expert](#lauto-régulation-en-mode-expert)
|
||||
- [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve)
|
||||
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi)
|
||||
- [Configurer la température préréglée](#configurer-la-température-préréglée)
|
||||
@@ -51,7 +52,7 @@
|
||||
- [Attributs personnalisés](#attributs-personnalisés)
|
||||
- [Quelques résultats](#quelques-résultats)
|
||||
- [Encore mieux](#encore-mieux)
|
||||
- [Bien mieux avec le Veersatile Thermostat UI Card](#bien-mieux-avec-le-veersatile-thermostat-ui-card)
|
||||
- [Bien mieux avec le Versatile Thermostat UI Card](#bien-mieux-avec-le-versatile-thermostat-ui-card)
|
||||
- [Encore mieux avec le composant Scheduler !](#encore-mieux-avec-le-composant-scheduler-)
|
||||
- [Encore bien mieux avec la custom:simple-thermostat front integration](#encore-bien-mieux-avec-la-customsimple-thermostat-front-integration)
|
||||
- [Toujours mieux avec Apex-chart pour régler votre thermostat](#toujours-mieux-avec-apex-chart-pour-régler-votre-thermostat)
|
||||
@@ -63,13 +64,14 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
|
||||
|
||||
>  _*Nouveautés*_
|
||||
> * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194).
|
||||
> * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
|
||||
> * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
|
||||
> * **Release 3.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)
|
||||
<details>
|
||||
<summary>Autres versions</summary>
|
||||
|
||||
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
|
||||
> * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
|
||||
> * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
|
||||
> * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
|
||||
@@ -85,7 +87,7 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
La puissance de l'appareil doit maintenant être la puissance totale de tous les appareils controlée par le VTherm. Cela permet d'avoir des équipements hétérogènes de puissance différente. Dans le cas de plusieurs appareils contrôlés par un seul VTherm, vous devrez éditer et changer la valeur `device_power`. Vous devez configurer la puissance totale de tous les appareils.
|
||||
|
||||
# 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 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 pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
@@ -105,7 +107,9 @@ Les installations avec fil pilote et diode d'activation bénéficie d'une option
|
||||
## Incompatibilités
|
||||
Certains thermostat de type TRV sont réputés incompatibles avec le Versatile Thermostat. C'est le cas des vannes suivantes :
|
||||
1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le VTherm,
|
||||
2. les vannes thermstatiques "Homematic radio". Elles ont un cycle de service incompatible avec une commande par le Versatile Thermostat
|
||||
2. les vannes thermstatiques "Homematic radio". Elles ont un cycle de service incompatible avec une commande par le Versatile Thermostat,
|
||||
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.
|
||||
|
||||
# Pourquoi une nouvelle implémentation du thermostat ?
|
||||
|
||||
@@ -202,8 +206,8 @@ Il est possible de choisir un thermostat over climate qui commande une climatisa
|
||||
#### L'auto-régulation
|
||||
Depuis la release 3.8, vous avez la possibilité d'activer la fonction d'auto-régulation. Cette fonction autorise VersatileThermostat à adapter la consigne de température donnée au climate sous-jacent afin que la température de la pièce atteigne réellement la consigne.
|
||||
Pour faire ça, le VersatileThermostat calcule un décalage basé sur les informations suivantes :
|
||||
1. la différence actuelle entre la température réelle et la température de consigne,
|
||||
2. l'accumulation des différences passées,
|
||||
1. la différence actuelle entre la température réelle et la température de consigne, appelé erreur brute,
|
||||
2. l'accumulation des erreurs passées,
|
||||
3. la différence entre la température extérieure et la consigne
|
||||
|
||||
Ces trois informations sont combinées pour calculer le décalage qui sera ajouté à la consigne courante et envoyé au climate sous-jacent.
|
||||
@@ -222,10 +226,89 @@ Ces trois paramètres permettent de moduler la régulation et éviter de multipl
|
||||
> 1. Ne démarrez pas tout de suite l'auto-régulation. Regardez comment se passe la régulation naturelle de votre équipement. Si vous constatez que la température de consigne n'est pas atteinte ou qu'elle met trop de temps à être atteinte, démarrez la régulation,
|
||||
> 2. D'abord commencez par une légère auto-régulation et gardez les deux paramètres avec leur valeurs par défaut. Attendez quelques jours et vérifiez si la situation s'est améliorée,
|
||||
> 3. Si ce n'est pas suffisant, passez en auto-régulation Medium, attendez une stabilisation,
|
||||
> 4. Si ce n'est toujours pas suffisant, passez en auto-régulation Forte.
|
||||
> 4. Si ce n'est toujours pas suffisant, passez en auto-régulation Forte,
|
||||
> 5. Si ce n'est toujours pas bon, il faudra passer en mode expert pour pouvoir régler les paramètres de régulation de façon fine. Voir en-dessous
|
||||
|
||||
L'auto-régulation consiste à forcer l'équipement a aller plus loin en lui forçant sa température de consigne régulièrement. Sa consommation peut donc être augmentée, ainsi que son usure.
|
||||
|
||||
#### L'auto-régulation en mode Expert
|
||||
|
||||
En mode **Expert** pouvez régler finement les paramètres de l'auto-régulation pour atteindre vos objeetifs et optimiser au mieux. L'algorithme calcule l'écart entre la consigne et la température réelle de la pièce. Cet écard est appelé erreur.
|
||||
Les paramètres réglables sont les suivants :
|
||||
1. `kp` : le facteur appliqué à l'erreur brute,
|
||||
2. `ki` : le facteur appliqué à l'accumulation des erreurs,
|
||||
3. `k_ext` : le facteur appliqué à la différence entre la température intérieure et la température externe,
|
||||
4. `offset_max` : le maximum de correction (offset) que la régulation peut appliquer,
|
||||
5. `stabilization_threshold` : un seuil de stabilisation qui lorsqu'il est atteint par l'erreur remet à 0, l'accumulation des erreurs,
|
||||
6. `accumulated_error_threshold` : le maximum pour l'accumulation d'erreur.
|
||||
|
||||
Pour le tuning il faut tenir compte de ces observations :
|
||||
1. `kp * erreur` va donner l'offset lié à l'erreur brute. Cet offset est directement proportionnel à l'erreur et sera à 0 lorsque la target sera atteinte,
|
||||
2. l'accumulation de l'erreur permet de corriger le stabilisation de la courbe alors qu'il reste une erreur. L'erreur s'accumule et l'offset augmente donc progressivement ce qui devrait finir par stabiliser sur la température cible. Pour que ce paramètre fondamental est un effet il faut qu'il soit pas trop petit. Une valeur moyenne est 30
|
||||
3. `ki * accumulated_error_threshold` va donner l'offset maximal lié à l'accumulation de l'erreur,
|
||||
4. `k_ext` permet d'appliquer tout de suite (sans attendre une accumulation des erreurs) une correction lorsque la température extérieure est très différente de la température cible. Si la stabilisation se fait trop haut lorsqu'il les écarts de température sont importants, c'est que ce paramètre est trop fort. Il devrait pouvoir être annulé totalement pour laisser faire les 2 premiers offset
|
||||
|
||||
Les valeurs préprogrammées sont les suivantes :
|
||||
|
||||
Slow régulation :
|
||||
|
||||
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
|
||||
|
||||
Light régulation :
|
||||
|
||||
kp: 0.2
|
||||
ki: 0.05
|
||||
k_ext: 0.05
|
||||
offset_max: 1.5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 10
|
||||
|
||||
|
||||
Medium régulation :
|
||||
|
||||
kp: 0.3
|
||||
ki: 0.05
|
||||
k_ext: 0.1
|
||||
offset_max: 2
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 20
|
||||
|
||||
|
||||
Strong régulation :
|
||||
|
||||
"""Strong parameters for regulation
|
||||
A set of parameters which doesn't take into account the external temp
|
||||
and concentrate to internal temp error + accumulated error.
|
||||
This should work for cold external conditions which else generates
|
||||
high external_offset"""
|
||||
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
|
||||
Pour utiliser le mode Expert il vous faut déclarer les valeurs que vous souhaitez utiliser pour chacun de ces paramètres dans votre `configuration.yaml` sous la forme suivante :
|
||||
```
|
||||
versatile_thermostat:
|
||||
auto_regulation_expert:
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
```
|
||||
et bien sur, configurer le mode auto-régulation du VTherm en mode Expert. Tous les VTherm en mode **Expert** utiliseront ces mêmes paramètres.
|
||||
|
||||
Pour que les modifications soient prises en compte, il faut soit **relancer totalement Home Assistant** soit juste l'intégration Versatile Thermostat (Outils de dev / Yaml / rechargement de la configuration / Versatile Thermostat).
|
||||
|
||||
### Pour un thermostat de type ```thermostat_over_valve```:
|
||||

|
||||
Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes.
|
||||
@@ -735,7 +818,7 @@ Enjoy !
|
||||
|
||||
# Encore mieux
|
||||
|
||||
## Bien mieux avec le Veersatile Thermostat UI Card
|
||||
## Bien mieux avec le Versatile Thermostat UI Card
|
||||
Une carte spéciale pour le Versatile Thermostat a été développée (sur la base du Better Thermostat). Elle est dispo ici [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card) et propose une vision moderne de tous les status du VTherm :
|
||||
|
||||

|
||||
|
||||
89
README.md
89
README.md
@@ -22,6 +22,7 @@
|
||||
- [For a ```thermostat_over_switch``` type thermostat](#for-a-thermostat_over_switch-type-thermostat)
|
||||
- [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate)
|
||||
- [Self-regulation](#self-regulation)
|
||||
- [Self-regulation in Expert mode](#self-regulation-in-expert-mode)
|
||||
- [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve)
|
||||
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
|
||||
- [Configure the preset temperature](#configure-the-preset-temperature)
|
||||
@@ -61,13 +62,14 @@
|
||||
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 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
|
||||
> * **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)
|
||||
<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)
|
||||
> * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
|
||||
@@ -83,7 +85,7 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
|
||||
The power of the device should now be the total power of all controler devices by the VTherm. This allow to have eterogeneous equipment with different power. In case of multi-devices controlled by a single VTherm you will have to edit and change the `device_power` value. Set the total power of all devices.
|
||||
|
||||
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome 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 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:
|
||||
@@ -103,7 +105,9 @@ Installations with pilot wire and activation diode benefit from an option which
|
||||
|
||||
Some TRV type thermostats are known to be incompatible with the Versatile Thermostat. This is the case for the following valves:
|
||||
1. Danfoss POPP valves with temperature feedback. It is impossible to turn off this valve and it self-regulates, causing conflicts with the VTherm,
|
||||
2. “Homematic radio” thermostatic valves. They have a duty cycle incompatible with control by the Versatile Thermostat
|
||||
2. “Homematic radio” thermostatic valves. They have a duty cycle incompatible with control by the Versatile Thermostat,
|
||||
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.
|
||||
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
@@ -219,10 +223,87 @@ These three parameters make it possible to modulate the regulation and avoid mul
|
||||
> 1. Do not start self-regulation straight away. Watch how the natural regulation of your equipment works. If you notice that the set temperature is not reached or that it is taking too long to be reached, start the regulation,
|
||||
> 2. First start with a slight self-regulation and keep both parameters at their default values. Wait a few days and check if the situation has improved,
|
||||
> 3. If this is not sufficient, switch to Medium self-regulation, wait for stabilization,
|
||||
> 4. If this is still not sufficient, switch to Strong self-regulation.
|
||||
> 4. If this is still not sufficient, switch to Strong self-regulation,
|
||||
> 5. If it is still not good, you will have to switch to expert mode to be able to finely adjust the regulation parameters. See below.
|
||||
|
||||
Self-regulation consists of forcing the equipment to go further by forcing its set temperature regularly. Its consumption can therefore be increased, as well as its wear.
|
||||
|
||||
#### Self-regulation in Expert mode
|
||||
|
||||
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
|
||||
The adjustable parameters are as follows:
|
||||
1. `kp`: the factor applied to the raw error,
|
||||
2. `ki`: the factor applied to the accumulation of errors,
|
||||
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
|
||||
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
|
||||
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
|
||||
6. `accumulated_error_threshold`: the maximum for error accumulation.
|
||||
|
||||
For tuning, these observations must be taken into account:
|
||||
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
|
||||
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
|
||||
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
|
||||
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
|
||||
|
||||
The pre-programmed values are as follows:
|
||||
|
||||
Slow régulation :
|
||||
|
||||
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
|
||||
|
||||
Light régulation :
|
||||
|
||||
kp: 0.2
|
||||
ki: 0.05
|
||||
k_ext: 0.05
|
||||
offset_max: 1.5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 10
|
||||
|
||||
Medium régulation :
|
||||
|
||||
kp: 0.3
|
||||
ki: 0.05
|
||||
k_ext: 0.1
|
||||
offset_max: 2
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 20
|
||||
|
||||
Strong régulation :
|
||||
|
||||
"""Strong parameters for regulation
|
||||
A set of parameters which doesn't take into account the external temp
|
||||
and concentrate to internal temp error + accumulated error.
|
||||
This should work for cold external conditions which else generates
|
||||
high external_offset"""
|
||||
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
|
||||
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
|
||||
```
|
||||
versatile_thermostat:
|
||||
auto_regulation_expert:
|
||||
kp: 0.4
|
||||
ki: 0.08
|
||||
k_ext: 0.0
|
||||
offset_max: 5
|
||||
stabilization_threshold: 0.1
|
||||
accumulated_error_threshold: 50
|
||||
```
|
||||
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
|
||||
|
||||
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
|
||||
|
||||
### For a thermostat of type ```thermostat_over_valve```:
|
||||

|
||||
You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves.
|
||||
|
||||
@@ -4,16 +4,84 @@ from __future__ import annotations
|
||||
from typing import Dict
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigType
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_SLOW,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
)
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SELF_REGULATION_PARAM_SCHEMA = (
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("kp"): vol.Coerce(float),
|
||||
vol.Required("ki"): vol.Coerce(float),
|
||||
vol.Required("k_ext"): vol.Coerce(float),
|
||||
vol.Required("offset_max"): vol.Coerce(float),
|
||||
vol.Required("stabilization_threshold"): vol.Coerce(float),
|
||||
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
CONF_AUTO_REGULATION_EXPERT: vol.Schema(
|
||||
{
|
||||
vol.Required("kp"): vol.Coerce(float),
|
||||
vol.Required("ki"): vol.Coerce(float),
|
||||
vol.Required("k_ext"): vol.Coerce(float),
|
||||
vol.Required("offset_max"): vol.Coerce(float),
|
||||
vol.Required("stabilization_threshold"): vol.Coerce(float),
|
||||
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
): # pylint: disable=unused-argument
|
||||
"""Initialisation de l'intégration"""
|
||||
_LOGGER.info(
|
||||
"Initializing %s integration with config: %s",
|
||||
DOMAIN,
|
||||
config.get(DOMAIN),
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# L'argument config contient votre fichier configuration.yaml
|
||||
vtherm_config = config.get(DOMAIN)
|
||||
|
||||
if vtherm_config is not None:
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
api.set_global_config(vtherm_config)
|
||||
else:
|
||||
_LOGGER.info("No global config from configuration.yaml available")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Versatile Thermostat from a config entry."""
|
||||
@@ -24,11 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data,
|
||||
)
|
||||
|
||||
# hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
|
||||
if api is None:
|
||||
api = VersatileThermostatAPI(hass)
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
api.add_entry(entry)
|
||||
|
||||
@@ -46,7 +110,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
if api:
|
||||
@@ -55,43 +119,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return unload_ok
|
||||
|
||||
|
||||
class VersatileThermostatAPI(dict):
|
||||
"""The VersatileThermostatAPI"""
|
||||
|
||||
_hass: HomeAssistant
|
||||
# _entries: Dict(str, ConfigEntry)
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
_LOGGER.debug("building a VersatileThermostatAPI")
|
||||
super().__init__()
|
||||
self._hass = hass
|
||||
# self._entries = dict()
|
||||
# Add the API in hass.data
|
||||
self._hass.data[DOMAIN] = self
|
||||
|
||||
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
|
||||
self._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)
|
||||
self._hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# If not more entries are preset, remove the API
|
||||
if len(self) == 0:
|
||||
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
|
||||
self._hass.data.pop(DOMAIN)
|
||||
|
||||
@property
|
||||
def hass(self):
|
||||
"""Get the HomeAssistant object"""
|
||||
return self._hass
|
||||
|
||||
|
||||
# Example migration function
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Migrate old entry."""
|
||||
|
||||
@@ -130,47 +130,54 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_motion_state: bool
|
||||
_presence_state: bool
|
||||
_window_auto_state: bool
|
||||
#PR - Adding Window ByPass
|
||||
_window_bypass_state: bool
|
||||
_underlyings: list[UnderlyingEntity]
|
||||
_last_change_time: datetime
|
||||
|
||||
_entity_component_unrecorded_attributes = ClimateEntity._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"type",
|
||||
"eco_temp",
|
||||
"boost_temp",
|
||||
"comfort_temp",
|
||||
"eco_away_temp",
|
||||
"boost_away_temp",
|
||||
"comfort_away_temp",
|
||||
"power_temp",
|
||||
"ac_mode",
|
||||
"current_power_max",
|
||||
"saved_preset_mode",
|
||||
"saved_target_temp",
|
||||
"saved_hvac_mode",
|
||||
"security_delay_min",
|
||||
"security_min_on_percent",
|
||||
"security_default_on_percent",
|
||||
"last_temperature_datetime",
|
||||
"last_ext_temperature_datetime",
|
||||
"minimal_activation_delay_sec",
|
||||
"device_power",
|
||||
"mean_cycle_power",
|
||||
"last_update_datetime",
|
||||
"timezone",
|
||||
"window_sensor_entity_id",
|
||||
"window_delay_sec",
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"motion_sensor_entity_id",
|
||||
"presence_sensor_entity_id",
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
}
|
||||
))
|
||||
_entity_component_unrecorded_attributes = (
|
||||
ClimateEntity._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_on",
|
||||
"type",
|
||||
"eco_temp",
|
||||
"boost_temp",
|
||||
"comfort_temp",
|
||||
"eco_away_temp",
|
||||
"boost_away_temp",
|
||||
"comfort_away_temp",
|
||||
"power_temp",
|
||||
"ac_mode",
|
||||
"current_power_max",
|
||||
"saved_preset_mode",
|
||||
"saved_target_temp",
|
||||
"saved_hvac_mode",
|
||||
"security_delay_min",
|
||||
"security_min_on_percent",
|
||||
"security_default_on_percent",
|
||||
"last_temperature_datetime",
|
||||
"last_ext_temperature_datetime",
|
||||
"minimal_activation_delay_sec",
|
||||
"device_power",
|
||||
"mean_cycle_power",
|
||||
"last_update_datetime",
|
||||
"timezone",
|
||||
"window_sensor_entity_id",
|
||||
"window_delay_sec",
|
||||
"window_auto_open_threshold",
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"motion_sensor_entity_id",
|
||||
"presence_sensor_entity_id",
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
"temperature_unit",
|
||||
"is_device_active",
|
||||
"target_temperature_step",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
@@ -291,6 +298,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
|
||||
self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX)
|
||||
self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN)
|
||||
# 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)
|
||||
@@ -621,7 +630,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
self._window_state = (window_state.state == STATE_ON)
|
||||
self._window_state = window_state.state == STATE_ON
|
||||
_LOGGER.debug(
|
||||
"%s - Window state have been retrieved: %s",
|
||||
self,
|
||||
@@ -684,6 +693,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
EVENT_HOMEASSISTANT_START, _async_startup_internal
|
||||
)
|
||||
|
||||
def restore_specific_previous_state(self, old_state):
|
||||
"""Should be overriden in each specific thermostat
|
||||
if a specific previous state or attribute should be
|
||||
restored
|
||||
"""
|
||||
|
||||
async def get_my_previous_state(self):
|
||||
"""Try to get my previou state"""
|
||||
# Check If we have an old state
|
||||
@@ -729,6 +744,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
|
||||
if old_total_energy:
|
||||
self._total_energy = old_total_energy
|
||||
|
||||
self.restore_specific_previous_state(old_state)
|
||||
else:
|
||||
# No previous state, try and restore defaults
|
||||
if self._target_temp is None:
|
||||
@@ -762,17 +779,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool:
|
||||
""" True if the Thermostat is over_climate"""
|
||||
"""True if the Thermostat is over_climate"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
""" True if the Thermostat is over_switch"""
|
||||
"""True if the Thermostat is over_switch"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_over_valve(self) -> bool:
|
||||
""" True if the Thermostat is over_valve"""
|
||||
"""True if the Thermostat is over_valve"""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -898,27 +915,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> float | None:
|
||||
"""Return the supported step of target temperature."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the highbound target temperature we try to reach.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the lowbound target temperature we try to reach.
|
||||
|
||||
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool | None:
|
||||
"""Return true if aux heater.
|
||||
@@ -933,10 +929,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
if not self._device_power:
|
||||
return None
|
||||
|
||||
return float(
|
||||
self._device_power
|
||||
* self._prop_algorithm.on_percent
|
||||
)
|
||||
return float(self._device_power * self._prop_algorithm.on_percent)
|
||||
|
||||
@property
|
||||
def total_energy(self) -> float | None:
|
||||
@@ -963,7 +956,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Get the window_auto_state"""
|
||||
return STATE_ON if self._window_auto_state else STATE_OFF
|
||||
|
||||
#PR - Adding Window ByPass
|
||||
@property
|
||||
def window_bypass_state(self) -> bool | None:
|
||||
"""Get the Window Bypass"""
|
||||
@@ -1033,6 +1025,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Returns the number of underlying entities"""
|
||||
return len(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
|
||||
|
||||
def underlying_entity_id(self, index=0) -> str | None:
|
||||
"""The climate_entity_id. Added for retrocompatibility reason"""
|
||||
if index < self.nb_underlying_entities:
|
||||
@@ -1219,7 +1216,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any
|
||||
For testing purpose you can pass an event_timestamp.
|
||||
For testing purpose you can pass an event_timestamp.
|
||||
"""
|
||||
self._target_temp = temperature
|
||||
return
|
||||
@@ -1307,7 +1304,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug(
|
||||
"Window delay condition is not satisfied. Ignore window event"
|
||||
)
|
||||
self._window_state = (old_state.state == STATE_ON)
|
||||
self._window_state = old_state.state == STATE_ON
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Window delay condition is satisfied", self)
|
||||
@@ -1318,13 +1315,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.debug("%s - no change in window state. Forget the event")
|
||||
return
|
||||
|
||||
self._window_state = new_state.state == STATE_ON
|
||||
|
||||
self._window_state = (new_state.state == STATE_ON)
|
||||
|
||||
#PR - Adding Window ByPass
|
||||
# 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)
|
||||
_LOGGER.info(
|
||||
"%s - Window ByPass is activated. Ignore window event", self
|
||||
)
|
||||
else:
|
||||
if not self._window_state:
|
||||
_LOGGER.info(
|
||||
@@ -1587,8 +1585,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_update_presence(self, new_state):
|
||||
_LOGGER.debug("%s - Updating presence. New state is %s", self, new_state)
|
||||
self._presence_state = new_state
|
||||
_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
|
||||
)
|
||||
if self._attr_preset_mode in HIDDEN_PRESETS or self._presence_on is False:
|
||||
_LOGGER.info(
|
||||
"%s - Ignoring presence change cause in Power or Security preset or presence not configured",
|
||||
@@ -1605,24 +1605,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
|
||||
return
|
||||
|
||||
# Change temperature with preset named _away
|
||||
# new_temp = None
|
||||
# if new_state == STATE_ON or new_state == STATE_HOME:
|
||||
# new_temp = self._presets[self._attr_preset_mode]
|
||||
# _LOGGER.info(
|
||||
# "%s - Someone is back home. Restoring temperature to %.2f",
|
||||
# self,
|
||||
# new_temp,
|
||||
# )
|
||||
# else:
|
||||
# new_temp = self._presets_away[
|
||||
# self.get_preset_away_name(self._attr_preset_mode)
|
||||
# ]
|
||||
# _LOGGER.info(
|
||||
# "%s - No one is at home. Apply temperature %.2f",
|
||||
# self,
|
||||
# new_temp,
|
||||
# )
|
||||
new_temp = self.find_preset_temp(self.preset_mode)
|
||||
if new_temp is not None:
|
||||
_LOGGER.debug(
|
||||
@@ -1715,8 +1697,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
and self.hvac_mode != HVACMode.OFF
|
||||
):
|
||||
if (
|
||||
not self.proportional_algorithm
|
||||
or self.proportional_algorithm.on_percent <= 0.0
|
||||
self.proportional_algorithm
|
||||
and self.proportional_algorithm.on_percent <= 0.0
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event",
|
||||
@@ -1827,7 +1809,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._device_power,
|
||||
)
|
||||
|
||||
ret = (self._current_power + self._device_power) >= self._current_power_max
|
||||
if self.is_over_climate:
|
||||
power_consumption_max = self._device_power
|
||||
else:
|
||||
power_consumption_max = max(
|
||||
self._device_power / self.nb_underlying_entities,
|
||||
self._device_power * self._prop_algorithm.on_percent,
|
||||
)
|
||||
|
||||
ret = (self._current_power + power_consumption_max) >= self._current_power_max
|
||||
if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF:
|
||||
_LOGGER.warning(
|
||||
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
||||
@@ -1845,6 +1835,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"current_power": self._current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_power_max": self._current_power_max,
|
||||
"current_power_consumption": power_consumption_max,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1872,7 +1863,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
},
|
||||
)
|
||||
|
||||
self._overpowering_state = ret
|
||||
if self._overpowering_state != ret:
|
||||
self._overpowering_state = ret
|
||||
self.update_custom_attributes()
|
||||
|
||||
return self._overpowering_state
|
||||
|
||||
async def check_security(self) -> bool:
|
||||
@@ -2098,6 +2092,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Update the custom extra attributes for the entity"""
|
||||
|
||||
self._attr_extra_state_attributes: dict(str, str) = {
|
||||
"is_on": self.is_on,
|
||||
"hvac_action": self.hvac_action,
|
||||
"hvac_mode": self.hvac_mode,
|
||||
"preset_mode": self.preset_mode,
|
||||
@@ -2117,6 +2112,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"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,
|
||||
"current_power": self._current_power,
|
||||
@@ -2129,7 +2125,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"overpowering_state": self.overpowering_state,
|
||||
"presence_state": self._presence_state,
|
||||
"window_auto_state": self.window_auto_state,
|
||||
#PR - Adding Window ByPass
|
||||
"window_bypass_state": self._window_bypass_state,
|
||||
"security_delay_min": self._security_delay_min,
|
||||
"security_min_on_percent": self._security_min_on_percent,
|
||||
@@ -2158,6 +2153,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"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,
|
||||
}
|
||||
|
||||
@callback
|
||||
@@ -2256,14 +2253,25 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
target:
|
||||
entity_id: climate.thermostat_1
|
||||
"""
|
||||
_LOGGER.info("%s - Calling service_set_window_bypass, window_bypass: %s", self, window_bypass)
|
||||
_LOGGER.info(
|
||||
"%s - Calling service_set_window_bypass, window_bypass: %s",
|
||||
self,
|
||||
window_bypass,
|
||||
)
|
||||
self._window_bypass_state = window_bypass
|
||||
if not self._window_bypass_state and self._window_state:
|
||||
_LOGGER.info("%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'", self, HVACMode.OFF)
|
||||
_LOGGER.info(
|
||||
"%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'",
|
||||
self,
|
||||
HVACMode.OFF,
|
||||
)
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
if self._window_bypass_state and self._window_state:
|
||||
_LOGGER.info("%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode", self)
|
||||
_LOGGER.info(
|
||||
"%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode",
|
||||
self,
|
||||
)
|
||||
await self.restore_hvac_mode(True)
|
||||
self.update_custom_attributes()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from .prop_algorithm import (
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
)
|
||||
|
||||
PRESET_AC_SUFFIX = "_ac"
|
||||
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
|
||||
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
|
||||
@@ -83,15 +84,16 @@ CONF_VALVE = "valve_entity_id"
|
||||
CONF_VALVE_2 = "valve_entity2_id"
|
||||
CONF_VALVE_3 = "valve_entity3_id"
|
||||
CONF_VALVE_4 = "valve_entity4_id"
|
||||
CONF_AUTO_REGULATION_MODE= "auto_regulation_mode"
|
||||
CONF_AUTO_REGULATION_NONE= "auto_regulation_none"
|
||||
CONF_AUTO_REGULATION_SLOW= "auto_regulation_slow"
|
||||
CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light"
|
||||
CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
|
||||
CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong"
|
||||
CONF_AUTO_REGULATION_DTEMP="auto_regulation_dtemp"
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN="auto_regulation_periode_min"
|
||||
CONF_INVERSE_SWITCH="inverse_switch_command"
|
||||
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode"
|
||||
CONF_AUTO_REGULATION_NONE = "auto_regulation_none"
|
||||
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow"
|
||||
CONF_AUTO_REGULATION_LIGHT = "auto_regulation_light"
|
||||
CONF_AUTO_REGULATION_MEDIUM = "auto_regulation_medium"
|
||||
CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong"
|
||||
CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
|
||||
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
|
||||
CONF_INVERSE_SWITCH = "inverse_switch_command"
|
||||
|
||||
CONF_PRESETS = {
|
||||
p: f"{p}_temp"
|
||||
@@ -196,7 +198,7 @@ ALL_CONF = (
|
||||
CONF_AUTO_REGULATION_MODE,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
CONF_INVERSE_SWITCH
|
||||
CONF_INVERSE_SWITCH,
|
||||
]
|
||||
+ CONF_PRESETS_VALUES
|
||||
+ CONF_PRESETS_AWAY_VALUES
|
||||
@@ -208,9 +210,20 @@ CONF_FUNCTIONS = [
|
||||
PROPORTIONAL_FUNCTION_TPI,
|
||||
]
|
||||
|
||||
CONF_AUTO_REGULATION_MODES = [CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_SLOW]
|
||||
CONF_AUTO_REGULATION_MODES = [
|
||||
CONF_AUTO_REGULATION_NONE,
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_SLOW,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
]
|
||||
|
||||
CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_VALVE]
|
||||
CONF_THERMOSTAT_TYPES = [
|
||||
CONF_THERMOSTAT_SWITCH,
|
||||
CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_THERMOSTAT_VALVE,
|
||||
]
|
||||
|
||||
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
|
||||
@@ -226,54 +239,73 @@ DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
|
||||
ATTR_TOTAL_ENERGY = "total_energy"
|
||||
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
|
||||
|
||||
|
||||
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
|
||||
class RegulationParamSlow:
|
||||
""" Light parameters for slow latency regulation"""
|
||||
kp:float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki:float = 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext:float = 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max:float = 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold:float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold:float = 2.0 * 288 # this allows up to 2°C long term offset in both directions
|
||||
"""Light parameters for slow latency regulation"""
|
||||
|
||||
kp: float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
|
||||
ki: float = (
|
||||
0.8 / 288.0
|
||||
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
|
||||
k_ext: float = (
|
||||
1.0 / 25.0
|
||||
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
|
||||
offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C
|
||||
stabilization_threshold: float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
|
||||
accumulated_error_threshold: float = (
|
||||
2.0 * 288
|
||||
) # this allows up to 2°C long term offset in both directions
|
||||
|
||||
|
||||
class RegulationParamLight:
|
||||
""" Light parameters for regulation"""
|
||||
kp:float = 0.2
|
||||
ki:float = 0.05
|
||||
k_ext:float = 0.05
|
||||
offset_max:float = 1.5
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 10
|
||||
"""Light parameters for regulation"""
|
||||
|
||||
kp: float = 0.2
|
||||
ki: float = 0.05
|
||||
k_ext: float = 0.05
|
||||
offset_max: float = 1.5
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 10
|
||||
|
||||
|
||||
class RegulationParamMedium:
|
||||
""" Light parameters for regulation"""
|
||||
kp:float = 0.3
|
||||
ki:float = 0.05
|
||||
k_ext:float = 0.1
|
||||
offset_max:float = 2
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 20
|
||||
"""Light parameters for regulation"""
|
||||
|
||||
kp: float = 0.3
|
||||
ki: float = 0.05
|
||||
k_ext: float = 0.1
|
||||
offset_max: float = 2
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 20
|
||||
|
||||
|
||||
class RegulationParamStrong:
|
||||
""" Medium parameters for regulation"""
|
||||
kp:float = 0.4
|
||||
ki:float = 0.08
|
||||
k_ext:float = 0.1
|
||||
offset_max:float = 3
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 25
|
||||
"""Strong parameters for regulation
|
||||
A set of parameters which doesn't take into account the external temp
|
||||
and concentrate to internal temp error + accumulated error.
|
||||
This should work for cold external conditions which else generates
|
||||
high external_offset"""
|
||||
|
||||
kp: float = 0.4
|
||||
ki: float = 0.08
|
||||
k_ext: float = 0.0
|
||||
offset_max: float = 5
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 50
|
||||
|
||||
|
||||
# Not used now
|
||||
class RegulationParamVeryStrong:
|
||||
""" Strong parameters for regulation"""
|
||||
kp:float = 0.6
|
||||
ki:float = 0.1
|
||||
k_ext:float = 0.2
|
||||
offset_max:float = 4
|
||||
stabilization_threshold:float = 0.1
|
||||
accumulated_error_threshold:float = 30
|
||||
"""Strong parameters for regulation"""
|
||||
|
||||
kp: float = 0.6
|
||||
ki: float = 0.1
|
||||
k_ext: float = 0.2
|
||||
offset_max: float = 4
|
||||
stabilization_threshold: float = 0.1
|
||||
accumulated_error_threshold: float = 30
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
"""The event type that can be sent"""
|
||||
@@ -293,8 +325,10 @@ class UnknownEntity(HomeAssistantError):
|
||||
class WindowOpenDetectionMethod(HomeAssistantError):
|
||||
"""Error to indicate there is an error in the window open detection method given."""
|
||||
|
||||
class overrides: # pylint: disable=invalid-name
|
||||
""" An annotation to inform overrides """
|
||||
|
||||
class overrides: # pylint: disable=invalid-name
|
||||
"""An annotation to inform overrides"""
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from datetime import datetime
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# To filter bad values
|
||||
MIN_DELTA_T_SEC = 10 # two temp mesure should be > 10 sec
|
||||
MIN_DELTA_T_SEC = 30 # two temp mesure should be > 10 sec
|
||||
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PITemperatureRegulator:
|
||||
""" A class implementing a PI Algorithm
|
||||
"""A class implementing a PI Algorithm
|
||||
PI algorithms calculate a target temperature by adding an offset which is calculating as follow:
|
||||
- offset = kp * error + ki * accumulated_error
|
||||
|
||||
@@ -16,30 +17,52 @@ class PITemperatureRegulator:
|
||||
- call set_target_temp when the target temperature change.
|
||||
"""
|
||||
|
||||
def __init__(self, target_temp: float, kp: float, ki: float, k_ext: float, offset_max: float, stabilization_threshold: float, accumulated_error_threshold: float):
|
||||
self.target_temp:float = target_temp
|
||||
self.kp:float = kp # proportionnel gain
|
||||
self.ki:float = ki # integral gain
|
||||
self.k_ext:float = k_ext # exterior gain
|
||||
self.offset_max:float = offset_max
|
||||
self.stabilization_threshold:float = stabilization_threshold
|
||||
self.accumulated_error:float = 0
|
||||
self.accumulated_error_threshold:float = accumulated_error_threshold
|
||||
def __init__(
|
||||
self,
|
||||
target_temp: float,
|
||||
kp: float,
|
||||
ki: float,
|
||||
k_ext: float,
|
||||
offset_max: float,
|
||||
stabilization_threshold: float,
|
||||
accumulated_error_threshold: float,
|
||||
):
|
||||
self.target_temp: float = target_temp
|
||||
self.kp: float = kp # proportionnel gain
|
||||
self.ki: float = ki # integral gain
|
||||
self.k_ext: float = k_ext # exterior gain
|
||||
self.offset_max: float = offset_max
|
||||
self.stabilization_threshold: float = stabilization_threshold
|
||||
self.accumulated_error: float = 0
|
||||
self.accumulated_error_threshold: float = accumulated_error_threshold
|
||||
|
||||
def reset_accumulated_error(self):
|
||||
""" Reset the accumulated error """
|
||||
"""Reset the accumulated error"""
|
||||
self.accumulated_error = 0
|
||||
|
||||
def set_accumulated_error(self, accumulated_error):
|
||||
"""Allow to persist and restore the accumulated_error"""
|
||||
self.accumulated_error = accumulated_error
|
||||
|
||||
def set_target_temp(self, target_temp):
|
||||
""" Set the new target_temp"""
|
||||
"""Set the new target_temp"""
|
||||
self.target_temp = target_temp
|
||||
# Do not reset the accumulated error
|
||||
# self.accumulated_error = 0
|
||||
|
||||
def calculate_regulated_temperature(self, internal_temp: float, external_temp:float): # pylint: disable=unused-argument
|
||||
""" Calculate a new target_temp given some temperature"""
|
||||
if internal_temp is None or external_temp is None:
|
||||
_LOGGER.warning("Internal_temp or external_temp are not set. Regulation will be suspended")
|
||||
def calculate_regulated_temperature(
|
||||
self, internal_temp: float, external_temp: float
|
||||
): # pylint: disable=unused-argument
|
||||
"""Calculate a new target_temp given some temperature"""
|
||||
if internal_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
|
||||
)
|
||||
return self.target_temp
|
||||
if external_temp is None:
|
||||
_LOGGER.warning(
|
||||
"Temporarily skipping the self-regulation algorithm while the configured sensor for outdoor temperature is unavailable"
|
||||
)
|
||||
return self.target_temp
|
||||
|
||||
# Calculate the error factor (P)
|
||||
@@ -49,7 +72,10 @@ class PITemperatureRegulator:
|
||||
self.accumulated_error += error
|
||||
|
||||
# Capping of the error
|
||||
self.accumulated_error = min(self.accumulated_error_threshold, max(-self.accumulated_error_threshold, self.accumulated_error))
|
||||
self.accumulated_error = min(
|
||||
self.accumulated_error_threshold,
|
||||
max(-self.accumulated_error_threshold, self.accumulated_error),
|
||||
)
|
||||
|
||||
# Calculate the offset (proportionnel + intégral)
|
||||
offset = self.kp * error + self.ki * self.accumulated_error
|
||||
@@ -62,15 +88,22 @@ class PITemperatureRegulator:
|
||||
total_offset = offset + offset_ext
|
||||
total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
|
||||
|
||||
|
||||
# If temperature is near the target_temp, reset the accumulated_error
|
||||
if abs(error) < self.stabilization_threshold:
|
||||
_LOGGER.debug("Stabilisation")
|
||||
self.accumulated_error = 0
|
||||
# Issue #199 - don't reset the accumulation error
|
||||
# if abs(error) < self.stabilization_threshold:
|
||||
# _LOGGER.debug("Stabilisation")
|
||||
# self.accumulated_error = 0
|
||||
|
||||
result = round(self.target_temp + total_offset, 1)
|
||||
|
||||
_LOGGER.debug("PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
|
||||
error, self.accumulated_error, offset, offset_ext, self.target_temp, result)
|
||||
_LOGGER.debug(
|
||||
"PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
|
||||
error,
|
||||
self.accumulated_error,
|
||||
offset,
|
||||
offset_ext,
|
||||
self.target_temp,
|
||||
result,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
UnitOfTemperature
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -54,7 +54,10 @@ async def async_setup_entry(
|
||||
]
|
||||
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]:
|
||||
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:
|
||||
@@ -202,6 +205,9 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
if on_percent is None:
|
||||
return
|
||||
|
||||
if math.isnan(on_percent) or math.isinf(on_percent):
|
||||
raise ValueError(f"Sensor has illegal state {on_percent}")
|
||||
|
||||
@@ -234,6 +240,7 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 1
|
||||
|
||||
|
||||
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
|
||||
|
||||
@@ -295,6 +302,10 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
|
||||
if on_time is None:
|
||||
return
|
||||
|
||||
if math.isnan(on_time) or math.isinf(on_time):
|
||||
raise ValueError(f"Sensor has illegal state {on_time}")
|
||||
|
||||
@@ -340,6 +351,9 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if self.my_climate and self.my_climate.proportional_algorithm
|
||||
else None
|
||||
)
|
||||
if off_time is None:
|
||||
return
|
||||
|
||||
if math.isnan(off_time) or math.isinf(off_time):
|
||||
raise ValueError(f"Sensor has illegal state {off_time}")
|
||||
|
||||
@@ -476,6 +490,7 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Return the suggested number of decimal digits for display."""
|
||||
return 2
|
||||
|
||||
|
||||
class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
"""Representation of a Energy sensor which exposes the energy"""
|
||||
|
||||
@@ -493,7 +508,9 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf(
|
||||
self.my_climate.regulated_target_temp
|
||||
):
|
||||
raise ValueError(f"Sensor has illegal state {self.my_climate.regulated_target_temp}")
|
||||
raise ValueError(
|
||||
f"Sensor has illegal state {self.my_climate.regulated_target_temp}"
|
||||
)
|
||||
|
||||
old_state = self._attr_native_value
|
||||
self._attr_native_value = round(
|
||||
|
||||
@@ -160,3 +160,4 @@ set_auto_regulation_mode:
|
||||
- "Medium"
|
||||
- "Strong"
|
||||
- "Slow"
|
||||
- "Expert"
|
||||
|
||||
@@ -348,9 +348,11 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import HVACAction, HVACMode
|
||||
|
||||
@@ -14,6 +17,7 @@ from .pi_algorithm import PITemperatureRegulator
|
||||
|
||||
from .const import (
|
||||
overrides,
|
||||
DOMAIN,
|
||||
CONF_CLIMATE,
|
||||
CONF_CLIMATE_2,
|
||||
CONF_CLIMATE_3,
|
||||
@@ -24,32 +28,47 @@ from .const import (
|
||||
CONF_AUTO_REGULATION_LIGHT,
|
||||
CONF_AUTO_REGULATION_MEDIUM,
|
||||
CONF_AUTO_REGULATION_STRONG,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
RegulationParamSlow,
|
||||
RegulationParamLight,
|
||||
RegulationParamMedium,
|
||||
RegulationParamStrong
|
||||
RegulationParamStrong,
|
||||
)
|
||||
|
||||
from .vtherm_api import VersatileThermostatAPI
|
||||
from .underlyings import UnderlyingClimate
|
||||
|
||||
_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
|
||||
_regulation_algo = None
|
||||
_regulated_target_temp: float = None
|
||||
_auto_regulation_dtemp: float = None
|
||||
_auto_regulation_period_min: int = None
|
||||
_last_regulation_change: datetime = None
|
||||
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"is_over_climate", "start_hvac_action_date", "underlying_climate_0", "underlying_climate_1",
|
||||
"underlying_climate_2", "underlying_climate_3", "regulation_accumulated_error"
|
||||
}))
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_climate",
|
||||
"start_hvac_action_date",
|
||||
"underlying_climate_0",
|
||||
"underlying_climate_1",
|
||||
"underlying_climate_2",
|
||||
"underlying_climate_3",
|
||||
"regulation_accumulated_error",
|
||||
"auto_regulation_mode",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the thermostat over switch."""
|
||||
@@ -60,12 +79,12 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
@property
|
||||
def is_over_climate(self) -> bool:
|
||||
""" True if the Thermostat is over_climate"""
|
||||
"""True if the Thermostat is over_climate"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
""" Returns the current hvac_action by checking all hvac_action of the underlyings """
|
||||
"""Returns the current hvac_action by checking all hvac_action of the underlyings"""
|
||||
|
||||
# if one not IDLE or OFF -> return it
|
||||
# else if one IDLE -> IDLE
|
||||
@@ -92,29 +111,53 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await self._send_regulated_temperature(force=True)
|
||||
|
||||
async def _send_regulated_temperature(self, force=False):
|
||||
""" Sends the regulated temperature to all underlying """
|
||||
"""Sends the regulated temperature to all underlying"""
|
||||
_LOGGER.info(
|
||||
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s",
|
||||
self,
|
||||
force,
|
||||
)
|
||||
|
||||
now: datetime = NowClass.get_now(self._hass)
|
||||
period = float((now - self._last_regulation_change).total_seconds()) / 60.0
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - period (%.1f) min is < %.0f min -> forget the regulation send",
|
||||
self,
|
||||
period,
|
||||
self._auto_regulation_period_min,
|
||||
)
|
||||
return
|
||||
|
||||
if not self._regulated_target_temp:
|
||||
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(self.current_temperature, self._cur_ext_temp),
|
||||
self._auto_regulation_dtemp)
|
||||
self._regulation_algo.calculate_regulated_temperature(
|
||||
self.current_temperature, self._cur_ext_temp
|
||||
),
|
||||
self._auto_regulation_dtemp,
|
||||
)
|
||||
dtemp = new_regulated_temp - self._regulated_target_temp
|
||||
|
||||
if not force and abs(dtemp) < self._auto_regulation_dtemp:
|
||||
_LOGGER.debug("%s - dtemp (%.1f) is < %.1f -> forget the regulation send", self, dtemp, self._auto_regulation_dtemp)
|
||||
_LOGGER.info(
|
||||
"%s - dtemp (%.1f) is < %.1f -> forget the regulation send",
|
||||
self,
|
||||
dtemp,
|
||||
self._auto_regulation_dtemp,
|
||||
)
|
||||
return
|
||||
|
||||
now:datetime = NowClass.get_now(self._hass)
|
||||
period = float((now - self._last_regulation_change).total_seconds()) / 60.
|
||||
if not force and period < self._auto_regulation_period_min:
|
||||
_LOGGER.debug("%s - period (%.1f) is < %.0f -> forget the regulation send", self, period, self._auto_regulation_period_min)
|
||||
return
|
||||
|
||||
|
||||
self._regulated_target_temp = new_regulated_temp
|
||||
_LOGGER.info("%s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp)
|
||||
self._last_regulation_change = now
|
||||
_LOGGER.info(
|
||||
"%s - Regulated temp have changed to %.1f. Resend it to underlyings",
|
||||
self,
|
||||
new_regulated_temp,
|
||||
)
|
||||
|
||||
for under in self._underlyings:
|
||||
await under.set_temperature(
|
||||
@@ -123,7 +166,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(entry_infos)
|
||||
for climate in [
|
||||
@@ -142,14 +185,24 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
|
||||
self.choose_auto_regulation_mode(
|
||||
entry_infos.get(CONF_AUTO_REGULATION_MODE) if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None else CONF_AUTO_REGULATION_NONE
|
||||
entry_infos.get(CONF_AUTO_REGULATION_MODE)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None
|
||||
else CONF_AUTO_REGULATION_NONE
|
||||
)
|
||||
|
||||
self._auto_regulation_dtemp = entry_infos.get(CONF_AUTO_REGULATION_DTEMP) if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None else 0.5
|
||||
self._auto_regulation_period_min = entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None else 5
|
||||
self._auto_regulation_dtemp = (
|
||||
entry_infos.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.5
|
||||
)
|
||||
self._auto_regulation_period_min = (
|
||||
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||
else 5
|
||||
)
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode):
|
||||
""" Choose or change the regulation mode"""
|
||||
"""Choose or change the regulation mode"""
|
||||
self._auto_regulation_mode = auto_regulation_mode
|
||||
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
@@ -159,7 +212,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
RegulationParamLight.k_ext,
|
||||
RegulationParamLight.offset_max,
|
||||
RegulationParamLight.stabilization_threshold,
|
||||
RegulationParamLight.accumulated_error_threshold)
|
||||
RegulationParamLight.accumulated_error_threshold,
|
||||
)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
@@ -168,7 +222,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
RegulationParamMedium.k_ext,
|
||||
RegulationParamMedium.offset_max,
|
||||
RegulationParamMedium.stabilization_threshold,
|
||||
RegulationParamMedium.accumulated_error_threshold)
|
||||
RegulationParamMedium.accumulated_error_threshold,
|
||||
)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
@@ -177,7 +232,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
RegulationParamStrong.k_ext,
|
||||
RegulationParamStrong.offset_max,
|
||||
RegulationParamStrong.stabilization_threshold,
|
||||
RegulationParamStrong.accumulated_error_threshold)
|
||||
RegulationParamStrong.accumulated_error_threshold,
|
||||
)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_SLOW:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
@@ -186,11 +242,40 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
RegulationParamSlow.k_ext,
|
||||
RegulationParamSlow.offset_max,
|
||||
RegulationParamSlow.stabilization_threshold,
|
||||
RegulationParamSlow.accumulated_error_threshold)
|
||||
else:
|
||||
RegulationParamSlow.accumulated_error_threshold,
|
||||
)
|
||||
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_EXPERT:
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(
|
||||
self._hass
|
||||
)
|
||||
if api is not None:
|
||||
if (expert_param := api.self_regulation_expert) is not None:
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature,
|
||||
expert_param.get("kp"),
|
||||
expert_param.get("ki"),
|
||||
expert_param.get("k_ext"),
|
||||
expert_param.get("offset_max"),
|
||||
expert_param.get("stabilization_threshold"),
|
||||
expert_param.get("accumulated_error_threshold"),
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - Cannot initialize Expert self-regulation mode due to VTherm API doesn't exists. Please contact the publisher of the integration",
|
||||
self,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"%s - Cannot initialize Expert self-regulation mode cause the configuration in configuration.yaml have not been found. Please see readme documentation for %s",
|
||||
self,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
if not self._regulation_algo:
|
||||
# A default empty algo (which does nothing)
|
||||
self._regulation_algo = PITemperatureRegulator(
|
||||
self.target_temperature, 0, 0, 0, 0, 0.1, 0)
|
||||
self.target_temperature, 0, 0, 0, 0, 0.1, 0
|
||||
)
|
||||
|
||||
@overrides
|
||||
async def async_added_to_hass(self):
|
||||
@@ -217,29 +302,51 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
)
|
||||
|
||||
@overrides
|
||||
def restore_specific_previous_state(self, old_state):
|
||||
"""Restore my specific attributes from previous state"""
|
||||
old_error = old_state.attributes.get("regulation_accumulated_error")
|
||||
if old_error:
|
||||
self._regulation_algo.set_accumulated_error(old_error)
|
||||
_LOGGER.debug(
|
||||
"%s - Old regulation accumulated_error have been restored to %f",
|
||||
self,
|
||||
old_error,
|
||||
)
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
""" Custom attributes """
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate
|
||||
self._attr_extra_state_attributes["start_hvac_action_date"] = (
|
||||
self._underlying_climate_start_hvac_action_date)
|
||||
self._attr_extra_state_attributes["underlying_climate_0"] = (
|
||||
self._underlyings[0].entity_id)
|
||||
self._attr_extra_state_attributes[
|
||||
"start_hvac_action_date"
|
||||
] = self._underlying_climate_start_hvac_action_date
|
||||
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_climate_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_climate_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
if self.is_regulated:
|
||||
self._attr_extra_state_attributes["regulated_target_temperature"] = self._regulated_target_temp
|
||||
self._attr_extra_state_attributes["regulation_accumulated_error"] = self._regulation_algo.accumulated_error
|
||||
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated
|
||||
self._attr_extra_state_attributes[
|
||||
"regulated_target_temperature"
|
||||
] = self._regulated_target_temp
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_regulation_mode"
|
||||
] = self.auto_regulation_mode
|
||||
self._attr_extra_state_attributes[
|
||||
"regulation_accumulated_error"
|
||||
] = self._regulation_algo.accumulated_error
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -473,17 +580,17 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self):
|
||||
""" Get the regulation mode """
|
||||
"""Get the regulation mode"""
|
||||
return self._auto_regulation_mode
|
||||
|
||||
@property
|
||||
def regulated_target_temp(self):
|
||||
""" Get the regulated target temperature """
|
||||
"""Get the regulated target temperature"""
|
||||
return self._regulated_target_temp
|
||||
|
||||
@property
|
||||
def is_regulated(self):
|
||||
""" Check if the ThermostatOverClimate is regulated """
|
||||
"""Check if the ThermostatOverClimate is regulated"""
|
||||
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
|
||||
|
||||
@property
|
||||
@@ -668,7 +775,11 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
target:
|
||||
entity_id: climate.thermostat_1
|
||||
"""
|
||||
_LOGGER.info("%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s", self, auto_regulation_mode)
|
||||
_LOGGER.info(
|
||||
"%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s",
|
||||
self,
|
||||
auto_regulation_mode,
|
||||
)
|
||||
if auto_regulation_mode == "None":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_NONE)
|
||||
elif auto_regulation_mode == "Light":
|
||||
@@ -679,6 +790,8 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_STRONG)
|
||||
elif auto_regulation_mode == "Slow":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_SLOW)
|
||||
elif auto_regulation_mode == "Expert":
|
||||
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_EXPERT)
|
||||
|
||||
await self._send_regulated_temperature()
|
||||
self.update_custom_attributes()
|
||||
|
||||
@@ -12,7 +12,7 @@ from .const import (
|
||||
CONF_HEATER_3,
|
||||
CONF_HEATER_4,
|
||||
CONF_INVERSE_SWITCH,
|
||||
overrides
|
||||
overrides,
|
||||
)
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
@@ -21,15 +21,31 @@ from .prop_algorithm import PropAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThermostatOverSwitch(BaseThermostat):
|
||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
|
||||
{
|
||||
"is_over_switch", "underlying_switch_0", "underlying_switch_1",
|
||||
"underlying_switch_2", "underlying_switch_3", "on_time_sec", "off_time_sec",
|
||||
"cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
|
||||
}))
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_switch",
|
||||
"is_inversed",
|
||||
"underlying_switch_0",
|
||||
"underlying_switch_1",
|
||||
"underlying_switch_2",
|
||||
"underlying_switch_3",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"power_percent",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
@@ -39,17 +55,25 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
""" True if the Thermostat is over_switch"""
|
||||
"""True if the Thermostat is over_switch"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_inversed(self) -> bool:
|
||||
""" True if the switch is inversed (for pilot wire and diode)"""
|
||||
"""True if the switch is inversed (for pilot wire and diode)"""
|
||||
return self._is_inversed is True
|
||||
|
||||
@property
|
||||
def power_percent(self) -> float | None:
|
||||
"""Get the current on_percent value"""
|
||||
if self._prop_algorithm:
|
||||
return round(self._prop_algorithm.on_percent * 100, 0)
|
||||
else:
|
||||
return None
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos):
|
||||
""" Initialize the Thermostat"""
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(entry_infos)
|
||||
|
||||
@@ -96,31 +120,34 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
async_track_state_change_event(
|
||||
self.hass, [switch.entity_id], self._async_switch_changed
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
self.hass.create_task(self.async_control_heating())
|
||||
|
||||
@overrides
|
||||
def update_custom_attributes(self):
|
||||
""" Custom attributes """
|
||||
"""Custom attributes"""
|
||||
super().update_custom_attributes()
|
||||
|
||||
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
|
||||
self._attr_extra_state_attributes["underlying_switch_0"] = (
|
||||
self._underlyings[0].entity_id)
|
||||
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed
|
||||
self._attr_extra_state_attributes["underlying_switch_0"] = self._underlyings[
|
||||
0
|
||||
].entity_id
|
||||
self._attr_extra_state_attributes["underlying_switch_1"] = (
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_2"] = (
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
|
||||
)
|
||||
self._attr_extra_state_attributes["underlying_switch_3"] = (
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes[
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
"on_percent"
|
||||
] = self._prop_algorithm.on_percent
|
||||
self._attr_extra_state_attributes["power_percent"] = self.power_percent
|
||||
self._attr_extra_state_attributes[
|
||||
"on_time_sec"
|
||||
] = self._prop_algorithm.on_time_sec
|
||||
@@ -182,3 +209,4 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
if old_state is None:
|
||||
self.hass.create_task(self._check_initial_state())
|
||||
self.async_write_ha_state()
|
||||
self.update_custom_attributes()
|
||||
|
||||
@@ -348,9 +348,11 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,9 +349,11 @@
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Lente",
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Moyenne",
|
||||
"auto_regulation_light": "Légère",
|
||||
"auto_regulation_expert": "Expert",
|
||||
"auto_regulation_none": "Aucune"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +30,15 @@
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Termostato sottostante",
|
||||
"climate_entity2_id": "Secundo termostato sottostante",
|
||||
"climate_entity3_id": "Terzo termostato sottostante",
|
||||
"climate_entity4_id": "Quarto termostato sottostante",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
"climate_entity3_id": "Terzo termostato",
|
||||
"climate_entity4_id": "Quarto termostato",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "Primo valvola numero",
|
||||
"valve_entity2_id": "Secondo valvola numero",
|
||||
"valve_entity3_id": "Terzo valvola numero",
|
||||
"valve_entity4_id": "Quarto valvola numero",
|
||||
"valve_entity_id": "Prima valvola",
|
||||
"valve_entity2_id": "Seconda valvolao",
|
||||
"valve_entity3_id": "Terza valvola",
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso"
|
||||
},
|
||||
@@ -48,15 +48,15 @@
|
||||
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
|
||||
"climate_entity_id": "Entity id del termostato sottostante",
|
||||
"climate_entity2_id": "Entity id del secundo termostato sottostante",
|
||||
"climate_entity3_id": "Entity id del terzo termostato sottostante",
|
||||
"climate_entity4_id": "Entity id del quarto termostato sottostante",
|
||||
"climate_entity_id": "Entity id del primo termostato",
|
||||
"climate_entity2_id": "Entity id del secondo termostato",
|
||||
"climate_entity3_id": "Entity id del terzo termostato",
|
||||
"climate_entity4_id": "Entity id del quarto termostato",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
|
||||
"valve_entity_id": "Entity id del primo valvola numero",
|
||||
"valve_entity2_id": "Entity id del secondo valvola numero",
|
||||
"valve_entity3_id": "Entity id del terzo valvola numero",
|
||||
"valve_entity4_id": "Entity id del quarto valvola numero",
|
||||
"valve_entity_id": "Entity id della prima valvola",
|
||||
"valve_entity2_id": "Entity id della seconda valvola",
|
||||
"valve_entity3_id": "Entity id della terza valvola",
|
||||
"valve_entity4_id": "Entity id della quarta valvola",
|
||||
"auto_regulation_mode": "Regolazione automatica della temperatura target",
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
|
||||
}
|
||||
@@ -188,15 +188,15 @@
|
||||
"heater_entity3_id": "Terzo riscaldatore",
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"proportional_function": "Algoritmo",
|
||||
"climate_entity_id": "Termostato sottostante",
|
||||
"climate_entity2_id": "Secundo termostato sottostante",
|
||||
"climate_entity3_id": "Terzo termostato sottostante",
|
||||
"climate_entity4_id": "Quarto termostato sottostante",
|
||||
"climate_entity_id": "Primo termostato",
|
||||
"climate_entity2_id": "Secondo termostato",
|
||||
"climate_entity3_id": "Terzo termostato",
|
||||
"climate_entity4_id": "Quarto termostato",
|
||||
"ac_mode": "AC mode ?",
|
||||
"valve_entity_id": "Primo valvola numero",
|
||||
"valve_entity2_id": "Secondo valvola numero",
|
||||
"valve_entity3_id": "Terzo valvola numero",
|
||||
"valve_entity4_id": "Quarto valvola numero",
|
||||
"valve_entity_id": "Prima valvola",
|
||||
"valve_entity2_id": "Seconda valvola",
|
||||
"valve_entity3_id": "Terza valvola",
|
||||
"valve_entity4_id": "Quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Comando inverso"
|
||||
},
|
||||
@@ -206,15 +206,15 @@
|
||||
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
|
||||
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
|
||||
"climate_entity_id": "Entity id del termostato sottostante",
|
||||
"climate_entity2_id": "Entity id del secundo termostato sottostante",
|
||||
"climate_entity3_id": "Entity id del terzo termostato sottostante",
|
||||
"climate_entity4_id": "Entity id del quarto termostato sottostante",
|
||||
"climate_entity_id": "Entity id del primo termostato",
|
||||
"climate_entity2_id": "Entity id del secondo termostato",
|
||||
"climate_entity3_id": "Entity id del terzo termostato",
|
||||
"climate_entity4_id": "Entity id del quarto termostato",
|
||||
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
|
||||
"valve_entity_id": "Entity id del primo valvola numero",
|
||||
"valve_entity2_id": "Entity id del secondo valvola numero",
|
||||
"valve_entity3_id": "Entity id del terzo valvola numero",
|
||||
"valve_entity4_id": "Entity id del quarto valvola numero",
|
||||
"valve_entity_id": "Entity id della prima valvola",
|
||||
"valve_entity2_id": "Entity id della seconda valvola",
|
||||
"valve_entity3_id": "Entity id della terza valvola",
|
||||
"valve_entity4_id": "Entity id della quarta valvola",
|
||||
"auto_regulation_mode": "Autoregolamentazione",
|
||||
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
|
||||
}
|
||||
@@ -252,9 +252,9 @@
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
|
||||
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_close_threshold": "Valore consigliato: 0 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
|
||||
"window_auto_max_duration": "Valore consigliato: 60 minuti. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -320,15 +320,17 @@
|
||||
"thermostat_type": {
|
||||
"options": {
|
||||
"thermostat_over_switch": "Termostato su un interruttore",
|
||||
"thermostat_over_climate": "Termostato sopra un altro termostato",
|
||||
"thermostat_over_valve": "Thermostato su una valvola"
|
||||
"thermostat_over_climate": "Termostato su un climatizzatore",
|
||||
"thermostat_over_valve": "Termostato su una valvola"
|
||||
}
|
||||
},
|
||||
"auto_regulation_mode": {
|
||||
"options": {
|
||||
"auto_regulation_slow": "Lento",
|
||||
"auto_regulation_strong": "Forte",
|
||||
"auto_regulation_medium": "Media",
|
||||
"auto_regulation_light": "Leggera",
|
||||
"auto_regulation_expert": "Esperto",
|
||||
"auto_regulation_none": "Nessuna autoregolamentazione"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,9 +348,11 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
70
custom_components/versatile_thermostat/vtherm_api.py
Normal file
70
custom_components/versatile_thermostat/vtherm_api.py
Normal file
@@ -0,0 +1,70 @@
|
||||
""" The API of Versatile Thermostat"""
|
||||
import logging
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_AUTO_REGULATION_EXPERT,
|
||||
)
|
||||
|
||||
VTHERM_API_NAME = "vtherm_api"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VersatileThermostatAPI(dict):
|
||||
"""The VersatileThermostatAPI"""
|
||||
|
||||
_hass: HomeAssistant
|
||||
# _entries: Dict(str, ConfigEntry)
|
||||
|
||||
@classmethod
|
||||
def get_vtherm_api(cls, hass: HomeAssistant):
|
||||
"""Get the eventual VTherm API class instance"""
|
||||
ret = hass.data.get(DOMAIN).get(VTHERM_API_NAME)
|
||||
if ret is None:
|
||||
ret = VersatileThermostatAPI(hass)
|
||||
hass.data[DOMAIN][VTHERM_API_NAME] = ret
|
||||
return ret
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
_LOGGER.debug("building a VersatileThermostatAPI")
|
||||
super().__init__()
|
||||
self._hass = hass
|
||||
self._expert_params = None
|
||||
|
||||
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
|
||||
self._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)
|
||||
self._hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# If not more entries are preset, remove the API
|
||||
if len(self) == 0:
|
||||
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
|
||||
self._hass.data.pop(DOMAIN)
|
||||
|
||||
def set_global_config(self, config):
|
||||
"""Read the global configuration from configuration.yaml file"""
|
||||
_LOGGER.info("Read global config from configuration.yaml")
|
||||
|
||||
self._expert_params = config.get(CONF_AUTO_REGULATION_EXPERT)
|
||||
if self._expert_params:
|
||||
_LOGGER.debug("We have found expert params %s", self._expert_params)
|
||||
|
||||
@property
|
||||
def self_regulation_expert(self):
|
||||
"""Get the self regulation params"""
|
||||
return self._expert_params
|
||||
|
||||
@property
|
||||
def hass(self):
|
||||
"""Get the HomeAssistant object"""
|
||||
return self._hass
|
||||
@@ -1,7 +1,7 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
|
||||
""" Test the normal start of a Thermostat """
|
||||
from unittest.mock import patch #, call
|
||||
from unittest.mock import patch # , call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -14,13 +14,18 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
|
||||
from .commons 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_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
async def test_over_climate_regulation(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the regulation of an over climate thermostat"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -41,7 +46,8 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
@@ -57,7 +63,7 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -90,36 +96,45 @@ async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_
|
||||
# set manual target temp (at now - 7) -> the regulation should occurs
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
# In medium we could go up to +3 degre
|
||||
# normally the calcul gives 18 + 2.2 but we round the result to the nearest 0.5 which is 2.0
|
||||
assert entity.regulated_target_temp == 18+2.0
|
||||
assert entity.regulated_target_temp == 18 + 1.5
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 23, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 18-0.5 # normally 0.6 but round_to_nearest gives 0.5
|
||||
assert (
|
||||
entity.regulated_target_temp == 18 - 2
|
||||
) # normally 0.6 but round_to_nearest gives 0.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
async def test_over_climate_regulation_ac_mode(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the regulation of an over climate thermostat"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -140,7 +155,8 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
|
||||
event_timestamp = now - timedelta(minutes=10)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
@@ -156,7 +172,7 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -185,53 +201,66 @@ async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_st
|
||||
await send_temperature_change_event(entity, 30, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
||||
|
||||
|
||||
# set manual target temp
|
||||
event_timestamp = now - timedelta(minutes=7)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=25)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.COOLING) # simulate under heating
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.COOLING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.COOLING
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
assert entity.preset_mode == PRESET_NONE # Manual mode
|
||||
|
||||
# the regulated temperature should be lower
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25-3 # In medium we could go up to -3 degre
|
||||
assert (
|
||||
entity.regulated_target_temp == 25 - 2.5
|
||||
) # In medium we could go up to -3 degre
|
||||
assert entity.hvac_action == HVACAction.COOLING
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 26, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 35, event_timestamp)
|
||||
|
||||
# the regulated temperature should be under
|
||||
assert entity.regulated_target_temp < entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25-2 # +2.3 without round_to_nearest
|
||||
assert (
|
||||
entity.regulated_target_temp == 25 - 1
|
||||
) # +2.3 without round_to_nearest
|
||||
|
||||
# change temperature so that the regulated temperature should slow down
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 25, event_timestamp)
|
||||
|
||||
# the regulated temperature should be greater
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 25+0.5 # +0.4 without round_to_nearest
|
||||
assert (
|
||||
entity.regulated_target_temp == 25 + 3
|
||||
) # +0.4 without round_to_nearest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_hass_states_is_state, skip_send_event):
|
||||
async def test_over_climate_regulation_limitations(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
|
||||
):
|
||||
"""Test the limitations of the regulation of an over climate thermostat:
|
||||
1. test the period_min parameter: do not send regulation event too frequently
|
||||
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
|
||||
1. test the period_min parameter: do not send regulation event too frequently
|
||||
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
|
||||
"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -252,7 +281,8 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
|
||||
event_timestamp = now - timedelta(minutes=20)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
), patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
@@ -268,7 +298,7 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
|
||||
if entity.entity_id == entity_id:
|
||||
return entity
|
||||
|
||||
entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverClimate)
|
||||
@@ -289,30 +319,37 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
|
||||
# set manual target temp (at now - 19) -> the regulation should be ignored because too early
|
||||
event_timestamp = now - timedelta(minutes=19)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=18)
|
||||
|
||||
fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
|
||||
fake_underlying_climate.set_hvac_action(
|
||||
HVACAction.HEATING
|
||||
) # simulate under heating
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
# the regulated temperature will change because when we set temp manually it is forced
|
||||
assert entity.regulated_target_temp == 20.
|
||||
assert entity.regulated_target_temp == 19.5
|
||||
|
||||
# set manual target temp (at now - 18) -> the regulation should be taken into account
|
||||
event_timestamp = now - timedelta(minutes=18)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await entity.async_set_temperature(temperature=17)
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 18+1 # In strong we could go up to +3 degre. 0.7 without round_to_nearest
|
||||
assert (
|
||||
entity.regulated_target_temp == 18 + 0
|
||||
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
|
||||
old_regulated_temp = entity.regulated_target_temp
|
||||
|
||||
# change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=15)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 16, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
@@ -323,12 +360,15 @@ async def test_over_climate_regulation_limitations(hass: HomeAssistant, skip_has
|
||||
# change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
|
||||
event_timestamp = now - timedelta(minutes=12)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now", return_value=event_timestamp
|
||||
"custom_components.versatile_thermostat.commons.NowClass.get_now",
|
||||
return_value=event_timestamp,
|
||||
):
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
await send_temperature_change_event(entity, 17, event_timestamp)
|
||||
await send_ext_temperature_change_event(entity, 12, event_timestamp)
|
||||
|
||||
# the regulated should have been done
|
||||
assert entity.regulated_target_temp != old_regulated_temp
|
||||
assert entity.regulated_target_temp > entity.target_temperature
|
||||
assert entity.regulated_target_temp == 17 + 1.5 # 0.7 without round_to_nearest
|
||||
assert (
|
||||
entity.regulated_target_temp == 17 + 0.5
|
||||
) # 0.7 without round_to_nearest
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# pylint: disable=unused-argument, line-too-long
|
||||
""" Test the Versatile Thermostat config flow """
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
@@ -29,7 +30,9 @@ async def test_show_form(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
|
||||
async def test_user_config_flow_over_switch(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the config flow with all thermostat_over_switch features"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@@ -128,7 +131,9 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
|
||||
async def test_user_config_flow_over_climate(
|
||||
hass: HomeAssistant, skip_hass_states_get
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the config flow with all thermostat_over_climate features and no additional features"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@@ -184,7 +189,9 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_window_auto_ok(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_get,
|
||||
skip_control_heating, # pylint: disable=unused-argument
|
||||
):
|
||||
"""Test the config flow with only window auto feature"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -353,7 +360,9 @@ async def test_user_config_flow_window_auto_ko(
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_user_config_flow_over_4_switches(
|
||||
hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_get,
|
||||
skip_control_heating, # pylint: disable=unused-argument
|
||||
):
|
||||
"""Test the config flow with 4 switchs thermostat_over_switch features"""
|
||||
|
||||
@@ -369,7 +378,7 @@ async def test_user_config_flow_over_4_switches(
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
}
|
||||
|
||||
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
|
||||
@@ -427,15 +436,11 @@ async def test_user_config_flow_over_4_switches(
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert (
|
||||
result["data"]
|
||||
== SOURCE_CONFIG
|
||||
| TYPE_CONFIG
|
||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||
| MOCK_PRESETS_CONFIG
|
||||
| MOCK_ADVANCED_CONFIG
|
||||
| { CONF_INVERSE_SWITCH: False }
|
||||
)
|
||||
assert result[
|
||||
"data"
|
||||
] == SOURCE_CONFIG | TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | {
|
||||
CONF_INVERSE_SWITCH: False
|
||||
}
|
||||
assert result["result"]
|
||||
assert result["result"].domain == DOMAIN
|
||||
assert result["result"].version == 1
|
||||
|
||||
@@ -163,7 +163,9 @@ async def test_one_switch_cycle(
|
||||
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
|
||||
|
||||
# Simulate the relaunch
|
||||
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||
await entity.underlying_entity(
|
||||
0
|
||||
)._turn_on_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
# wait restart
|
||||
@@ -184,7 +186,9 @@ async def test_one_switch_cycle(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_off_later( # pylint: disable=protected-access
|
||||
await entity.underlying_entity(
|
||||
0
|
||||
)._turn_off_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
|
||||
@@ -207,7 +211,9 @@ async def test_one_switch_cycle(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
) as mock_device_active:
|
||||
await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
|
||||
await entity.underlying_entity(
|
||||
0
|
||||
)._turn_on_later( # pylint: disable=protected-access
|
||||
None
|
||||
)
|
||||
|
||||
@@ -591,3 +597,139 @@ async def test_multiple_climates_underlying_changes(
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
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_switch_power_management(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Power management"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOver4SwitchMockName",
|
||||
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: 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: True,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch1",
|
||||
CONF_HEATER_2: "switch.mock_switch2",
|
||||
CONF_HEATER_3: "switch.mock_switch3",
|
||||
CONF_HEATER_4: "switch.mock_switch4",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
||||
CONF_DEVICE_POWER: 100,
|
||||
CONF_PRESET_POWER: 12,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theover4switchmockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.is_over_climate is False
|
||||
assert entity.nb_underlying_entities == 4
|
||||
|
||||
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.overpowering_state is None
|
||||
assert entity.target_temperature == 19
|
||||
|
||||
# 1. Send power mesurement
|
||||
await send_power_change_event(entity, 50, datetime.now())
|
||||
# Send power max mesurement
|
||||
await send_max_power_change_event(entity, 300, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
# All configuration is complete and power is < power_max
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.overpowering_state is False
|
||||
|
||||
# 2. Send power max mesurement too low and HVACMode is on
|
||||
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:
|
||||
# 100 of the device / 4 -> 25, current power 50 so max is 75
|
||||
await send_max_power_change_event(entity, 74, datetime.now())
|
||||
assert await entity.check_overpowering() is True
|
||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||
assert entity.preset_mode is PRESET_POWER
|
||||
assert entity.overpowering_state is True
|
||||
assert entity.target_temperature == 12
|
||||
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
||||
call.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{
|
||||
"type": "start",
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_power_max": 74,
|
||||
"current_power_consumption": 25.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count == 4 # The fourth are shutdown
|
||||
|
||||
# 3. change PRESET
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
await entity.async_set_preset_mode(PRESET_ECO)
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
# No change
|
||||
assert entity.overpowering_state is True
|
||||
|
||||
# 4. Send hugh power max mesurement to release overpowering
|
||||
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:
|
||||
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
|
||||
await send_max_power_change_event(entity, 150, datetime.now())
|
||||
assert await entity.check_overpowering() is False
|
||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||
assert entity.preset_mode is PRESET_ECO
|
||||
assert entity.overpowering_state is False
|
||||
assert entity.target_temperature == 17
|
||||
|
||||
assert (
|
||||
mock_heater_on.call_count == 0
|
||||
) # The fourth are not restarted because temperature is enought
|
||||
assert mock_heater_off.call_count == 0
|
||||
|
||||
@@ -4,8 +4,11 @@ from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
|
||||
from custom_components.versatile_thermostat.thermostat_switch import (
|
||||
ThermostatOverSwitch,
|
||||
)
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@@ -185,6 +188,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_power_max": 149,
|
||||
"current_power_consumption": 100.0,
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -242,7 +242,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 Power management"""
|
||||
"""Test the Window management"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -430,11 +430,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
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,
|
||||
@@ -447,10 +447,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
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_CLIMATE: "switch.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
@@ -461,7 +458,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
@@ -469,7 +466,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
now = datetime.now(tz)
|
||||
|
||||
tpi_algo = entity._prop_algorithm
|
||||
assert tpi_algo
|
||||
assert tpi_algo is None
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
@@ -484,18 +481,16 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
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",
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
await send_temperature_change_event(entity, 19, event_timestamp)
|
||||
|
||||
# The heater turns on
|
||||
assert mock_heater_on.call_count == 1
|
||||
# The climate turns on but was alredy on
|
||||
assert mock_set_hvac_mode.call_count == 0
|
||||
assert entity.last_temperature_slope is None
|
||||
assert entity._window_auto_algo.is_window_open_detected() is False
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
@@ -505,10 +500,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
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.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
@@ -531,8 +524,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
assert mock_heater_on.call_count == 0
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert mock_set_hvac_mode.call_count >= 1
|
||||
assert entity.last_temperature_slope == -1
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
assert entity._window_auto_algo.is_window_close_detected() is False
|
||||
@@ -543,17 +535,14 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
|
||||
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.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_set_hvac_mode, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count == 0
|
||||
assert mock_set_hvac_mode.call_count == 1
|
||||
assert round(entity.last_temperature_slope, 3) == -1
|
||||
# Because the algorithm is not aware of the expiration, for the algo we are still in alert
|
||||
assert entity._window_auto_algo.is_window_open_detected() is True
|
||||
@@ -674,12 +663,11 @@ async def test_window_auto_no_on_percent(
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
#PR - Adding Window Bypass
|
||||
|
||||
# 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
|
||||
):
|
||||
async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test the Window management when bypass enabled"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
@@ -810,7 +798,8 @@ async def test_window_bypass(
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
#PR - Adding Window bypass for window auto algorithm
|
||||
|
||||
# PR - Adding Window bypass for window auto algorithm
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@@ -921,7 +910,8 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
#PR - Adding Window bypass AFTER detection have been done should reactivate the heater
|
||||
|
||||
# PR - Adding Window bypass AFTER detection have been done should reactivate the heater
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
@@ -1049,4 +1039,4 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
|
||||
)
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
entity.remove_thermostat()
|
||||
|
||||
Reference in New Issue
Block a user