Compare commits

...

13 Commits

Author SHA1 Message Date
Jean-Marc Collin 7dbdcf0ee4 Fix security modes corner cases 2023-02-11 12:38:14 +01:00
Jean-Marc Collin ade1ee4365 Missing translations for preset mode Power and Security #46
Set preset internal #45
Notify (send event messages) when something important happens #43
Enhancing Security mode #42
Rename None preset to Manual #3
2023-02-11 10:34:45 +01:00
adi90x 7b57f7da28 Minimal security threeshold (#44)
* Add SECURITY_MIN_ON_PERCENT
2023-02-10 19:13:57 +01:00
Jean-Marc Collin 9fe307ba1e FIX Issue #38 Support Fahrenheit 2023-02-09 00:05:51 +01:00
Jean-Marc Collin 717c893c75 Reset security mode when preset or hvac_mode change #41 2023-02-07 07:47:07 +01:00
Jean-Marc Collin 5b8fb81053 Documentation update for release 2.0.0 2023-02-03 23:17:09 +01:00
Jean-Marc Collin 8b77d9d75f Some switch are not visible in the list of radiators #40 2023-02-03 22:30:33 +01:00
Jean-Marc Collin 3d977c4981 Failure to initialize #39 2023-02-01 10:43:21 +01:00
Jean-Marc Collin 115df52f67 Issue#36 - Error at initialisation
Issue#32 - Error with Fan Modes on thermostat over climate type
2023-01-31 22:19:43 +01:00
Jean-Marc Collin 3371197594 Fix issue#32 Error with Fan Modes on thermostat over climate type 2023-01-31 09:35:27 +01:00
Jean-Marc Collin 7e63c9aa79 Heater can restart after HVAC_MODE_OFF 2023-01-30 07:09:43 +01:00
Jean-Marc Collin 100cfaeac9 Add binary sensors for window sensors 2023-01-30 06:49:39 +01:00
Jean-Marc Collin 77631e94d9 FIX feature selection 2023-01-29 23:21:51 +01:00
14 changed files with 787 additions and 275 deletions
+27
View File
@@ -111,3 +111,30 @@ recorder:
- switch - switch
- climate - climate
- sensor - sensor
template:
- binary_sensor:
- name: maison_occupee
unique_id: maison_occupee
state: "{{is_state('person.jmc', 'home') }}"
device_class: occupancy
switch:
- platform: template
switches:
pilote_sdb_rdc:
friendly_name: "Pilote chauffage SDB RDC"
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
turn_on:
service: select.select_option
data:
option: comfort
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off:
service: select.select_option
data:
option: comfort-2
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
+49 -22
View File
@@ -13,12 +13,13 @@
- [HACS installation (recommendé)](#hacs-installation-recommendé) - [HACS installation (recommendé)](#hacs-installation-recommendé)
- [Installation manuelle](#installation-manuelle) - [Installation manuelle](#installation-manuelle)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Configuration minimale](#configuration-minimale) - [Choix des attributs de base](#choix-des-attributs-de-base)
- [Sélectionnez l'entité pilotée](#sélectionnez-lentité-pilotée)
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi) - [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi)
- [Configurer la température préréglée](#configurer-la-température-préréglée) - [Configurer la température préréglée](#configurer-la-température-préréglée)
- [Configurer les portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats) - [Configurer les portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats)
- [Configurer le mode d'activité ou la détection de mouvement](#configurer-le-mode-dactivité-ou-la-détection-de-mouvement) - [Configurer le mode d'activité ou la détection de mouvement](#configurer-le-mode-dactivité-ou-la-détection-de-mouvement)
- [Configurer la gestion de l'alimentation](#configurer-la-gestion-de-lalimentation) - [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance)
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation) - [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation)
- [Configuration avancée](#configuration-avancée) - [Configuration avancée](#configuration-avancée)
- [Exemples de réglage](#exemples-de-réglage) - [Exemples de réglage](#exemples-de-réglage)
@@ -40,16 +41,22 @@
- [Toujours mieux avec Apex-chart pour régler votre thermostat](#toujours-mieux-avec-apex-chart-pour-régler-votre-thermostat) - [Toujours mieux avec Apex-chart pour régler votre thermostat](#toujours-mieux-avec-apex-chart-pour-régler-votre-thermostat)
- [Les contributions sont les bienvenues !](#les-contributions-sont-les-bienvenues) - [Les contributions sont les bienvenues !](#les-contributions-sont-les-bienvenues)
_Composant développé à l'aide de l'incroyable modèle de développement [[blueprint](https://github.com/custom-components/integration_blueprint)]._ _Composant développé à l'aide de l'incroyable modèle de développement [[blueprint](https://github.com/custom-components/integration_blueprint)]._
Ce composant personnalisé pour Home Assistant est une mise à niveau et est une réécriture complète du composant "Awesome thermostat" (voir [Github](https://github.com/dadge/awesome_thermostat)) avec l'ajout de fonctionnalités. Ce composant personnalisé pour Home Assistant est une mise à niveau et est une réécriture complète du composant "Awesome thermostat" (voir [Github](https://github.com/dadge/awesome_thermostat)) avec l'ajout de fonctionnalités.
# Quand l'utiliser et ne pas l'utiliser # Quand l'utiliser et ne pas l'utiliser
Ce thermostat a pour but de commander un radiateur qui ne fonctionne qu'en mode marche/arrêt. La configuration minimale nécessaire pour utiliser ce thermostat est : Ce thermostat peut piloter 2 types d'équipement:
1. un équipement comme un radiateur (un interrupteur), 1. un radiateur qui ne fonctionne qu'en mode marche/arrêt (nommé ```thermostat_over_switch```). La configuration minimale nécessaire pour utiliser ce type thermostat est :
2. une sonde de température pour la pièce (ou un input_number), a. un équipement comme un radiateur (un ```switch``` ou équivalent),
3. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas) b. une sonde de température pour la pièce (ou un input_number),
c. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
2. un autre thermostat qui a ses propres modes de fonctionnement (nommé ```thermostat_over_climate```). Pour ce type de thermostat la configuration minimale nécessite :
a. un équipement comme une climatisation qui est pilotée par sa propre entity de type ```climate```,
b. une sonde de température pour la pièce (ou un input_number),
c. un capteur de température externe (pensez à l'intégration météo si vous n'en avez pas)
Le type ```thermostat_over_climate``` permet d'ajouter à votre équipement existant toutes les fonctionnalités fournies par VersatileThermostat. L'entité climate VersatileThermostat pilotera votre entité climate, en la coupant si les fenêtres sont ouvertes, la passant en mode Eco si personne n'est présent, etc. Cf. [ici](#pourquoi-une-nouvelle-implémentation-du-thermostat). Pour ce type de thermostat, les cycles éventuels de chauffe sont pilotés par l'entité climate sous-jacente et pas par le Versatile Thermostat lui-même.
Parce que cette intégration vise à commander le radiateur en tenant compte du préréglage configuré (preset) et de la température ambiante, ces informations sont obligatoires. Parce que cette intégration vise à commander le radiateur en tenant compte du préréglage configuré (preset) et de la température ambiante, ces informations sont obligatoires.
@@ -100,28 +107,44 @@ La configuration peut être modifiée via la même interface. Sélectionnez simp
Suivez ensuite les étapes de configuration comme suit : Suivez ensuite les étapes de configuration comme suit :
## Configuration minimale ## Choix des attributs de base
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-main.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-main.png?raw=true)
Donnez les principaux attributs obligatoires : Donnez les principaux attributs obligatoires :
1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate) 1. un nom (sera le nom de l'intégration et aussi le nom de l'entité climate)
2. un identifiant d'entité d'équipement qui représente l'élément chauffant. Cet équipement doit pouvoir s'allumer ou s'éteindre, 2. le type de thermostat ```thermostat_over_switch``` pour piloter un radiateur commandé par un switch ou ```thermostat_over_climate``` pour piloter un autre thermostat. Cf. [ci-dessus](#pourquoi-une-nouvelle-implémentation-du-thermostat)
3. un identifiant d'entité de capteur de température qui donne la température de la pièce dans laquelle le radiateur est installé, 4. un identifiant d'entité de capteur de température qui donne la température de la pièce dans laquelle le radiateur est installé,
4. une entité capteur de température donnant la température extérieure. Si vous n'avez pas de capteur externe, vous pouvez utiliser l'intégration météo locale 5. une entité capteur de température donnant la température extérieure. Si vous n'avez pas de capteur externe, vous pouvez utiliser l'intégration météo locale
5. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous), 6. une durée de cycle en minutes. A chaque cycle, le radiateur s'allumera puis s'éteindra pendant une durée calculée afin d'atteindre la température ciblée (voir [preset](#configure-the-preset-temperature) ci-dessous),
6. Algorithme à utiliser. Aujourd'hui, seul l'algorithme TPI est disponible. Voir [algorithme](#algorithme) 7. les températures minimales et maximales du thermostat,
8. la liste des fonctionnalités qui seront utilisées pour ce thermostat. En fonction de vos choix, les écrans de configuration suivants s'afficheront ou pas.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**, 1. avec le type ```thermostat_over_swutch```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible en effet pour le radiateur à accumulation et il sera sollicité inutilement 2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible en effet pour le radiateur à accumulation et il sera sollicité inutilement
## Sélectionnez l'entité pilotée
En fonction de votre choix sur le type de thermostat, vous devrez choisir une entité de type switch ou une entité de type climate. Seules les entités compatibles sont présentées.
Pour un thermostat de type ```thermostat_over_switch```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme)
Pour un thermostat de type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
## Configurez les coefficients de l'algorithme TPI ## Configurez les coefficients de l'algorithme TPI
Cliquez sur 'Valider' sur la page précédente et vous y arriverez : Si vous avez choisi un thermostat de type ```thermostat_over_switch``` vous arriverez sur cette page :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true)
Vous devez donner :
1. le coefficient coef_int de l'algorithme TPI,
2. le coefficient coef_ext de l'algorithme TPI
Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm). Pour plus d'informations sur l'algorithme TPI et son réglage, veuillez vous référer à [algorithm](#algorithm).
## Configurer la température préréglée ## Configurer la température préréglée
@@ -144,7 +167,7 @@ Le mode préréglé (preset) vous permet de préconfigurer la température cibl
5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front 5. Si vous ne souhaitez pas utiliser le préréglage, indiquez 0 comme température. Le préréglage sera alors ignoré et ne s'affichera pas dans le composant front
## Configurer les portes/fenêtres en allumant/éteignant les thermostats ## Configurer les portes/fenêtres en allumant/éteignant les thermostats
Cliquez sur 'Valider' sur la page précédente et vous y arriverez : Si vous avez choisi la fonctionnalité ```Avec détection des ouvertures```, cliquez sur 'Valider' sur la page précédente et vous y arriverez :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window.png?raw=true)
@@ -159,7 +182,7 @@ Et c'est tout ! votre thermostat s'éteindra lorsque les fenêtres seront ouvert
2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide 2. Si vous n'avez pas de capteur de fenêtre/porte dans votre chambre, laissez simplement l'identifiant de l'entité du capteur vide
## Configurer le mode d'activité ou la détection de mouvement ## Configurer le mode d'activité ou la détection de mouvement
Cliquez sur 'Valider' sur la page précédente et vous y arriverez : Si vous avez choisi la fonctionnalité ```Avec détection de mouvement```, cliquez sur 'Valider' sur la page précédente et vous y arriverez :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true)
@@ -181,9 +204,9 @@ Pour que cela fonctionne, le thermostat climatique doit être en mode prérégl
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique 1. Sachez que comme pour les autres modes prédéfinis, ``Activity`` ne sera proposé que s'il est correctement configuré. En d'autres termes, les 4 clés de configuration doivent être définies si vous souhaitez voir l'activité dans l'interface de l'assistant domestique
## Configurer la gestion de l'alimentation ## Configurer la gestion de la puissance
Cliquez sur 'Valider' sur la page précédente et vous arriverez ici : Si vous avez choisi la fonctionnalité ```Avec détection de la puissance```, cliquez sur 'Valider' sur la page précédente et vous arriverez ici :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true)
@@ -193,13 +216,13 @@ Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW o
Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez. Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou de ce que vous voulez.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. En cas de délestage, le radiateur est réglé sur le préréglage nommé ``power``. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement. 1. En cas de délestage, le radiateur est réglé sur le préréglage nommé ```power```. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement.
2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation. 2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation.
3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés. 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
## Configurer la présence ou l'occupation ## Configurer la présence ou l'occupation
Cette fonction vous permet de modifier dynamiquement la température de tous les préréglages du thermostat configurés lorsque personne n'est à la maison ou lorsque quelqu'un rentre à la maison. Pour cela, vous devez configurer la température qui sera utilisée pour chaque préréglage lorsque la présence est désactivée. Lorsque le capteur de présence s'éteint, ces températures seront utilisées. Lorsqu'il se rallume, la température "normale" configurée pour le préréglage est utilisée. Voir [gestion des préréglages](#configure-the-preset-temperature). Si sélectionnée en première page, cette fonction vous permet de modifier dynamiquement la température de tous les préréglages du thermostat configurés lorsque personne n'est à la maison ou lorsque quelqu'un rentre à la maison. Pour cela, vous devez configurer la température qui sera utilisée pour chaque préréglage lorsque la présence est désactivée. Lorsque le capteur de présence s'éteint, ces températures seront utilisées. Lorsqu'il se rallume, la température "normale" configurée pour le préréglage est utilisée. Voir [gestion des préréglages](#configure-the-preset-temperature).
Pour configurer la présence remplissez ce formulaire : Pour configurer la présence remplissez ce formulaire :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presence.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presence.png?raw=true)
@@ -224,6 +247,9 @@ Le premier délai (minimal_activation_delay_sec) en sec dans le délai minimum a
Le deuxième délai (security_delay_min) est le délai maximal entre deux mesures de température avant de régler le préréglage sur ``security`` et d'éteindre le thermostat. Si le capteur de température ne donne plus de mesures de température, le thermostat et le radiateur s'éteindront après ce délai et le préréglage du thermostat sera réglé sur ``security``. Ceci est utile pour éviter une surchauffe si la batterie de votre capteur de température est trop faible. Le deuxième délai (security_delay_min) est le délai maximal entre deux mesures de température avant de régler le préréglage sur ``security`` et d'éteindre le thermostat. Si le capteur de température ne donne plus de mesures de température, le thermostat et le radiateur s'éteindront après ce délai et le préréglage du thermostat sera réglé sur ``security``. Ceci est utile pour éviter une surchauffe si la batterie de votre capteur de température est trop faible.
Le troisième paramétre (security_min_on_percent) est la valeur minimal de on_percent en dessous de laquelle le préréglage sécurité ne sera pas activé.
Mettre ce paramètre à ``0.00`` déclenchera le préréglage sécurité quelque soit la dernière consigne de chauffage, à l'inverse ``1.00`` ne déclenchera jamais le préréglage sécurité.
Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglage communs
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
@@ -352,7 +378,8 @@ Les attributs personnalisés sont les suivants :
| ``motion_state`` | Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré | | ``motion_state`` | Le dernier état connu du capteur de mouvement. Aucun si le mouvement n'est pas configuré |
| ``overpowering_state`` | Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée | | ``overpowering_state`` | Le dernier état connu du capteur surpuissant. Aucun si la gestion de l'alimentation n'est pas configurée |
| ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée | | ``presence_state`` | Le dernier état connu du capteur de présence. Aucun si la gestion de présence n'est pas configurée |
| ``delay_security_min`` | Le délai avant de régler le mode de sécurité lorsque le capteur de température est éteint | | ``security_delay_min`` | Le délai avant de régler le mode de sécurité lorsque le capteur de température est éteint |
| ``security_min_on_percent`` | Seuil en dessous duquel le thermostat ne passera pas en sécurité |
| ``last_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température interne | | ``last_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température interne |
| ``last_ext_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température extérieure | | ``last_ext_temperature_datetime`` | La date et l'heure au format ISO8866 de la dernière réception de température extérieure |
| ``**état_sécurité**`` | L'état de sécurité. vrai ou faux | | ``**état_sécurité**`` | L'état de sécurité. vrai ou faux |
+36 -17
View File
@@ -14,6 +14,7 @@
- [Manual installation](#manual-installation) - [Manual installation](#manual-installation)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Minimal configuration update](#minimal-configuration-update) - [Minimal configuration update](#minimal-configuration-update)
- [Select the driven entity](#select-the-driven-entity)
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients) - [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
- [Configure the preset temperature](#configure-the-preset-temperature) - [Configure the preset temperature](#configure-the-preset-temperature)
- [Configure the doors/windows turning on/off the thermostats](#configure-the-doorswindows-turning-onoff-the-thermostats) - [Configure the doors/windows turning on/off the thermostats](#configure-the-doorswindows-turning-onoff-the-thermostats)
@@ -40,18 +41,22 @@
- [Even better with Apex-chart to tune your Thermostat](#even-better-with-apex-chart-to-tune-your-thermostat) - [Even better with Apex-chart to tune your Thermostat](#even-better-with-apex-chart-to-tune-your-thermostat)
- [Contributions are welcome!](#contributions-are-welcome) - [Contributions are welcome!](#contributions-are-welcome)
_Component developed by using the amazing development template [[blueprint](https://github.com/custom-components/integration_blueprint)]._ _Component developed by using the amazing development template [[blueprint](https://github.com/custom-components/integration_blueprint)]._
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features. This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
# When to use / not use # When to use / not use
This thermostat aims to command a heater which works only in on/off mode. The minimal needed configuration to use this thermostat is: This thermostat can control 2 types of equipment:
1. an equipement like a heater (a switch), 1. a heater that only works in on/off mode (named ```thermostat_over_switch```). The minimum configuration required to use this type of thermostat is:
2. a temperature sensor for the room (or an input_number), has. equipment such as a radiator (a ```switch``` or equivalent),
3. an external temperature sensor (think of the meteo integration if you don't have one) b. a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one)
2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). For this type of thermostat, the minimum configuration requires:
has. equipment such as air conditioning which is controlled by its own ```climate``` type entity,
b. a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one)
Because this integration aims to command the heater considering the preset configured and the room temperature, those informations are mandatory. The ```thermostat_over_climate``` type allows you to add all the functionality provided by VersatileThermostat to your existing equipment. The climate VersatileThermostat entity will control your climate entity, turning it off if the windows are open, switching it to Eco mode if no one is present, etc. See [here](#why-a-new-implementation-of-the-thermostat). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself.
# Why another thermostat implementation ? # Why another thermostat implementation ?
@@ -103,17 +108,28 @@ Then follow the configurations steps as follow:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-main.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-main.png?raw=true)
Give the main mandatory attributes: Give the main mandatory attributes:
1. a name (will be the integration name and also the climate entity name) 1. a name (will be the name of the integration and also the name of the climate entity)
2. an equipment entity id which represent the heater. This equipment should be able to switch on or off, 2. the type of thermostat ```thermostat_over_switch``` to control a radiator controlled by a switch or ```thermostat_over_climate``` to control another thermostat. Cf. [above](#why-a-new-thermostat-implementation)
3. a temporature sensor entity id which gives the temperature of the room in which the heater is installed, 4. a temperature sensor entity identifier which gives the temperature of the room in which the radiator is installed,
4. a temperature sensor entity giving the external temperature. If don't have any external sensor, you can use the local meteo integration 5. a temperature sensor entity giving the outside temperature. If you don't have an external sensor, you can use local weather integration
5. a cycle duration in minutes. At each cycle, the heater will be turned on then off for a calculated period in order to reach the targeted temperature (see [preset](#configure-the-preset-temperature) below), 6. a cycle duration in minutes. On each cycle, the heater will cycle on and then off for a calculated time to reach the target temperature (see [preset](#configure-the-preset-temperature) below),
6. Algorithm to use. Today only the TPI algorithm is available. See [algorithm](#algorithm) 7. minimum and maximum thermostat temperatures,
8. the list of features that will be used for this thermostat. Depending on your choices, the following configuration screens will appear or not.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
1. Calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**, 1. With the ```thermostat_over_switch``` type, calculation are done at each cycle. So in case of conditions change, you will have to wait for the next cycle to see a change. For this reason, the cycle should not be too long. **5 min is a good value**,
2. if the cycle is too short, the heater could never reach the target temperature indeed for heater with accumulation features and it will be unnecessary solicited 2. if the cycle is too short, the heater could never reach the target temperature indeed for heater with accumulation features and it will be unnecessary solicited
## Select the driven entity
Depending on your choice on the type of thermostat, you will have to choose a switch type entity or a climate type entity. Only compatible entities are shown.
For a ```thermostat_over_switch``` thermostat:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
The algorithm to be used today is limited to TPI is available. See [algorithm](#algorithm)
For a ```thermostat_over_climate``` thermostat:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
## Configure the TPI algorithm coefficients ## Configure the TPI algorithm coefficients
Click on 'Validate' on the previous page and you will get there: Click on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-tpi.png?raw=true)
@@ -139,7 +155,7 @@ The preset mode allows you to pre-configurate targeted temperature. Used in conj
5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component 5. ff you don't want to use the preseet, give 0 as temperature. The preset will then been ignored and will not displayed in the front component
## Configure the doors/windows turning on/off the thermostats ## Configure the doors/windows turning on/off the thermostats
Click on 'Validate' on the previous page and you will get there: If you choose the ```Window management``` feature, click on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-window.png?raw=true)
Give the following attributes: Give the following attributes:
@@ -153,7 +169,7 @@ And that's it ! your thermostat will turn off when the windows is open and be tu
2. If you don't have any window/door sensor in your room, just leave the sensor entity id empty 2. If you don't have any window/door sensor in your room, just leave the sensor entity id empty
## Configure the activity mode or motion detection ## Configure the activity mode or motion detection
Click on 'Validate' on the previous page and you will get there: If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true)
We will now see how to configure the new Activity mode. We will now see how to configure the new Activity mode.
@@ -176,7 +192,7 @@ For this to work, the climate thermostat should be in ``Activity`` preset mode.
## Configure the power management ## Configure the power management
Click on 'Validate' on the previous page and you will get there: If you choose the ```Power management``` feature, click on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-power.png?raw=true)
This feature allows you to regulate the power consumption of your radiators. Known as shedding, this feature allows you to limit the electrical power consumption of your heater if overpowering conditions are detected. Give a **sensor to the current power consumption of your house**, a **sensor to the max power** that should not be exceeded, the **power consumption of your heater** and the algorithm will not start a radiator if the max power will be exceeded after radiator starts. This feature allows you to regulate the power consumption of your radiators. Known as shedding, this feature allows you to limit the electrical power consumption of your heater if overpowering conditions are detected. Give a **sensor to the current power consumption of your house**, a **sensor to the max power** that should not be exceeded, the **power consumption of your heater** and the algorithm will not start a radiator if the max power will be exceeded after radiator starts.
@@ -192,7 +208,7 @@ This allows you to change the max power along time using a Scheduler or whatever
4. If you don't want to use this feature, just leave the entities id empty 4. If you don't want to use this feature, just leave the entities id empty
## Configure the presence or occupancy ## Configure the presence or occupancy
This feature allows you to dynamically changes the temperature of all configured Versatile thermostat's presets when nobody is at home or when someone comes back home. For this, you have to configure the temperature that will be used for each preset when presence is off. When the occupancy sensor turns to off, those tempoeratures will be used. When it turns on again the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature). If you choose the ```Presence management``` feature, this feature allows you to dynamically changes the temperature of all configured Versatile thermostat's presets when nobody is at home or when someone comes back home. For this, you have to configure the temperature that will be used for each preset when presence is off. When the occupancy sensor turns to off, those tempoeratures will be used. When it turns on again the "normal" temperature configured for the preset is used. See [preset management](#configure-the-preset-temperature).
To configure presence fills this form: To configure presence fills this form:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presence.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-presence.png?raw=true)
@@ -217,6 +233,8 @@ The first delay (minimal_activation_delay_sec) in sec in the minimum delay accep
The second delay (security_delay_min) is the maximal delay between two temperature measure before setting the preset to ``security`` and turning off the thermostat. If the temperature sensor is no more giving temperature measures, the thermostat and heater will turns off after this delay and the preset of the thermostat will be set to ``security``. This is useful to avoid overheating is the battery of your temperature sensor is too low. The second delay (security_delay_min) is the maximal delay between two temperature measure before setting the preset to ``security`` and turning off the thermostat. If the temperature sensor is no more giving temperature measures, the thermostat and heater will turns off after this delay and the preset of the thermostat will be set to ``security``. This is useful to avoid overheating is the battery of your temperature sensor is too low.
The third parameter (security_min_on_percent) is the minimal on_percent value below which the security preset won't be trigger. If you set it to ``0.00`` security preset will be trigger regardeless of the heating on_percent when there is a temperature loss, at the opposite ``1.00`` will never trigger the security preset.
See [exemple tuning](#examples-tuning) to have some commons tuning examples See [exemple tuning](#examples-tuning) to have some commons tuning examples
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
@@ -346,6 +364,7 @@ Custom attributes are the following:
| ``overpowering_state`` | The last known state of the overpowering sensor. None if power management is not configured | | ``overpowering_state`` | The last known state of the overpowering sensor. None if power management is not configured |
| ``presence_state`` | The last known state of the presence sensor. None if presence management is not configured | | ``presence_state`` | The last known state of the presence sensor. None if presence management is not configured |
| ``security_delay_min`` | The delay before setting the security mode when temperature sensor are off | | ``security_delay_min`` | The delay before setting the security mode when temperature sensor are off |
| ``security_min_on_percent`` | The minimal on_percent below which security preset won't be trigger |
| ``last_temperature_datetime`` | The date and time in ISO8866 format of the last internal temperature reception | | ``last_temperature_datetime`` | The date and time in ISO8866 format of the last internal temperature reception |
| ``last_ext_temperature_datetime`` | The date and time in ISO8866 format of the last external temperature reception | | ``last_ext_temperature_datetime`` | The date and time in ISO8866 format of the last external temperature reception |
| ``security_state`` | The security state. true or false | | ``security_state`` | The security state. true or false |
+352 -97
View File
@@ -20,7 +20,6 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.util.unit_system import UnitOfTemperature
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
@@ -34,17 +33,15 @@ from homeassistant.helpers import (
entity_platform, entity_platform,
) # , config_validation as cv ) # , config_validation as cv
from homeassistant.components.climate.const import ( from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
ATTR_PRESET_MODE, ATTR_PRESET_MODE,
# ATTR_FAN_MODE, # ATTR_FAN_MODE,
CURRENT_HVAC_COOL, HVACMode,
CURRENT_HVAC_HEAT, HVACAction,
CURRENT_HVAC_IDLE, # HVAC_MODE_COOL,
CURRENT_HVAC_OFF, # HVAC_MODE_HEAT,
HVAC_MODE_COOL, # HVAC_MODE_OFF,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_ACTIVITY, PRESET_ACTIVITY,
# PRESET_AWAY, # PRESET_AWAY,
PRESET_BOOST, PRESET_BOOST,
@@ -53,7 +50,8 @@ from homeassistant.components.climate.const import (
# PRESET_HOME, # PRESET_HOME,
PRESET_NONE, PRESET_NONE,
# PRESET_SLEEP, # PRESET_SLEEP,
SUPPORT_PRESET_MODE, ClimateEntityFeature,
# ClimateEntityFeature.PRESET_MODE,
# SUPPORT_TARGET_TEMPERATURE, # SUPPORT_TARGET_TEMPERATURE,
SERVICE_SET_FAN_MODE, SERVICE_SET_FAN_MODE,
SERVICE_SET_HUMIDITY, SERVICE_SET_HUMIDITY,
@@ -63,6 +61,13 @@ from homeassistant.components.climate.const import (
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
) )
# from homeassistant.components.climate import (
# CURRENT_HVAC_HEAT,
# HVACAction.IDLE,
# HVACAction.OFF,
# HVACAction.COOLING,
# )
from homeassistant.const import ( from homeassistant.const import (
# UnitOfTemperature, # UnitOfTemperature,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
@@ -111,6 +116,10 @@ from .const import (
SERVICE_SET_PRESET_TEMPERATURE, SERVICE_SET_PRESET_TEMPERATURE,
PRESET_AWAY_SUFFIX, PRESET_AWAY_SUFFIX,
CONF_SECURITY_DELAY_MIN, CONF_SECURITY_DELAY_MIN,
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SECURITY_DEFAULT_ON_PERCENT,
DEFAULT_SECURITY_MIN_ON_PERCENT,
DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
CONF_MINIMAL_ACTIVATION_DELAY, CONF_MINIMAL_ACTIVATION_DELAY,
CONF_TEMP_MAX, CONF_TEMP_MAX,
CONF_TEMP_MIN, CONF_TEMP_MIN,
@@ -120,6 +129,7 @@ from .const import (
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_CLIMATE, CONF_CLIMATE,
UnknownEntity, UnknownEntity,
EventType,
) )
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
@@ -176,7 +186,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# No more needed # No more needed
# _registry: dict[str, object] = {} # _registry: dict[str, object] = {}
def __init__(self, hass, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat.""" """Initialize the thermostat."""
super().__init__() super().__init__()
@@ -211,7 +221,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._presence_state = None self._presence_state = None
self._overpowering_state = None self._overpowering_state = None
self._should_relaunch_control_heating = None self._should_relaunch_control_heating = None
self._security_delay_min = None self._security_delay_min = None
self._security_min_on_percent = None
self._security_default_on_percent = None
self._security_state = None self._security_state = None
self._thermostat_type = None self._thermostat_type = None
@@ -220,6 +233,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._is_over_climate = False self._is_over_climate = False
self._underlying_climate = None self._underlying_climate = None
self._attr_translation_key = "versatile_thermostat"
self.post_init(entry_infos) self.post_init(entry_infos)
def post_init(self, entry_infos): def post_init(self, entry_infos):
@@ -232,7 +247,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
# convert entry_infos into usable attributes # convert entry_infos into usable attributes
presets = {} presets = {}
for (key, value) in CONF_PRESETS.items(): for key, value in CONF_PRESETS.items():
_LOGGER.debug("looking for key=%s, value=%s", key, value) _LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos: if value in entry_infos:
presets[key] = entry_infos.get(value) presets[key] = entry_infos.get(value)
@@ -240,7 +255,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug("value %s not found in Entry", value) _LOGGER.debug("value %s not found in Entry", value)
presets_away = {} presets_away = {}
for (key, value) in CONF_PRESETS_AWAY.items(): for key, value in CONF_PRESETS_AWAY.items():
_LOGGER.debug("looking for key=%s, value=%s", key, value) _LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos: if value in entry_infos:
presets_away[key] = entry_infos.get(value) presets_away[key] = entry_infos.get(value)
@@ -297,8 +312,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# TODO if self.ac_mode: # TODO if self.ac_mode:
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF] # self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
# else: # else:
self._hvac_list = [HVAC_MODE_HEAT, HVAC_MODE_OFF] self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
self._unit = UnitOfTemperature.CELSIUS
self._unit = self._hass.config.units.temperature_unit
# Will be restored if possible # Will be restored if possible
self._hvac_mode = None # HVAC_MODE_OFF self._hvac_mode = None # HVAC_MODE_OFF
self._saved_hvac_mode = self._hvac_mode self._saved_hvac_mode = self._hvac_mode
@@ -355,6 +371,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._tpi_coef_ext = 0 self._tpi_coef_ext = 0
self._security_delay_min = entry_infos.get(CONF_SECURITY_DELAY_MIN) self._security_delay_min = entry_infos.get(CONF_SECURITY_DELAY_MIN)
self._security_min_on_percent = (
entry_infos.get(CONF_SECURITY_MIN_ON_PERCENT)
or DEFAULT_SECURITY_MIN_ON_PERCENT
)
self._security_default_on_percent = (
entry_infos.get(CONF_SECURITY_DEFAULT_ON_PERCENT)
or DEFAULT_SECURITY_DEFAULT_ON_PERCENT
)
self._minimal_activation_delay = entry_infos.get(CONF_MINIMAL_ACTIVATION_DELAY) self._minimal_activation_delay = entry_infos.get(CONF_MINIMAL_ACTIVATION_DELAY)
self._last_temperature_mesure = datetime.now() self._last_temperature_mesure = datetime.now()
self._last_ext_temperature_mesure = datetime.now() self._last_ext_temperature_mesure = datetime.now()
@@ -383,7 +407,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Calculate all possible presets # Calculate all possible presets
self._attr_preset_modes = [PRESET_NONE] self._attr_preset_modes = [PRESET_NONE]
if len(presets): if len(presets):
self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE
for key, val in presets.items(): for key, val in presets.items():
if val != 0.0: if val != 0.0:
@@ -719,7 +743,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
float(old_state.attributes[ATTR_TEMPERATURE]) float(old_state.attributes[ATTR_TEMPERATURE])
) )
if old_state.attributes.get(ATTR_PRESET_MODE) in self._attr_preset_modes: old_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
# Never restore a Power or Security preset
if (
old_preset_mode in self._attr_preset_modes
and old_preset_mode not in HIDDEN_PRESETS
):
self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE) self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
self.save_preset_mode() self.save_preset_mode()
@@ -741,7 +770,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Set default state to off # Set default state to off
if not self._hvac_mode: if not self._hvac_mode:
self._hvac_mode = HVAC_MODE_OFF self._hvac_mode = HVACMode.OFF
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
_LOGGER.info( _LOGGER.info(
"%s - restored state is target_temp=%.1f, preset_mode=%s, hvac_mode=%s", "%s - restored state is target_temp=%.1f, preset_mode=%s, hvac_mode=%s",
@@ -774,9 +806,56 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
return self._hvac_list return self._hvac_list
@property
def fan_mode(self) -> str | None:
"""Return the fan setting.
Requires ClimateEntityFeature.FAN_MODE.
"""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.fan_mode
return None
@property
def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes.
Requires ClimateEntityFeature.FAN_MODE.
"""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.fan_modes
return []
@property
def swing_mode(self) -> str | None:
"""Return the swing setting.
Requires ClimateEntityFeature.SWING_MODE.
"""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.swing_mode
return None
@property
def swing_modes(self) -> list[str] | None:
"""Return the list of available swing modes.
Requires ClimateEntityFeature.SWING_MODE.
"""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.swing_modes
return None
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.temperature_unit
return self._unit return self._unit
@property @property
@@ -796,13 +875,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if self._is_over_climate and self._underlying_climate: if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.hvac_action return self._underlying_climate.hvac_action
if self._hvac_mode == HVAC_MODE_OFF: if self._hvac_mode == HVACMode.OFF:
return CURRENT_HVAC_OFF return HVACAction.OFF
if not self._is_device_active: if not self._is_device_active:
return CURRENT_HVAC_IDLE return HVACAction.IDLE
if self._ac_mode: if self._ac_mode:
return CURRENT_HVAC_COOL return HVACAction.COOLING
return CURRENT_HVAC_HEAT return HVACAction.HEATING
@property @property
def target_temperature(self): def target_temperature(self):
@@ -820,20 +899,114 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
@property @property
def _is_device_active(self): def _is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
if self._is_over_climate or not self.hass.states.get(self._heater_entity_id): if self._is_over_climate:
return None if self._underlying_climate:
return self._underlying_climate.hvac_action not in [
return self.hass.states.is_state(self._heater_entity_id, STATE_ON) HVACAction.IDLE,
HVACAction.OFF,
]
else:
return None
else:
return self.hass.states.is_state(self._heater_entity_id, STATE_ON)
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the sensor temperature.""" """Return the sensor temperature."""
return self._cur_temp return self._cur_temp
@property
def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.target_temperature_step
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach.
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
"""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.target_temperature_high
return None
@property
def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach.
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
"""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.target_temperature_low
return None
@property
def is_aux_heat(self) -> bool | None:
"""Return true if aux heater.
Requires ClimateEntityFeature.AUX_HEAT.
"""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.is_aux_heat
return None
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.turn_aux_heat_on()
raise NotImplementedError()
async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if self._is_over_climate and self._underlying_climate:
await self._underlying_climate.async_turn_aux_heat_on()
raise NotImplementedError()
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
if self._is_over_climate and self._underlying_climate:
return self._underlying_climate.turn_aux_heat_off()
raise NotImplementedError()
async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
if self._is_over_climate and self._underlying_climate:
await self._underlying_climate.async_turn_aux_heat_off()
raise NotImplementedError()
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp.
Requires ClimateEntityFeature.PRESET_MODE.
"""
return self._attr_preset_mode
@property
def preset_modes(self) -> list[str] | None:
"""Return a list of available preset modes.
Requires ClimateEntityFeature.PRESET_MODE.
"""
return self._attr_preset_modes
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode.""" """Set new target hvac mode."""
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode) _LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
if hvac_mode is None:
return
if self._is_over_climate and self._underlying_climate: if self._is_over_climate and self._underlying_climate:
data = {ATTR_ENTITY_ID: self._climate_entity_id, "hvac_mode": hvac_mode} data = {ATTR_ENTITY_ID: self._climate_entity_id, "hvac_mode": hvac_mode}
await self.hass.services.async_call( await self.hass.services.async_call(
@@ -842,14 +1015,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# await self._underlying_climate.async_set_hvac_mode(hvac_mode) # await self._underlying_climate.async_set_hvac_mode(hvac_mode)
self._hvac_mode = hvac_mode # self._underlying_climate.hvac_mode self._hvac_mode = hvac_mode # self._underlying_climate.hvac_mode
else: else:
if hvac_mode == HVAC_MODE_HEAT: if hvac_mode == HVACMode.HEAT:
self._hvac_mode = HVAC_MODE_HEAT self._hvac_mode = HVACMode.HEAT
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
elif hvac_mode == HVAC_MODE_COOL: elif hvac_mode == HVACMode.COOL:
self._hvac_mode = HVAC_MODE_COOL self._hvac_mode = HVACMode.COOL
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
elif hvac_mode == HVAC_MODE_OFF: elif hvac_mode == HVACMode.OFF:
self._hvac_mode = HVAC_MODE_OFF self._hvac_mode = HVACMode.OFF
if self._is_device_active: if self._is_device_active:
await self._async_underlying_entity_turn_off() await self._async_underlying_entity_turn_off()
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
@@ -857,8 +1030,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.error("Unrecognized hvac mode: %s", hvac_mode) _LOGGER.error("Unrecognized hvac mode: %s", hvac_mode)
return return
# Ensure we update the current operation after changing the mode # Ensure we update the current operation after changing the mode
self.reset_last_temperature_time()
self.update_custom_attributes() self.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
async def async_set_preset_mode(self, preset_mode): async def async_set_preset_mode(self, preset_mode):
"""Set new preset mode.""" """Set new preset mode."""
@@ -879,6 +1055,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if preset_mode == self._attr_preset_mode and not force: if preset_mode == self._attr_preset_mode and not force:
# I don't think we need to call async_write_ha_state if we didn't change the state # I don't think we need to call async_write_ha_state if we didn't change the state
return return
old_preset_mode = self._attr_preset_mode
if preset_mode == PRESET_NONE: if preset_mode == PRESET_NONE:
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
if self._saved_target_temp: if self._saved_target_temp:
@@ -894,9 +1071,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self.find_preset_temp(preset_mode) self.find_preset_temp(preset_mode)
) )
self.save_preset_mode() self.reset_last_temperature_time(old_preset_mode)
self.save_preset_mode()
self.recalculate() self.recalculate()
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
def reset_last_temperature_time(self, old_preset_mode=None):
"""Reset to now the last temperature time if conditions are satisfied"""
if (
self._attr_preset_mode not in HIDDEN_PRESETS
and old_preset_mode not in HIDDEN_PRESETS
):
self._last_temperature_mesure = (
self._last_ext_temperature_mesure
) = datetime.now()
def find_preset_temp(self, preset_mode): def find_preset_temp(self, preset_mode):
"""Find the right temperature of a preset considering the presence if configured""" """Find the right temperature of a preset considering the presence if configured"""
@@ -1006,7 +1195,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def _async_temperature_changed(self, event): async def _async_temperature_changed(self, event):
"""Handle temperature changes.""" """Handle temperature changes."""
new_state = event.data.get("new_state") new_state = event.data.get("new_state")
_LOGGER.info( _LOGGER.debug(
"%s - Temperature changed. Event.new_state is %s", "%s - Temperature changed. Event.new_state is %s",
self, self,
new_state, new_state,
@@ -1021,7 +1210,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def _async_ext_temperature_changed(self, event): async def _async_ext_temperature_changed(self, event):
"""Handle external temperature changes.""" """Handle external temperature changes."""
new_state = event.data.get("new_state") new_state = event.data.get("new_state")
_LOGGER.info( _LOGGER.debug(
"%s - external Temperature changed. Event.new_state is %s", "%s - external Temperature changed. Event.new_state is %s",
self, self,
new_state, new_state,
@@ -1080,10 +1269,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await self.restore_hvac_mode() await self.restore_hvac_mode()
elif self._window_state == STATE_ON: elif self._window_state == STATE_ON:
_LOGGER.info( _LOGGER.info(
"%s - Window is open. Set hvac_mode to '%s'", self, HVAC_MODE_OFF "%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
) )
self.save_hvac_mode() self.save_hvac_mode()
await self.async_set_hvac_mode(HVAC_MODE_OFF) await self.async_set_hvac_mode(HVACMode.OFF)
self.update_custom_attributes() self.update_custom_attributes()
if self._window_call_cancel: if self._window_call_cancel:
@@ -1155,7 +1344,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def _check_switch_initial_state(self): async def _check_switch_initial_state(self):
"""Prevent the device from keep running if HVAC_MODE_OFF.""" """Prevent the device from keep running if HVAC_MODE_OFF."""
_LOGGER.debug("%s - Calling _check_switch_initial_state", self) _LOGGER.debug("%s - Calling _check_switch_initial_state", self)
if self._hvac_mode == HVAC_MODE_OFF and self._is_device_active: if self._hvac_mode == HVACMode.OFF and self._is_device_active:
_LOGGER.warning( _LOGGER.warning(
"The climate mode is OFF, but the switch device is ON. Turning off device %s", "The climate mode is OFF, but the switch device is ON. Turning off device %s",
self._heater_entity_id, self._heater_entity_id,
@@ -1185,9 +1374,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
# old_state = event.data.get("old_state") # old_state = event.data.get("old_state")
if new_state is None or new_state.state not in [ if new_state is None or new_state.state not in [
HVAC_MODE_COOL, HVACMode.OFF,
HVAC_MODE_HEAT, HVACMode.HEAT,
HVAC_MODE_OFF, HVACMode.COOL,
HVACMode.HEAT_COOL,
HVACMode.DRY,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
]: ]:
return return
self._hvac_mode = new_state.state self._hvac_mode = new_state.state
@@ -1377,11 +1570,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def _async_underlying_entity_turn_off(self): async def _async_underlying_entity_turn_off(self):
"""Turn heater toggleable device off.""" """Turn heater toggleable device off."""
if not self._is_over_climate: if not self._is_over_climate:
_LOGGER.debug(
"%s - Stopping underlying switch %s", self, self._heater_entity_id
)
data = {ATTR_ENTITY_ID: self._heater_entity_id} data = {ATTR_ENTITY_ID: self._heater_entity_id}
await self.hass.services.async_call( await self.hass.services.async_call(
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
) )
else: else:
_LOGGER.debug(
"%s - Stopping underlying switch %s", self, self._climate_entity_id
)
data = {ATTR_ENTITY_ID: self._climate_entity_id} data = {ATTR_ENTITY_ID: self._climate_entity_id}
await self.hass.services.async_call( await self.hass.services.async_call(
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
@@ -1443,11 +1642,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._device_power, self._device_power,
) )
ret = self._current_power + self._device_power >= self._current_power_max ret = self._current_power + self._device_power >= self._current_power_max
if ( if not self._overpowering_state and ret and not self._hvac_mode == HVACMode.OFF:
not self._overpowering_state
and ret
and not self._hvac_mode == HVAC_MODE_OFF
):
_LOGGER.warning( _LOGGER.warning(
"%s - overpowering is detected. Heater preset will be set to 'power'", "%s - overpowering is detected. Heater preset will be set to 'power'",
self, self,
@@ -1457,6 +1652,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self.save_preset_mode() self.save_preset_mode()
await self._async_underlying_entity_turn_off() await self._async_underlying_entity_turn_off()
await self._async_set_preset_mode_internal(PRESET_POWER) await self._async_set_preset_mode_internal(PRESET_POWER)
self.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": self._current_power,
"device_power": self._device_power,
"current_power_max": self._current_power_max,
},
)
# Check if we need to remove the POWER preset # Check if we need to remove the POWER preset
if ( if (
@@ -1472,6 +1676,15 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if self._is_over_climate: if self._is_over_climate:
await self.restore_hvac_mode() await self.restore_hvac_mode()
await self.restore_preset_mode() await self.restore_preset_mode()
self.send_event(
EventType.POWER_EVENT,
{
"type": "end",
"current_power": self._current_power,
"device_power": self._device_power,
"current_power_max": self._current_power_max,
},
)
self._overpowering_state = ret self._overpowering_state = ret
return self._overpowering_state return self._overpowering_state
@@ -1483,25 +1696,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
delta_ext_temp = ( delta_ext_temp = (
now - self._last_ext_temperature_mesure now - self._last_ext_temperature_mesure
).total_seconds() / 60.0 ).total_seconds() / 60.0
_LOGGER.debug(
"%s - checking security delta_temp=%.1f delta_ext_temp=%.1f",
self,
delta_temp,
delta_ext_temp,
)
temp_cond: bool = ( temp_cond: bool = (
delta_temp > self._security_delay_min delta_temp > self._security_delay_min
or delta_ext_temp > self._security_delay_min or delta_ext_temp > self._security_delay_min
) )
climate_cond: bool = self._is_over_climate and self.hvac_action not in [ climate_cond: bool = self._is_over_climate and self.hvac_action not in [
CURRENT_HVAC_COOL, HVACAction.COOLING,
CURRENT_HVAC_IDLE, HVACAction.IDLE,
] ]
switch_cond: bool = ( switch_cond: bool = (
not self._is_over_climate not self._is_over_climate
and self._prop_algorithm is not None and self._prop_algorithm is not None
and self._prop_algorithm.on_percent > 0.75 and self._prop_algorithm.calculated_on_percent
> self._security_min_on_percent
) )
ret = False ret = False
@@ -1517,24 +1725,63 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
ret = True ret = True
_LOGGER.debug(
"%s - checking security delta_temp=%.1f delta_ext_temp=%.1f temp_cond=%s climate_cond=%s switch_cond=%s",
self,
delta_temp,
delta_ext_temp,
temp_cond,
climate_cond,
switch_cond,
)
if temp_cond and switch_cond: if temp_cond and switch_cond:
if not self._security_state: if not self._security_state:
_LOGGER.warning( _LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent is high (%.2f). Set it into security mode", "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f) is over defined value (%.2f). Set it into security mode",
self, self,
self._security_delay_min, self._security_delay_min,
delta_temp, delta_temp,
delta_ext_temp, delta_ext_temp,
self._prop_algorithm.on_percent, self._prop_algorithm.on_percent,
self._security_min_on_percent,
) )
ret = True ret = True
if not self._security_state and temp_cond:
self.send_event(
EventType.TEMPERATURE_EVENT,
{
"last_temperature_mesure": self._last_temperature_mesure.isoformat(),
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.isoformat(),
"current_temp": self._cur_temp,
"current_ext_temp": self._cur_ext_temp,
"target_temp": self.target_temperature,
},
)
if not self._security_state and ret: if not self._security_state and ret:
self._security_state = ret self._security_state = ret
self.save_hvac_mode() self.save_hvac_mode()
self.save_preset_mode() self.save_preset_mode()
await self._async_set_preset_mode_internal(PRESET_SECURITY) await self._async_set_preset_mode_internal(PRESET_SECURITY)
await self.async_set_hvac_mode(HVAC_MODE_OFF) # Turn off the underlying climate or heater if security default on_percent is 0
if self._is_over_climate or self._security_default_on_percent <= 0.0:
await self.async_set_hvac_mode(HVACMode.OFF)
if self._prop_algorithm:
self._prop_algorithm.set_security(self._security_default_on_percent)
self.send_event(
EventType.SECURITY_EVENT,
{
"type": "start",
"last_temperature_mesure": self._last_temperature_mesure.isoformat(),
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.isoformat(),
"current_temp": self._cur_temp,
"current_ext_temp": self._cur_ext_temp,
"target_temp": self.target_temperature,
},
)
if ( if (
self._security_state self._security_state
@@ -1548,15 +1795,30 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._saved_preset_mode, self._saved_preset_mode,
) )
self._security_state = ret self._security_state = ret
await self.restore_hvac_mode() # Restore hvac_mode if previously saved
if self._is_over_climate or self._security_default_on_percent <= 0.0:
await self.restore_hvac_mode()
await self.restore_preset_mode() await self.restore_preset_mode()
if self._prop_algorithm:
self._prop_algorithm.unset_security()
self.send_event(
EventType.SECURITY_EVENT,
{
"type": "end",
"last_temperature_mesure": self._last_temperature_mesure.isoformat(),
"last_ext_temperature_mesure": self._last_ext_temperature_mesure.isoformat(),
"current_temp": self._cur_temp,
"current_ext_temp": self._cur_ext_temp,
"target_temp": self.target_temperature,
},
)
return ret return ret
async def _async_control_heating(self, force=False, _=None): async def _async_control_heating(self, force=False, _=None):
"""The main function used to run the calculation at each cycle""" """The main function used to run the calculation at each cycle"""
_LOGGER.info( _LOGGER.debug(
"%s - Checking new cycle. hvac_mode=%s, security_state=%s, preset_mode=%s", "%s - Checking new cycle. hvac_mode=%s, security_state=%s, preset_mode=%s",
self, self,
self._hvac_mode, self._hvac_mode,
@@ -1567,22 +1829,26 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Check overpowering condition # Check overpowering condition
overpowering: bool = await self.check_overpowering() overpowering: bool = await self.check_overpowering()
if overpowering: if overpowering:
_LOGGER.debug("%s - End of cycle (0)", self) _LOGGER.debug("%s - End of cycle (overpowering)", self)
return return
security: bool = await self.check_security() security: bool = await self.check_security()
if security: if security and self._is_over_climate:
_LOGGER.debug("%s - End of cycle (1)", self) _LOGGER.debug("%s - End of cycle (security and over climate)", self)
return return
# Stop here if we are off # Stop here if we are off
if self._hvac_mode == HVAC_MODE_OFF: if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF)", self)
if self._is_device_active:
await self._async_underlying_entity_turn_off()
return return
if not self._is_over_climate: if not self._is_over_climate:
on_time_sec: int = self._prop_algorithm.on_time_sec on_time_sec: int = self._prop_algorithm.on_time_sec
off_time_sec: int = self._prop_algorithm.off_time_sec off_time_sec: int = self._prop_algorithm.off_time_sec
_LOGGER.info(
_LOGGER.debug(
"%s - Checking new cycle. on_time_sec=%.0f, off_time_sec=%.0f, security_state=%s, preset_mode=%s", "%s - Checking new cycle. on_time_sec=%.0f, off_time_sec=%.0f, security_state=%s, preset_mode=%s",
self, self,
on_time_sec, on_time_sec,
@@ -1606,7 +1872,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug("%s - End of cycle (2)", self) _LOGGER.debug("%s - End of cycle (2)", self)
return return
if self._hvac_mode == HVAC_MODE_HEAT and on_time_sec > 0: if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
async def _turn_on_off_later( async def _turn_on_off_later(
on: bool, time, heater_action, next_cycle_action on: bool, time, heater_action, next_cycle_action
@@ -1616,14 +1882,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._async_cancel_cycle = None self._async_cancel_cycle = None
_LOGGER.debug("%s - Stopping cycle during calculation", self) _LOGGER.debug("%s - Stopping cycle during calculation", self)
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
if self._is_device_active:
await self._async_underlying_entity_turn_off()
return
if on: if on:
security = ( if await self.check_overpowering():
await self.check_security()
or await self.check_overpowering()
)
if security:
_LOGGER.debug("%s - End of cycle (3)", self) _LOGGER.debug("%s - End of cycle (3)", self)
return return
# Security mode could have change the on_time percent
await self.check_security()
time = self._prop_algorithm.on_time_sec
action_label = "start" if on else "stop" action_label = "start" if on else "stop"
if self._should_relaunch_control_heating: if self._should_relaunch_control_heating:
@@ -1638,7 +1909,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if time > 0: if time > 0:
_LOGGER.info( _LOGGER.info(
"%s - !!! %s heating for %d min %d sec", "%s - %s heating for %d min %d sec",
self, self,
action_label, action_label,
time // 60, time // 60,
@@ -1733,6 +2004,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"overpowering_state": self._overpowering_state, "overpowering_state": self._overpowering_state,
"presence_state": self._presence_state, "presence_state": self._presence_state,
"security_delay_min": self._security_delay_min, "security_delay_min": self._security_delay_min,
"security_min_on_percent": self._security_min_on_percent,
"security_default_on_percent": self._security_default_on_percent,
"last_temperature_datetime": self._last_temperature_mesure.isoformat(), "last_temperature_datetime": self._last_temperature_mesure.isoformat(),
"last_ext_temperature_datetime": self._last_ext_temperature_mesure.isoformat(), "last_ext_temperature_datetime": self._last_ext_temperature_mesure.isoformat(),
"security_state": self._security_state, "security_state": self._security_state,
@@ -1823,28 +2096,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await self._async_set_preset_mode_internal(preset, force=True) await self._async_set_preset_mode_internal(preset, force=True)
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
# No more needed def send_event(self, event_type: EventType, data: dict):
"""Send an event"""
_LOGGER.info("%s - Sending event %s with data: %s", self, event_type, data)
# @classmethod data["entity_id"] = self.entity_id
# def add_entity(cls, entry_id, entity): data["name"] = self.name
# """Adds an entity into the VersatileRegistry entities""" data["state_attributes"] = self.state_attributes
# _LOGGER.debug("Adding entity %s", entry_id) self._hass.bus.fire(event_type.value, data)
# cls._registry[entry_id] = entity
# _LOGGER.debug("Entity registry is now %s", cls._registry)
#
# @classmethod
# async def update_entity(cls, entry_id, infos):
# """Updates an existing entity referenced by entry_id with the infos in arguments"""
# entity: VersatileThermostat = cls._registry.get(entry_id)
# if entity is None:
# _LOGGER.warning(
# "Tries to update VersatileThermostat entity %s but was not found in thermostat registry",
# entry_id,
# )
# return
#
# _LOGGER.debug("We have found the entity to update")
# entity.post_init(infos)
#
# await entity.async_added_to_hass()
@@ -8,25 +8,20 @@ from collections.abc import Mapping
import voluptuous as vol import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.const import TEMPERATURE, UnitOfPower
from homeassistant.util.unit_system import TEMPERATURE_UNITS
from homeassistant.core import callback, async_get_hass from homeassistant.core import callback
from homeassistant.config_entries import ( from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow as HAConfigFlow, ConfigFlow as HAConfigFlow,
OptionsFlow, OptionsFlow,
) )
from homeassistant.data_entry_flow import FlowHandler from homeassistant.data_entry_flow import FlowHandler, FlowResult
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_registry import (
RegistryEntry, from homeassistant.helpers import selector
async_get,
)
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.input_boolean import ( from homeassistant.components.input_boolean import (
@@ -39,6 +34,7 @@ from homeassistant.components.input_number import (
) )
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from .const import ( from .const import (
@@ -67,6 +63,10 @@ from .const import (
CONF_PRESENCE_SENSOR, CONF_PRESENCE_SENSOR,
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
CONF_SECURITY_DELAY_MIN, CONF_SECURITY_DELAY_MIN,
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SECURITY_DEFAULT_ON_PERCENT,
DEFAULT_SECURITY_MIN_ON_PERCENT,
DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
CONF_MINIMAL_ACTIVATION_DELAY, CONF_MINIMAL_ACTIVATION_DELAY,
CONF_TEMP_MAX, CONF_TEMP_MAX,
CONF_TEMP_MIN, CONF_TEMP_MIN,
@@ -125,35 +125,33 @@ def add_suggested_values_to_schema(
return vol.Schema(schema) return vol.Schema(schema)
def is_temperature_sensor(sensor: RegistryEntry): # def is_temperature_sensor(sensor: RegistryEntry):
"""Check if a registryEntry is a temperature sensor or assimilable to a temperature sensor""" # """Check if a registryEntry is a temperature sensor or assimilable to a temperature sensor"""
if not sensor.entity_id.startswith( # if not sensor.entity_id.startswith(
INPUT_NUMBER_DOMAIN # INPUT_NUMBER_DOMAIN
) and not sensor.entity_id.startswith(SENSOR_DOMAIN): # ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
return False # return False
return ( # return (
sensor.device_class == TEMPERATURE # sensor.device_class == TEMPERATURE
or sensor.original_device_class == TEMPERATURE # or sensor.original_device_class == TEMPERATURE
or sensor.unit_of_measurement in TEMPERATURE_UNITS # or sensor.unit_of_measurement in TEMPERATURE_UNITS
) # )
#
#
def is_power_sensor(sensor: RegistryEntry): # def is_power_sensor(sensor: RegistryEntry):
"""Check if a registryEntry is a power sensor or assimilable to a temperature sensor""" # """Check if a registryEntry is a power sensor or assimilable to a temperature sensor"""
if not sensor.entity_id.startswith( # if not sensor.entity_id.startswith(
INPUT_NUMBER_DOMAIN # INPUT_NUMBER_DOMAIN
) and not sensor.entity_id.startswith(SENSOR_DOMAIN): # ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
return False # return False
return ( # return (
# sensor.device_class == TEMPERATURE # sensor.unit_of_measurement
# or sensor.original_device_class == TEMPERATURE # in [
sensor.unit_of_measurement # UnitOfPower.KILO_WATT,
in [ # UnitOfPower.WATT,
UnitOfPower.KILO_WATT, # UnitOfPower.BTU_PER_HOUR,
UnitOfPower.WATT, # ]
UnitOfPower.BTU_PER_HOUR, # )
]
)
class VersatileThermostatBaseConfigFlow(FlowHandler): class VersatileThermostatBaseConfigFlow(FlowHandler):
@@ -166,55 +164,43 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
super().__init__() super().__init__()
_LOGGER.debug("CTOR BaseConfigFlow infos: %s", infos) _LOGGER.debug("CTOR BaseConfigFlow infos: %s", infos)
self._infos = infos self._infos = infos
self.hass = async_get_hass() is_empty: bool = not bool(infos)
ent_reg = async_get(hass=self.hass) # Fix features selection depending to infos
self._infos[CONF_USE_WINDOW_FEATURE] = (
climates = [] is_empty or self._infos.get(CONF_WINDOW_SENSOR) is not None
switches = [] )
temp_sensors = [] self._infos[CONF_USE_MOTION_FEATURE] = (
power_sensors = [] is_empty or self._infos.get(CONF_MOTION_SENSOR) is not None
window_sensors = [] )
presence_sensors = [] self._infos[CONF_USE_POWER_FEATURE] = is_empty or (
self._infos.get(CONF_POWER_SENSOR) is not None
k: str and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
for k in ent_reg.entities: )
v: RegistryEntry = ent_reg.entities[k] self._infos[CONF_USE_PRESENCE_FEATURE] = (
_LOGGER.debug("Looking entity: %s", k) is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None
# if k.startswith(CLIMATE_DOMAIN) and ( )
# infos is None or k != infos.get("entity_id")
# ):
# _LOGGER.debug("Climate !")
# climates.append(k)
if k.startswith(SWITCH_DOMAIN) or k.startswith(INPUT_BOOLEAN_DOMAIN):
_LOGGER.debug("Switch !")
switches.append(k)
elif is_temperature_sensor(v):
_LOGGER.debug("Temperature sensor !")
temp_sensors.append(k)
elif is_power_sensor(v):
_LOGGER.debug("Power sensor !")
power_sensors.append(k)
elif k.startswith(PERSON_DOMAIN):
_LOGGER.debug("Presence sensor !")
presence_sensors.append(k)
# window sensor
if k.startswith(INPUT_BOOLEAN_DOMAIN):
_LOGGER.debug("Window or presence sensor !")
window_sensors.append(k)
presence_sensors.append(k)
# Special case for climates which are not in EntityRegistry
climates = self.find_all_climates()
self.STEP_USER_DATA_SCHEMA = vol.Schema( self.STEP_USER_DATA_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required( vol.Required(
CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH CONF_THERMOSTAT_TYPE, default=CONF_THERMOSTAT_SWITCH
): vol.In(CONF_THERMOSTAT_TYPES), ): selector.SelectSelector(
vol.Required(CONF_TEMP_SENSOR): vol.In(temp_sensors), selector.SelectSelectorConfig(
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): vol.In(temp_sensors), options=CONF_THERMOSTAT_TYPES, translation_key="thermostat_type"
)
),
vol.Required(CONF_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
),
),
# vol.In(temp_sensors),
vol.Required(CONF_EXTERNAL_TEMP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
),
), # vol.In(temp_sensors),
vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int, vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float), vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float), vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
@@ -227,7 +213,11 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self.STEP_THERMOSTAT_SWITCH = vol.Schema( self.STEP_THERMOSTAT_SWITCH = vol.Schema(
{ {
vol.Required(CONF_HEATER): vol.In(switches), vol.Required(CONF_HEATER): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
), # vol.In(switches),
vol.Required( vol.Required(
CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI
): vol.In( ): vol.In(
@@ -240,7 +230,9 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self.STEP_THERMOSTAT_CLIMATE = vol.Schema( self.STEP_THERMOSTAT_CLIMATE = vol.Schema(
{ {
vol.Required(CONF_CLIMATE): vol.In(climates), vol.Required(CONF_CLIMATE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
), # vol.In(climates),
} }
) )
@@ -260,14 +252,22 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self.STEP_WINDOW_DATA_SCHEMA = vol.Schema( self.STEP_WINDOW_DATA_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_WINDOW_SENSOR): vol.In(window_sensors), vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
), # vol.In(window_sensors),
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
} }
) )
self.STEP_MOTION_DATA_SCHEMA = vol.Schema( self.STEP_MOTION_DATA_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_MOTION_SENSOR): vol.In(window_sensors), vol.Optional(CONF_MOTION_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[BINARY_SENSOR_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
), # vol.In(window_sensors),
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int, vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int,
vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In( vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In(
CONF_PRESETS_SELECTIONABLE CONF_PRESETS_SELECTIONABLE
@@ -280,8 +280,16 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self.STEP_POWER_DATA_SCHEMA = vol.Schema( self.STEP_POWER_DATA_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_POWER_SENSOR): vol.In(power_sensors), vol.Optional(CONF_POWER_SENSOR): selector.EntitySelector(
vol.Optional(CONF_MAX_POWER_SENSOR): vol.In(power_sensors), selector.EntitySelectorConfig(
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
),
), # vol.In(power_sensors),
vol.Optional(CONF_MAX_POWER_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SENSOR_DOMAIN, INPUT_NUMBER_DOMAIN]
),
), # vol.In(power_sensors),
vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float), vol.Optional(CONF_DEVICE_POWER, default="1"): vol.Coerce(float),
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float), vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
} }
@@ -289,7 +297,15 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
self.STEP_PRESENCE_DATA_SCHEMA = vol.Schema( self.STEP_PRESENCE_DATA_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_PRESENCE_SENSOR): vol.In(presence_sensors), vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[
PERSON_DOMAIN,
BINARY_SENSOR_DOMAIN,
INPUT_BOOLEAN_DOMAIN,
]
),
), # vol.In(presence_sensors),
} }
).extend( ).extend(
{ {
@@ -304,6 +320,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
CONF_MINIMAL_ACTIVATION_DELAY, default=10 CONF_MINIMAL_ACTIVATION_DELAY, default=10
): cv.positive_int, ): cv.positive_int,
vol.Required(CONF_SECURITY_DELAY_MIN, default=60): cv.positive_int, vol.Required(CONF_SECURITY_DELAY_MIN, default=60): cv.positive_int,
vol.Required(
CONF_SECURITY_MIN_ON_PERCENT,
default=DEFAULT_SECURITY_MIN_ON_PERCENT,
): vol.Coerce(float),
vol.Required(
CONF_SECURITY_DEFAULT_ON_PERCENT,
default=DEFAULT_SECURITY_DEFAULT_ON_PERCENT,
): vol.Coerce(float),
} }
) )
@@ -601,8 +625,18 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_presets user_input=%s", user_input "Into OptionsFlowHandler.async_step_presets user_input=%s", user_input
) )
next_step = self.async_step_advanced
if self._infos[CONF_USE_WINDOW_FEATURE]:
next_step = self.async_step_window
elif self._infos[CONF_USE_MOTION_FEATURE]:
next_step = self.async_step_motion
elif self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step( return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, self.async_step_window "presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
) )
async def async_step_window(self, user_input: dict | None = None) -> FlowResult: async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
@@ -611,8 +645,15 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_window user_input=%s", user_input "Into OptionsFlowHandler.async_step_window user_input=%s", user_input
) )
next_step = self.async_step_advanced
if self._infos[CONF_USE_MOTION_FEATURE]:
next_step = self.async_step_motion
elif self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step( return await self.generic_step(
"window", self.STEP_WINDOW_DATA_SCHEMA, user_input, self.async_step_motion "window", self.STEP_WINDOW_DATA_SCHEMA, user_input, next_step
) )
async def async_step_motion(self, user_input: dict | None = None) -> FlowResult: async def async_step_motion(self, user_input: dict | None = None) -> FlowResult:
@@ -621,8 +662,14 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_motion user_input=%s", user_input "Into OptionsFlowHandler.async_step_motion user_input=%s", user_input
) )
next_step = self.async_step_advanced
if self._infos[CONF_USE_POWER_FEATURE]:
next_step = self.async_step_power
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step( return await self.generic_step(
"motion", self.STEP_MOTION_DATA_SCHEMA, user_input, self.async_step_power "motion", self.STEP_MOTION_DATA_SCHEMA, user_input, next_step
) )
async def async_step_power(self, user_input: dict | None = None) -> FlowResult: async def async_step_power(self, user_input: dict | None = None) -> FlowResult:
@@ -631,11 +678,15 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_power user_input=%s", user_input "Into OptionsFlowHandler.async_step_power user_input=%s", user_input
) )
next_step = self.async_step_advanced
if self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step( return await self.generic_step(
"power", "power",
self.STEP_POWER_DATA_SCHEMA, self.STEP_POWER_DATA_SCHEMA,
user_input, user_input,
self.async_step_presence, next_step,
) )
async def async_step_presence(self, user_input: dict | None = None) -> FlowResult: async def async_step_presence(self, user_input: dict | None = None) -> FlowResult:
@@ -666,34 +717,20 @@ class VersatileThermostatOptionsFlowHandler(
async def async_end(self): async def async_end(self):
"""Finalization of the ConfigEntry creation""" """Finalization of the ConfigEntry creation"""
_LOGGER.debug( if not self._infos[CONF_USE_WINDOW_FEATURE]:
"ConfigFlow.async_finalize - updating entry with: %s", self._infos self._infos[CONF_WINDOW_SENSOR] = None
) if not self._infos[CONF_USE_MOTION_FEATURE]:
# Find eventual existing entity to update it self._infos[CONF_MOTION_SENSOR] = None
# removing entities from registry (they will be recreated) if not self._infos[CONF_USE_POWER_FEATURE]:
self._infos[CONF_POWER_SENSOR] = None
self._infos[CONF_MAX_POWER_SENSOR] = None
if not self._infos[CONF_USE_PRESENCE_FEATURE]:
self._infos[CONF_PRESENCE_SENSOR] = None
# No need to do that. Only the update_listener on __init__.py is necessary
# ent_reg = entity_registry.async_get(self.hass)
# for entry in entity_registry.async_entries_for_config_entry(
# ent_reg, self.config_entry.entry_id
# ):
# _LOGGER.info(
# "Removing entity %s due to configuration change", entry.entity_id
# )
# ent_reg.async_remove(entry.entity_id)
# _LOGGER.debug(
# "We have found entities to update: %s", self.config_entry.entry_id
# )
# await VersatileThermostat.update_entity(self.config_entry.entry_id, self._infos)
# for entity_id in reg_entities.values():
# ent_reg.async_remove(entity_id)
#
_LOGGER.info( _LOGGER.info(
"Recreating entry %s due to configuration change", "Recreating entry %s due to configuration change. New config is now: %s",
self.config_entry.entry_id, self.config_entry.entry_id,
self._infos,
) )
self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos) self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos)
return self.async_create_entry(title=None, data=None) return self.async_create_entry(title=None, data=None)
@@ -1,5 +1,6 @@
"""Constants for the Versatile Thermostat integration.""" """Constants for the Versatile Thermostat integration."""
from enum import Enum
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
# PRESET_ACTIVITY, # PRESET_ACTIVITY,
@@ -44,6 +45,8 @@ CONF_MINIMAL_ACTIVATION_DELAY = "minimal_activation_delay"
CONF_TEMP_MIN = "temp_min" CONF_TEMP_MIN = "temp_min"
CONF_TEMP_MAX = "temp_max" CONF_TEMP_MAX = "temp_max"
CONF_SECURITY_DELAY_MIN = "security_delay_min" CONF_SECURITY_DELAY_MIN = "security_delay_min"
CONF_SECURITY_MIN_ON_PERCENT = "security_min_on_percent"
CONF_SECURITY_DEFAULT_ON_PERCENT = "security_default_on_percent"
CONF_THERMOSTAT_TYPE = "thermostat_type" CONF_THERMOSTAT_TYPE = "thermostat_type"
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch" CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate" CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
@@ -102,6 +105,8 @@ ALL_CONF = (
CONF_TEMP_MIN, CONF_TEMP_MIN,
CONF_TEMP_MAX, CONF_TEMP_MAX,
CONF_SECURITY_DELAY_MIN, CONF_SECURITY_DELAY_MIN,
CONF_SECURITY_MIN_ON_PERCENT,
CONF_SECURITY_DEFAULT_ON_PERCENT,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
@@ -126,6 +131,17 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
class EventType(Enum):
SECURITY_EVENT: str = "versatile_thermostat_security_event"
POWER_EVENT: str = "versatile_thermostat_power_event"
TEMPERATURE_EVENT: str = "versatile_thermostat_temperature_event"
HVAC_MODE_EVENT: str = "versatile_thermostat_hvac_mode_event"
PRESET_EVENT: str = "versatile_thermostat_preset_event"
class UnknownEntity(HomeAssistantError): class UnknownEntity(HomeAssistantError):
"""Error to indicate there is an unknown entity_id given.""" """Error to indicate there is an unknown entity_id given."""
@@ -37,8 +37,11 @@ class PropAlgorithm:
self._cycle_min = cycle_min self._cycle_min = cycle_min
self._minimal_activation_delay = minimal_activation_delay self._minimal_activation_delay = minimal_activation_delay
self._on_percent = 0 self._on_percent = 0
self._calculated_on_percent = 0
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = self._cycle_min * 60 self._off_time_sec = self._cycle_min * 60
self._security = False
self._default_on_percent = 0
def calculate( def calculate(
self, target_temp: float, current_temp: float, ext_current_temp: float self, target_temp: float, current_temp: float, ext_current_temp: float
@@ -48,7 +51,7 @@ class PropAlgorithm:
_LOGGER.warning( _LOGGER.warning(
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long "Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long
) )
self._on_percent = 0 self._calculated_on_percent = 0
else: else:
delta_temp = target_temp - current_temp delta_temp = target_temp - current_temp
delta_ext_temp = ( delta_ext_temp = (
@@ -56,7 +59,7 @@ class PropAlgorithm:
) )
if self._function == PROPORTIONAL_FUNCTION_TPI: if self._function == PROPORTIONAL_FUNCTION_TPI:
self._on_percent = ( self._calculated_on_percent = (
self._tpi_coef_int * delta_temp self._tpi_coef_int * delta_temp
+ self._tpi_coef_ext * delta_ext_temp + self._tpi_coef_ext * delta_ext_temp
) )
@@ -65,7 +68,35 @@ class PropAlgorithm:
"Proportional algorithm: unknown %s function. Heating will be disabled", "Proportional algorithm: unknown %s function. Heating will be disabled",
self._function, self._function,
) )
self._on_percent = 0 self._calculated_on_percent = 0
self._calculate_internal()
_LOGGER.debug(
"heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
current_temp if current_temp else -9999.0,
ext_current_temp if ext_current_temp else -9999.0,
target_temp if target_temp else -9999.0,
self._calculated_on_percent,
self.on_time_sec,
self.off_time_sec,
)
def _calculate_internal(self):
"""Finish the calculation to get the on_percent in seconds"""
if self._security:
_LOGGER.debug(
"Security is On using the default_on_percent %f",
self._default_on_percent,
)
self._on_percent = self._default_on_percent
else:
_LOGGER.debug(
"Security is Off using the calculated_on_percent %f",
self._calculated_on_percent,
)
self._on_percent = self._calculated_on_percent
# calculated on_time duration in seconds # calculated on_time duration in seconds
if self._on_percent > 1: if self._on_percent > 1:
@@ -92,21 +123,31 @@ class PropAlgorithm:
self._off_time_sec = self._cycle_min * 60 - self._on_time_sec self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
_LOGGER.debug( def set_security(self, default_on_percent: float):
"heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long """Set a default value for on_percent (used for security mode)"""
current_temp if current_temp else -9999.0, self._security = True
ext_current_temp if ext_current_temp else -9999.0, self._default_on_percent = default_on_percent
target_temp if target_temp else -9999.0, self._calculate_internal()
self._on_percent,
self.on_time_sec, def unset_security(self):
self.off_time_sec, """Unset the security mode"""
) self._security = False
self._calculate_internal()
@property @property
def on_percent(self) -> float: def on_percent(self) -> float:
"""Returns the percentage the heater must be ON (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long """Returns the percentage the heater must be ON
In security mode this value is overriden with the _default_on_percent
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._on_percent, 2) return round(self._on_percent, 2)
@property
def calculated_on_percent(self) -> float:
"""Returns the calculated percentage the heater must be ON
Calculated means NOT overriden even in security mode
(1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._calculated_on_percent, 2)
@property @property
def on_time_sec(self) -> int: def on_time_sec(self) -> int:
"""Returns the calculated time in sec the heater must be ON""" """Returns the calculated time in sec the heater must be ON"""
@@ -89,7 +89,9 @@
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.", "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": { "data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated", "minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state" "security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
} }
} }
}, },
@@ -190,7 +192,9 @@
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.", "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": { "data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated", "minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state" "security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a security off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
} }
} }
}, },
@@ -209,5 +213,31 @@
"thermostat_over_climate": "Thermostat over another thermostat" "thermostat_over_climate": "Thermostat over another thermostat"
} }
} }
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"none": "Manual"
}
}
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"none": "Manual"
}
}
}
} }
} }
@@ -89,7 +89,9 @@
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.", "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": { "data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated", "minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state" "security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
} }
} }
}, },
@@ -190,7 +192,9 @@
"description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.", "description": "Configuration of advanced parameters. Leave the default values if you don't know what you are doing.\nThis parameters can lead to a very bad temperature or power regulation.",
"data": { "data": {
"minimal_activation_delay": "Delay in secondes under which the equipment will not be activated", "minimal_activation_delay": "Delay in secondes under which the equipment will not be activated",
"security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state" "security_delay_min": "Maximum allowed delay in minutes between two temperature mesures. Above this delay, the thermostat will turn to a sceurity off state",
"security_min_on_percent": "Minimal heating percent value for security preset activation. Below this amount of on_percent the thermostat won't go into security preset",
"security_default_on_percent": "The default heating percent value in security preset. Set to 0 to switch off heater in security present"
} }
} }
}, },
@@ -209,5 +213,31 @@
"thermostat_over_climate": "Thermostat over another thermostat" "thermostat_over_climate": "Thermostat over another thermostat"
} }
} }
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"none": "Manual"
}
}
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"none": "Manual"
}
}
}
} }
} }
@@ -87,8 +87,10 @@
"title": "Parameters avancés", "title": "Parameters avancés",
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.", "description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": { "data": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé", "minimal_activation_delay": "Délai en secondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position éteinte de sécurité" "security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position éteinte de sécurité",
"security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité"
} }
} }
}, },
@@ -190,7 +192,9 @@
"description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.", "description": "Configuration des paramètres avancés. Laissez les valeurs par défaut si vous ne savez pas ce que vous faites.\nCes paramètres peuvent induire des mauvais comportements du thermostat.",
"data": { "data": {
"minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé", "minimal_activation_delay": "Délai en seondes en-dessous duquel l'équipement ne sera pas activé",
"security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position éteinte de sécurité" "security_delay_min": "Délai maximal autorisé en minutes entre 2 mesures de températures. Au-dessus de ce délai, le thermostat se mettra en position éteinte de sécurité",
"security_min_on_percent": "Seuil minimal de pourcentage de chauffage en-dessous duquel le préréglage sécurité ne sera jamais activé",
"security_default_on_percent": "Valeur par défaut pour le pourcentage de chauffage en mode sécurité. Mettre 0 pour éteindre le radiateur en mode sécurité"
} }
} }
}, },
@@ -209,5 +213,31 @@
"thermostat_over_climate": "Thermostat sur un autre thermostat" "thermostat_over_climate": "Thermostat sur un autre thermostat"
} }
} }
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Délestage",
"security": "Sécurité",
"none": "Manuel"
}
}
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Délestage",
"security": "Sécurité",
"none": "Manuel"
}
}
}
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 14 KiB