Compare commits

..

6 Commits

Author SHA1 Message Date
Jean-Marc Collin
49377de248 Change regulation parameters 2023-10-30 16:43:21 +00:00
Jean-Marc Collin
6886dd6fb5 Implementation of regulation 2023-10-30 14:51:24 +00:00
Jean-Marc Collin
dde622e632 Implements regulation (tests ko) 2023-10-30 09:20:19 +00:00
Jean-Marc Collin
076d9eae24 Fix translations 2023-10-30 09:20:18 +00:00
Jean-Marc Collin
3356489f9d Add translations 2023-10-30 09:20:18 +00:00
Jean-Marc Collin
b323d676dc Algo implementation and tests 2023-10-30 09:20:18 +00:00
46 changed files with 663 additions and 2336 deletions

View File

@@ -13,15 +13,6 @@ debugpy:
wait: false wait: false
port: 5678 port: 5678
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
input_number: input_number:
fake_temperature_sensor1: fake_temperature_sensor1:
name: Temperature name: Temperature
@@ -221,8 +212,6 @@ switch:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
frontend: frontend:
extra_module_url:
- /config/www/community/versatile-thermostat-ui-card/versatile-thermostat-ui-card.js
themes: themes:
versatile_thermostat_theme: versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B" state-binary_sensor-safety-on-color: "#FF0B0B"

View File

@@ -10,9 +10,7 @@
"postCreateCommand": "./container dev-setup", "postCreateCommand": "./container dev-setup",
"mounts": [ "mounts": [
"source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached", "source=/Users/jmcollin/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
// uncomment this to get the versatile-thermostat-ui-card
"source=${localEnv:HOME}/SugarSync/Projets/home-assistant/versatile-thermostat-ui-card/dist,target=/workspaces/versatile_thermostat/config/www/community/versatile-thermostat-ui-card,type=bind,consistency=cached"
], ],
"customizations": { "customizations": {
@@ -21,14 +19,10 @@
"ms-python.python", "ms-python.python",
"github.vscode-pull-request-github", "github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters", "ryanluker.vscode-coverage-gutters",
"ms-python.black-formatter", "ms-python.vscode-pylance"
"ms-python.pylint",
"ferrierbenjamin.fold-unfold-all-icone",
"ms-python.isort",
"LittleFoxTeam.vscode-python-test-adapter"
], ],
// "mounts": [ // "mounts": [
// "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=${localWorkspaceFolder}/config/www/community/,type=bind,consistency=cached", // "source=${localWorkspaceFolder}/.devcontainer/configuration.yaml,target=/home/vscode/core/config/configuration.yaml,type=bind,consistency=cached",
// "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached" // "source=${localWorkspaceFolder}/custom_components,target=/home/vscode/core/config/custom_components,type=bind,consistency=cached"
// ], // ],
"settings": { "settings": {
@@ -44,7 +38,8 @@
// "terminal.integrated.shell.linux": "/bin/bash", // "terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/bin/python3", "python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": true, "python.analysis.autoSearchPaths": true,
"pylint.lintOnChange": false, "python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black", "python.formatting.provider": "black",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"editor.formatOnPaste": false, "editor.formatOnPaste": false,

View File

@@ -6,11 +6,10 @@ about: Create a report to help us improve
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem. <!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.
If you have a simple question or you are not sure this is an issue, don't open an issue but open a new discussion [here](https://github.com/jmcollin78/versatile_thermostat/discussions).
Issues not containing the minimum requirements will be closed: Issues not containing the minimum requirements will be closed:
- Issues without a description (using the header is not good enough) will be closed. - Issues without a description (using the header is not good enough) will be closed.
- Issues without debug logging will be closed.
- Issues without configuration will be closed - Issues without configuration will be closed
--> -->
@@ -22,116 +21,19 @@ If you are unsure about the version check the const.py file.
## Configuration ## Configuration
<!-- Copy / paste the attributes of the VTherm here. You can go to Development Tool / States, find and select your VTherm and the copy/paste the attributes.
Without these attribute support is impossible due to the number of configuration attributes the VTherm have (more than 60). -->
My VTherm attributes are the following:
```yaml ```yaml
hvac_modes:
- heat
- 'off'
min_temp: 7
max_temp: 35
preset_modes:
- none
- eco
- comfort
- boost
- activity
current_temperature: 18.9
temperature: 22
hvac_action: 'off'
preset_mode: security
hvac_mode: 'off'
type: null
eco_temp: 17
boost_temp: 20
comfort_temp: 19
eco_away_temp: 16.1
boost_away_temp: 16.3
comfort_away_temp: 16.2
power_temp: 13
ext_current_temperature: 11.6
ac_mode: false
current_power: 450
current_power_max: 910
saved_preset_mode: none
saved_target_temp: 22
saved_hvac_mode: heat
window_state: 'on'
motion_state: 'off'
overpowering_state: false
presence_state: 'on'
window_auto_state: false
window_bypass_state: false
security_delay_min: 2
security_min_on_percent: 0.5
security_default_on_percent: 0.1
last_temperature_datetime: '2023-11-05T00:48:54.873157+01:00'
last_ext_temperature_datetime: '2023-11-05T00:48:53.240122+01:00'
security_state: true
minimal_activation_delay_sec: 1
device_power: 300
mean_cycle_power: 30
total_energy: 137.5
last_update_datetime: '2023-11-05T00:51:54.901140+01:00'
timezone: Europe/Paris
window_sensor_entity_id: input_boolean.fake_window_sensor1
window_delay_sec: 20
window_auto_open_threshold: null
window_auto_close_threshold: null
window_auto_max_duration: null
motion_sensor_entity_id: input_boolean.fake_motion_sensor1
presence_sensor_entity_id: input_boolean.fake_presence_sensor1
power_sensor_entity_id: input_number.fake_current_power
max_power_sensor_entity_id: input_number.fake_current_power_max
is_over_switch: true
underlying_switch_0: input_boolean.fake_heater_switch1
underlying_switch_1: null
underlying_switch_2: null
underlying_switch_3: null
on_percent: 0.1
on_time_sec: 6
off_time_sec: 54
cycle_min: 1
function: tpi
tpi_coef_int: 0.6
tpi_coef_ext: 0.01
friendly_name: Thermostat switch 1
supported_features: 17
```
<!-- Please do not send an image but a copy / paste of the attributes in yaml format. --> Add your logs here.
```
## Describe the bug ## Describe the bug
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
I'm trying to:
<!-- compleete the description -->
And I expect:
<!-- complete the expectations -->
But I observe this ....
<!-- complete what you observe and why you think it is erroneous. -->
I read the documentation on the README.md file and I don't find any relevant information about this issue.
## Debug log ## Debug log
<!-- To enable debug logs check this https://www.home-assistant.io/components/logger/ <!-- To enable debug logs check this https://www.home-assistant.io/components/logger/ -->
Add the following configuration into your `configuration.yaml` (or `logger.yaml` if you have one) to enable logs: -->
```yaml
logger:
default: info
logs:
custom_components.versatile_thermostat: info
```
<!-- You can also switch to debug mode but be careful, in debug mode, the logs are verbose.
Please copy/paste the releveant logs (around the failure) below: -->
```text ```text

3
.gitignore vendored
View File

@@ -107,5 +107,4 @@ dist
custom_components/__init__.py custom_components/__init__.py
__pycache__ __pycache__
config/** config/**
custom_components/hacs

View File

@@ -1,9 +1,9 @@
{ {
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "ms-python.python"
"editor.formatOnSave": true
}, },
"pylint.lintOnChange": false, "python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"files.associations": { "files.associations": {
"*.yaml": "home-assistant" "*.yaml": "home-assistant"
}, },

View File

@@ -8,7 +8,6 @@
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-). > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-).
- [Changements majeurs dans la version 4.0.0](#changements-majeurs-dans-la-version-400)
- [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee) - [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser) - [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
- [Incompatibilités](#incompatibilités) - [Incompatibilités](#incompatibilités)
@@ -21,8 +20,6 @@
- [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées) - [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées)
- [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch) - [Pour un thermostat de type ```thermostat_over_switch```](#pour-un-thermostat-de-type-thermostat_over_switch)
- [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate) - [Pour un thermostat de type ```thermostat_over_climate```:](#pour-un-thermostat-de-type-thermostat_over_climate)
- [L'auto-régulation](#lauto-régulation)
- [L'auto-régulation en mode Expert](#lauto-régulation-en-mode-expert)
- [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve) - [Pour un thermostat de type ```thermostat_over_valve```:](#pour-un-thermostat-de-type-thermostat_over_valve)
- [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi) - [Configurez les coefficients de l'algorithme TPI](#configurez-les-coefficients-de-lalgorithme-tpi)
- [Configurer 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)
@@ -47,12 +44,10 @@
- [Forcer la présence/occupation](#forcer-la-présenceoccupation) - [Forcer la présence/occupation](#forcer-la-présenceoccupation)
- [Modifier la température des préréglages](#modifier-la-température-des-préréglages) - [Modifier la température des préréglages](#modifier-la-température-des-préréglages)
- [Modifier les paramètres de sécurité](#modifier-les-paramètres-de-sécurité) - [Modifier les paramètres de sécurité](#modifier-les-paramètres-de-sécurité)
- [ByPass Window Check](#bypass-window-check)
- [Notifications](#notifications) - [Notifications](#notifications)
- [Attributs personnalisés](#attributs-personnalisés) - [Attributs personnalisés](#attributs-personnalisés)
- [Quelques résultats](#quelques-résultats) - [Quelques résultats](#quelques-résultats)
- [Encore mieux](#encore-mieux) - [Encore mieux](#encore-mieux)
- [Bien mieux avec le Versatile Thermostat UI Card](#bien-mieux-avec-le-versatile-thermostat-ui-card)
- [Encore mieux avec le composant Scheduler !](#encore-mieux-avec-le-composant-scheduler-) - [Encore mieux avec le composant Scheduler !](#encore-mieux-avec-le-composant-scheduler-)
- [Encore bien mieux avec la custom:simple-thermostat front integration](#encore-bien-mieux-avec-la-customsimple-thermostat-front-integration) - [Encore bien mieux avec la custom:simple-thermostat front integration](#encore-bien-mieux-avec-la-customsimple-thermostat-front-integration)
- [Toujours mieux avec Apex-chart pour régler votre thermostat](#toujours-mieux-avec-apex-chart-pour-régler-votre-thermostat) - [Toujours mieux avec Apex-chart pour régler votre thermostat](#toujours-mieux-avec-apex-chart-pour-régler-votre-thermostat)
@@ -64,18 +59,15 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_ > ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_
> * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194). > * **Release 3.7**: Ajout du type de Versatile Thermostat `over valve` pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque
> * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
> * **Release 3.8**: Ajout d'une **fonction d'auto-régulation** pour les thermostats `over climate` dont la régulation est faite par le climate sous-jacent. Cf. [L'auto-régulation](#lauto-régulation) et [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Ajout de la **possibilité d'inverser la commande** pour un thermostat `over switch` pour adresser les installations avec fil pilote et diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Ajout du type de **Versatile Thermostat `over valve`** pour piloter une vanne TRV directement ou tout autre équipement type gradateur pour le chauffage. La régulation se fait alors directement en agissant sur le pourcentage d'ouverture de l'entité sous-jacente : 0 la vanne est coupée, 100 : la vanne est ouverte à fond. Cf. [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Ajout d'une fonction permettant le bypass de la détection d'ouverture [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Ajout de la langue Slovaque
<details>
<summary>Autres versions</summary>
> * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127) > * **Release 3.6**: Ajout du paramètre `motion_off_delay` pour améliorer la gestion de des mouvements [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Ajout du mode AC (air conditionné) pour un VTherm over switch. Préparation du projet Github pour faciliter les contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113) > * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103) > * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence > * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
> * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées) > * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées)
<details>
<summary>Autres versions</summary>
> * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto) > * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto)
> * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard. > * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard.
> * **release 2.3** : ajout de la mesure de puissance et d'énergie du radiateur piloté par le thermostat. > * **release 2.3** : ajout de la mesure de puissance et d'énergie du radiateur piloté par le thermostat.
@@ -83,15 +75,12 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier. > * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
</details> </details>
# Changements majeurs dans la version 4.0.0
La puissance de l'appareil doit maintenant être la puissance totale de tous les appareils controlée par le VTherm. Cela permet d'avoir des équipements hétérogènes de puissance différente. Dans le cas de plusieurs appareils contrôlés par un seul VTherm, vous devrez éditer et changer la valeur `device_power`. Vous devez configurer la puissance totale de tous les appareils.
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M pour les bières. Ca fait très plaisir et ça m'encourage à continuer ! Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome pour les bières. Ca fait très plaisir.
# Quand l'utiliser et ne pas l'utiliser # Quand l'utiliser et ne pas l'utiliser
Ce thermostat peut piloter 3 types d'équipements : Ce thermostat peut piloter 3 types d'équipement:
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 : 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 :
1. un équipement comme un radiateur (un ```switch``` ou équivalent), 1. un équipement comme un radiateur (un ```switch``` ou équivalent),
2. une sonde de température pour la pièce (ou un input_number), 2. une sonde de température pour la pièce (ou un input_number),
@@ -100,16 +89,12 @@ Ce thermostat peut piloter 3 types d'équipements :
1. un équipement - comme une climatisation, une valve thermostatique - qui est pilotée par sa propre entity de type ```climate```, 1. un équipement - comme une climatisation, une valve thermostatique - qui est pilotée par sa propre entity de type ```climate```,
3. un équipement qui peut prendre une valeur de 0 à 100% (nommée ```thermostat_over_valve```). A 0 le chauffage est coupé, 100% il est ouvert à fond. Ce type permet de piloter une valve thermostatique (cf. valve Shelly) qui expose une entité de type `number.` permetttant de piloter directement l'ouverture de la vanne. Versatile Thermostat régule la température de la pièce en jouant sur le pourcentage d'ouverture, à l'aide des capteurs de température intérieur et extérieur en utilisant l'algorithme TPI décrit ci-dessous. 3. un équipement qui peut prendre une valeur de 0 à 100% (nommée ```thermostat_over_valve```). A 0 le chauffage est coupé, 100% il est ouvert à fond. Ce type permet de piloter une valve thermostatique (cf. valve Shelly) qui expose une entité de type `number.` permetttant de piloter directement l'ouverture de la vanne. Versatile Thermostat régule la température de la pièce en jouant sur le pourcentage d'ouverture, à l'aide des capteurs de température intérieur et extérieur en utilisant l'algorithme TPI décrit ci-dessous.
Le type `over_climate` vous permet d'ajouter à votre équipement existant toutes les fonctionnalités apportées par VersatileThermostat. L'entité climate VersatileThermostat contrôlera votre entité climate sous-jacente, l'éteindra si les fenêtres sont ouvertes, la fera passer en mode Eco si personne n'est présent, etc. Voir [ici] (#pourquoi-un-nouveau-thermostat-implémentation). Pour ce type de thermostat, tous les cycles de chauffage sont contrôlés par l'entité climate sous-jacente et non par le thermostat polyvalent lui-même. Une fonction facultative d'auto-régulation permet au Versatile Thermostat d'ajuster la température donnée en consigne au sous-jacent afin d'atteindre la consigne. The ```thermostat_over_climate``` type allows you to add to your existing equipment all the functionalities provided by VersatileThermostat. The VersatileThermostat climate 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-thermostat-implementation). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself.
Les installations avec fil pilote et diode d'activation bénéficie d'une option qui permet d'inverser la commande on/off du radiateur sous-jacent. Pour cela, utilisez le type `over switch` et cochez l'option d'inversion de la commande.
## Incompatibilités ## Incompatibilités
Certains thermostat de type TRV sont réputés incompatibles avec le Versatile Thermostat. C'est le cas des vannes suivantes : Certains thermostat de type TRV sont réputés incompatibles avec le Versatile Thermostat. C'est le cas des vannes suivantes :
1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le VTherm, 1. les vannes POPP de Danfoss avec retour de température. Il est impossible d'éteindre cette vanne et elle s'auto-régule d'elle-même causant des conflits avec le VTherm,
2. les vannes thermstatiques "Homematic radio". Elles ont un cycle de service incompatible avec une commande par le Versatile Thermostat, 2. les vannes thermstatiques "Homematic radio". Elles ont un cycle de service incompatible avec une commande par le Versatile Thermostat
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
# Pourquoi une nouvelle implémentation du thermostat ? # Pourquoi une nouvelle implémentation du thermostat ?
@@ -196,119 +181,12 @@ Exemple de déclenchement synchronisé :
Il est possible de choisir un thermostat over switch qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible. Il est possible de choisir un thermostat over switch qui commande une climatisation en cochant la case "AC Mode". Dans ce cas, seul le mode refroidissement sera visible.
Si votre équipement est commandé par un fil pilote avec un diode, vous aurez certainement besoin de cocher la case "Inverser la case". Elle permet de mettre le switch à On lorsqu'on doit étiendre l'équipement et à Off lorsqu'on doit l'allumer.
### Pour un thermostat de type ```thermostat_over_climate```: ### Pour un thermostat de type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
Il est possible de choisir un thermostat over climate qui commande une climatisation réversible en cochant la case "AC Mode". Dans ce cas, selon l'équipement commandé vous aurez accès au chauffage et/ou au réfroidissement. Il est possible de choisir un thermostat over climate qui commande une climatisation réversible en cochant la case "AC Mode". Dans ce cas, selon l'équipement commandé vous aurez accès au chauffage et/ou au réfroidissement.
#### L'auto-régulation
Depuis la release 3.8, vous avez la possibilité d'activer la fonction d'auto-régulation. Cette fonction autorise VersatileThermostat à adapter la consigne de température donnée au climate sous-jacent afin que la température de la pièce atteigne réellement la consigne.
Pour faire ça, le VersatileThermostat calcule un décalage basé sur les informations suivantes :
1. la différence actuelle entre la température réelle et la température de consigne, appelé erreur brute,
2. l'accumulation des erreurs passées,
3. la différence entre la température extérieure et la consigne
Ces trois informations sont combinées pour calculer le décalage qui sera ajouté à la consigne courante et envoyé au climate sous-jacent.
La fonction d'auto-régulation se paramètre avec :
1. une dégré de régulation :
1. Légère - pour des faibles besoin en auto-régulation. Dans ce mode, le décalage maximal sera de 1,5°,
2. Medium - pour une auto-régulation moyenne. Un décalage maximal de 2° est possible dans ce mode,
3. Forte - pour un fort besoin d'auto-régulation. Le décalage maximal est de 3° dans ce mode et l'auto-régulation réagira fortement aux changements de température.
2. Un seuil d'auto-régulation : valeur en dessous de laquelle une nouvelle régulation ne sera pas appliquée. Imaginons qu'à un instant t, le décalage soit de 2°. Si au prochain calcul, le décalage est de 2.4°, il sera pas appliqué. Il ne sera appliqué que la différence entre 2 décalages sera au moins égal à ce seuil,
3. Période minimal entre 2 auto-régulation : ce nombre, exprimé en minute, indique la durée entre 2 changements de régulation.
Ces trois paramètres permettent de moduler la régulation et éviter de multiplier les envois de régulation. Certains équipements comme les TRV, les chaudières n'aiment pas qu'on change la consigne de température trop souvent.
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Conseil de mise en place*_
> 1. Ne démarrez pas tout de suite l'auto-régulation. Regardez comment se passe la régulation naturelle de votre équipement. Si vous constatez que la température de consigne n'est pas atteinte ou qu'elle met trop de temps à être atteinte, démarrez la régulation,
> 2. D'abord commencez par une légère auto-régulation et gardez les deux paramètres avec leur valeurs par défaut. Attendez quelques jours et vérifiez si la situation s'est améliorée,
> 3. Si ce n'est pas suffisant, passez en auto-régulation Medium, attendez une stabilisation,
> 4. Si ce n'est toujours pas suffisant, passez en auto-régulation Forte,
> 5. Si ce n'est toujours pas bon, il faudra passer en mode expert pour pouvoir régler les paramètres de régulation de façon fine. Voir en-dessous
L'auto-régulation consiste à forcer l'équipement a aller plus loin en lui forçant sa température de consigne régulièrement. Sa consommation peut donc être augmentée, ainsi que son usure.
#### L'auto-régulation en mode Expert
En mode **Expert** pouvez régler finement les paramètres de l'auto-régulation pour atteindre vos objeetifs et optimiser au mieux. L'algorithme calcule l'écart entre la consigne et la température réelle de la pièce. Cet écard est appelé erreur.
Les paramètres réglables sont les suivants :
1. `kp` : le facteur appliqué à l'erreur brute,
2. `ki` : le facteur appliqué à l'accumulation des erreurs,
3. `k_ext` : le facteur appliqué à la différence entre la température intérieure et la température externe,
4. `offset_max` : le maximum de correction (offset) que la régulation peut appliquer,
5. `stabilization_threshold` : un seuil de stabilisation qui lorsqu'il est atteint par l'erreur remet à 0, l'accumulation des erreurs,
6. `accumulated_error_threshold` : le maximum pour l'accumulation d'erreur.
Pour le tuning il faut tenir compte de ces observations :
1. `kp * erreur` va donner l'offset lié à l'erreur brute. Cet offset est directement proportionnel à l'erreur et sera à 0 lorsque la target sera atteinte,
2. l'accumulation de l'erreur permet de corriger le stabilisation de la courbe alors qu'il reste une erreur. L'erreur s'accumule et l'offset augmente donc progressivement ce qui devrait finir par stabiliser sur la température cible. Pour que ce paramètre fondamental est un effet il faut qu'il soit pas trop petit. Une valeur moyenne est 30
3. `ki * accumulated_error_threshold` va donner l'offset maximal lié à l'accumulation de l'erreur,
4. `k_ext` permet d'appliquer tout de suite (sans attendre une accumulation des erreurs) une correction lorsque la température extérieure est très différente de la température cible. Si la stabilisation se fait trop haut lorsqu'il les écarts de température sont importants, c'est que ce paramètre est trop fort. Il devrait pouvoir être annulé totalement pour laisser faire les 2 premiers offset
Les valeurs préprogrammées sont les suivantes :
Slow régulation :
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
Light régulation :
kp: 0.2
ki: 0.05
k_ext: 0.05
offset_max: 1.5
stabilization_threshold: 0.1
accumulated_error_threshold: 10
Medium régulation :
kp: 0.3
ki: 0.05
k_ext: 0.1
offset_max: 2
stabilization_threshold: 0.1
accumulated_error_threshold: 20
Strong régulation :
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
Pour utiliser le mode Expert il vous faut déclarer les valeurs que vous souhaitez utiliser pour chacun de ces paramètres dans votre `configuration.yaml` sous la forme suivante :
```
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
```
et bien sur, configurer le mode auto-régulation du VTherm en mode Expert. Tous les VTherm en mode **Expert** utiliseront ces mêmes paramètres.
Pour que les modifications soient prises en compte, il faut soit **relancer totalement Home Assistant** soit juste l'intégration Versatile Thermostat (Outils de dev / Yaml / rechargement de la configuration / Versatile Thermostat).
### Pour un thermostat de type ```thermostat_over_valve```: ### Pour un thermostat de type ```thermostat_over_valve```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true)
Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes. Vous pouvez choisir jusqu'à entité du domaine ```number``` ou ```ìnput_number``` qui vont commander les vannes.
@@ -433,7 +311,6 @@ Cela vous permet de modifier la puissance maximale au fil du temps à l'aide d'u
> 2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation. > 2. Je l'utilise pour éviter de dépasser la limite de mon contrat d'électricité lorsqu'un véhicule électrique est en charge. Cela crée une sorte d'autorégulation.
> 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés. > 3. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
> 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide > 4. Si vous ne souhaitez pas utiliser cette fonctionnalité, laissez simplement l'identifiant des entités vide
> 5. Si vous controlez plusieurs radiateurs, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
## Configurer la présence ou l'occupation ## Configurer la présence ou l'occupation
Si sélectionnée en première page, cette fonction vous permet de modifier dynamiquement la température de tous les préréglages du thermostat configurés lorsque personne n'est à la maison ou lorsque quelqu'un rentre à la maison. Pour cela, vous devez configurer la température qui sera utilisée pour chaque préréglage lorsque la présence est désactivée. Lorsque le capteur de présence s'éteint, ces températures seront utilisées. Lorsqu'il se rallume, la température "normale" configurée pour le préréglage est utilisée. Voir [gestion des préréglages](#configure-the-preset-temperature). Si sélectionnée en première page, cette fonction vous permet de modifier dynamiquement la température de tous les préréglages du thermostat configurés lorsque personne n'est à la maison ou lorsque quelqu'un rentre à la maison. Pour cela, vous devez configurer la température qui sera utilisée pour chaque préréglage lorsque la présence est désactivée. Lorsque le capteur de présence s'éteint, ces températures seront utilisées. Lorsqu'il se rallume, la température "normale" configurée pour le préréglage est utilisée. Voir [gestion des préréglages](#configure-the-preset-temperature).
@@ -475,7 +352,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
> 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security", > 2. Attention, deux températures sont nécessaires : la température interne et la température externe et chacune doit donner la température, sinon le thermostat sera en préréglage "security",
> 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage, > 3. Un service est disponible qui permet de régler les 3 paramètres de sécurité. Ca peut servir à adapter la fonction de sécurité à votre usage,
> 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``, > 4. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
> 5. Les thermostats de type ``thermostat_over_climate`` ne sont pas concernés par le mode security. > 5. Lorsqu'un thermostat de type ``thermostat_over_climate`` passe en mode ``security`` il est éteint. Les paramètres ``security_min_on_percent`` et ``security_default_on_percent`` ne sont alors pas utilisés.
## Synthèse des paramètres ## Synthèse des paramètres
@@ -538,10 +415,7 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - | | ``minimal_activation_delay`` | Délai minimal d'activation | X | - | - |
| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X | | ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - | X |
| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X | | ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - | | ``security_default_on_percent`` | Pourcentage de puissance a utiliser en mode securité | X | - | X |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - |
| ``inverse_switch_command`` | Inverse la commande du switch (pour switch avec fil pilote) | X | - | - |
# Exemples de réglage # Exemples de réglage
@@ -790,8 +664,6 @@ Les attributs personnalisés sont les suivants :
| ``friendly_name`` | Le nom du thermostat | | ``friendly_name`` | Le nom du thermostat |
| ``supported_features`` | Une combinaison de toutes les fonctionnalités prises en charge par ce thermostat. Voir la documentation officielle sur l'intégration climatique pour plus d'informations | | ``supported_features`` | Une combinaison de toutes les fonctionnalités prises en charge par ce thermostat. Voir la documentation officielle sur l'intégration climatique pour plus d'informations |
| ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne | | ``valve_open_percent`` | Le pourcentage d'ouverture de la vanne |
| ``regulated_target_temperature`` | La température de consigne calculée par l'auto-régulation |
| ``is_inversed`` | True si la commande est inversée (fil pilote avec diode) |
# Quelques résultats # Quelques résultats
@@ -818,11 +690,6 @@ Enjoy !
# Encore mieux # Encore mieux
## Bien mieux avec le Versatile Thermostat UI Card
Une carte spéciale pour le Versatile Thermostat a été développée (sur la base du Better Thermostat). Elle est dispo ici [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card) et propose une vision moderne de tous les status du VTherm :
![image](https://github.com/jmcollin78/versatile-thermostat-ui-card/blob/master/assets/1.png?raw=true)
## Encore mieux avec le composant Scheduler ! ## Encore mieux avec le composant Scheduler !
Afin de profiter de toute la puissance du Versatile Thermostat, je vous invite à l'utiliser avec https://github.com/nielsfaber/scheduler-component Afin de profiter de toute la puissance du Versatile Thermostat, je vous invite à l'utiliser avec https://github.com/nielsfaber/scheduler-component
@@ -928,21 +795,11 @@ series:
name: Current temp name: Current temp
curve: smooth curve: smooth
yaxis_id: left yaxis_id: left
- entity: climate.thermostat_mythermostat <--- for over_switch - entity: climate.thermostat_mythermostat
attribute: on_percent attribute: on_percent
name: Power percent name: Power percent
curve: stepline curve: stepline
yaxis_id: right yaxis_id: right
- entity: climate.thermostat_mythermostat <--- for over_thermostast
attribute: regulated_target_temperature
name: Regulated temperature
curve: stepline
yaxis_id: left
- entity: climate.thermostat_mythermostat <--- for over_valve
attribute: valve_open_percent
name: Valve open percent
curve: stepline
yaxis_id: right
``` ```
## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements ## Et toujours de mieux en mieux avec l'AappDaemon NOTIFIER pour notifier les évènements

171
README.md
View File

@@ -8,7 +8,6 @@
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-). > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) This thermostat integration aims to drastically simplify your automations around climate management. Because all classical events in climate are natively handled by the thermostat (nobody at home ?, activity detected in a room ?, window open ?, power shedding ?), you don't have to build over complicated scripts and automations to manage your climates ;-).
- [Breaking changes in 4.0.0](#breaking-changes-in-400)
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee) - [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
- [When to use / not use](#when-to-use--not-use) - [When to use / not use](#when-to-use--not-use)
- [Incompatibilities](#incompatibilities) - [Incompatibilities](#incompatibilities)
@@ -21,8 +20,6 @@
- [Select the driven entity](#select-the-driven-entity) - [Select the driven entity](#select-the-driven-entity)
- [For a ```thermostat_over_switch``` type thermostat](#for-a-thermostat_over_switch-type-thermostat) - [For a ```thermostat_over_switch``` type thermostat](#for-a-thermostat_over_switch-type-thermostat)
- [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate) - [For a thermostat of type ```thermostat_over_climate```:](#for-a-thermostat-of-type-thermostat_over_climate)
- [Self-regulation](#self-regulation)
- [Self-regulation in Expert mode](#self-regulation-in-expert-mode)
- [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve) - [For a thermostat of type ```thermostat_over_valve```:](#for-a-thermostat-of-type-thermostat_over_valve)
- [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients) - [Configure the TPI algorithm coefficients](#configure-the-tpi-algorithm-coefficients)
- [Configure the preset temperature](#configure-the-preset-temperature) - [Configure the preset temperature](#configure-the-preset-temperature)
@@ -31,6 +28,7 @@
- [Auto mode](#auto-mode) - [Auto mode](#auto-mode)
- [Configure the activity mode or motion detection](#configure-the-activity-mode-or-motion-detection) - [Configure the activity mode or motion detection](#configure-the-activity-mode-or-motion-detection)
- [Configure the power management](#configure-the-power-management) - [Configure the power management](#configure-the-power-management)
- [Configure the presence or occupancy](#configure-the-presence-or-occupancy)
- [Advanced configuration](#advanced-configuration) - [Advanced configuration](#advanced-configuration)
- [Parameters synthesis](#parameters-synthesis) - [Parameters synthesis](#parameters-synthesis)
- [Examples tuning](#examples-tuning) - [Examples tuning](#examples-tuning)
@@ -46,12 +44,10 @@
- [Force the presence / occupancy](#force-the-presence--occupancy) - [Force the presence / occupancy](#force-the-presence--occupancy)
- [Change the temperature of presets](#change-the-temperature-of-presets) - [Change the temperature of presets](#change-the-temperature-of-presets)
- [Change security settings](#change-security-settings) - [Change security settings](#change-security-settings)
- [ByPass Window Check](#bypass-window-check)
- [Notifications](#notifications) - [Notifications](#notifications)
- [Custom attributes](#custom-attributes) - [Custom attributes](#custom-attributes)
- [Some results](#some-results) - [Some results](#some-results)
- [Even better](#even-better) - [Even better](#even-better)
- [Much better with the Veersatile Thermostat UI Card](#much-better-with-the-veersatile-thermostat-ui-card)
- [Even Better with Scheduler Component !](#even-better-with-scheduler-component-) - [Even Better with Scheduler Component !](#even-better-with-scheduler-component-)
- [Even-even better with custom:simple-thermostat front integration](#even-even-better-with-customsimple-thermostat-front-integration) - [Even-even better with custom:simple-thermostat front integration](#even-even-better-with-customsimple-thermostat-front-integration)
- [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)
@@ -62,18 +58,15 @@
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features. This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
>![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_ >![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_
> * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194). > * **Release 3.7**: Addition of the Versatile Thermostat type `over valve` to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language
> * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144) > * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https ://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.8**: Added a **self-regulation function** for `over climate` thermostats whose regulation is done by the underlying climate. See [Self-regulation](#self-regulation) and [#129](https://github.com/jmcollin78/versatile_thermostat/issues/129). Added the possibility of **inverting the command** for an `over switch` thermostat to address installations with pilot wire and diode [#124](https://github.com/jmcollin78/versatile_thermostat/issues/124).
> * **Release 3.7**: Addition of the **Versatile Thermostat type `over valve`** to control a TRV valve directly or any other dimmer type equipment for heating. Regulation is then done directly by acting on the opening percentage of the underlying entity: 0 the valve is cut off, 100: the valve is fully opened. See [#131](https://github.com/jmcollin78/versatile_thermostat/issues/131). Added a function allowing the bypass of opening detection [#138](https://github.com/jmcollin78/versatile_thermostat/issues/138). Added Slovak language
<details>
<summary>Others releases</summary>
> * **Release 3.6**: Added the `motion_off_delay` parameter to improve motion management [#116](https://github.com/jmcollin78/versatile_thermostat/issues/116), [#128](https://github.com/jmcollin78/versatile_thermostat/issues/128). Added AC (air conditioning) mode for a VTherm over switch. Preparing the Github project to facilitate contributions [#127](https://github.com/jmcollin78/versatile_thermostat/issues/127)
> * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113) > * **Release 3.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103) > * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured > * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
> * **Release 3.2**: add the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity) > * **Release 3.2**: add the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity)
<details>
<summary>Others releases</summary>
> * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode) > * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode)
> * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard. > * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard.
> * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat. > * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat.
@@ -81,11 +74,8 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter. > * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
</details> </details>
# Breaking changes in 4.0.0
The power of the device should now be the total power of all controler devices by the VTherm. This allow to have eterogeneous equipment with different power. In case of multi-devices controlled by a single VTherm you will have to edit and change the `device_power` value. Set the total power of all devices.
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M for the beers. It's very nice and encourages me to continue! Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome for the beers. It's very pleasing.
# When to use / not use # When to use / not use
This thermostat can control 3 types of equipment: This thermostat can control 3 types of equipment:
@@ -96,18 +86,13 @@ This thermostat can control 3 types of equipment:
2. another thermostat which has its own operating modes (named ``thermostat_over_climate```). For this type of thermostat the minimum configuration requires: 2. another thermostat which has its own operating modes (named ``thermostat_over_climate```). For this type of thermostat the minimum configuration requires:
1. equipment - such as air conditioning, a thermostatic valve - which is controlled by its own ``climate'' type entity, 1. equipment - such as air conditioning, a thermostatic valve - which is controlled by its own ``climate'' type entity,
3. equipment which can take a value from 0 to 100% (called `thermostat_over_valve`). At 0 the heating is cut off, 100% it is fully opened. This type allows you to control a thermostatic valve (see Shelly valve) which exposes an entity of type `number.` allowing you to directly control the opening of the valve. Versatile Thermostat regulates the room temperature by adjusting the opening percentage, using the interior and exterior temperature sensors using the TPI algorithm described below. 3. equipment which can take a value from 0 to 100% (called `thermostat_over_valve`). At 0 the heating is cut off, 100% it is fully opened. This type allows you to control a thermostatic valve (see Shelly valve) which exposes an entity of type `number.` allowing you to directly control the opening of the valve. Versatile Thermostat regulates the room temperature by adjusting the opening percentage, using the interior and exterior temperature sensors using the TPI algorithm described below.
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 existing 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.
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 existing 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. An optional self-regulation function allows the Versatile Thermostat to adjust the temperature given as a setpoint to the underlying in order to reach the setpoint.
Installations with pilot wire and activation diode benefit from an option which allows the on/off control of the underlying radiator to be reversed. To do this, use the `over switch` type and check the command inversion option.
## Incompatibilities ## Incompatibilities
Some TRV type thermostats are known to be incompatible with the Versatile Thermostat. This is the case for the following valves: Some TRV type thermostats are known to be incompatible with the Versatile Thermostat. This is the case for the following valves:
1. Danfoss POPP valves with temperature feedback. It is impossible to turn off this valve and it self-regulates, causing conflicts with the VTherm, 1. Danfoss POPP valves with temperature feedback. It is impossible to turn off this valve and it self-regulates, causing conflicts with the VTherm,
2. “Homematic radio” thermostatic valves. They have a duty cycle incompatible with control by the Versatile Thermostat, 2. “Homematic radio” thermostatic valves. They have a duty cycle incompatible with control by the Versatile Thermostat
3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
# Why another thermostat implementation ? # Why another thermostat implementation ?
@@ -193,117 +178,12 @@ Example of synchronized triggering:
It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible. It is possible to choose an over switch thermostat which controls air conditioning by checking the "AC Mode" box. In this case, only the cooling mode will be visible.
If your equipment is controlled by a pilot wire with a diode, you will certainly need to check the "Invert Check" box. It allows you to set the switch to On when you need to turn the equipment off and to Off when you need to turn it on.
### For a thermostat of type ```thermostat_over_climate```: ### For a thermostat of type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)
It is possible to choose an over climate thermostat which controls reversible air conditioning by checking the “AC Mode” box. In this case, depending on the equipment ordered, you will have access to heating and/or cooling. It is possible to choose an over climate thermostat which controls reversible air conditioning by checking the “AC Mode” box. In this case, depending on the equipment ordered, you will have access to heating and/or cooling.
#### Self-regulation
Since release 3.8, you have the possibility to activate the self-regulation function. This function allows VersatileThermostat to adapt the temperature setpoint given to the underlying climate so that the room temperature actually reaches the setpoint.
To do this, the VersatileThermostat calculates an offset based on the following information:
1. the current difference between the actual temperature and the set temperature,
2. the accumulation of past differences,
3. the difference between the outside temperature and the setpoint
These three pieces of information are combined to calculate the offset which will be added to the current setpoint and sent to the underlying climate.
The self-regulation function is configured with:
1. a degree of regulation:
1. Light - for low self-regulation needs. In this mode, the maximum offset will be 1.5°,
2. Medium - for average self-regulation. A maximum offset of 2° is possible in this mode,
3. Strong - for a strong need for self-regulation. The maximum offset is 3° in this mode and the auto-regulation will react strongly to temperature changes.
2. A self-regulation threshold: value below which new regulation will not be applied. Let us imagine that at a time t, the offset is 2°. If in the next calculation, the offset is 2.4°, it will not be applied. It will only be applied that the difference between 2 offsets will be at least equal to this threshold,
3. Minimum period between 2 self-regulation changes: this number, expressed in minutes, indicates the duration between 2 regulation changes.
These three parameters make it possible to modulate the regulation and avoid multiplying the regulation sendings. Some equipment such as TRVs and boilers do not like the temperature setpoint to be changed too often.
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Implementation tip*_
> 1. Do not start self-regulation straight away. Watch how the natural regulation of your equipment works. If you notice that the set temperature is not reached or that it is taking too long to be reached, start the regulation,
> 2. First start with a slight self-regulation and keep both parameters at their default values. Wait a few days and check if the situation has improved,
> 3. If this is not sufficient, switch to Medium self-regulation, wait for stabilization,
> 4. If this is still not sufficient, switch to Strong self-regulation,
> 5. If it is still not good, you will have to switch to expert mode to be able to finely adjust the regulation parameters. See below.
Self-regulation consists of forcing the equipment to go further by forcing its set temperature regularly. Its consumption can therefore be increased, as well as its wear.
#### Self-regulation in Expert mode
In **Expert** mode you can finely adjust the auto-regulation parameters to achieve your objectives and optimize as best as possible. The algorithm calculates the difference between the setpoint and the actual temperature of the room. This discrepancy is called error.
The adjustable parameters are as follows:
1. `kp`: the factor applied to the raw error,
2. `ki`: the factor applied to the accumulation of errors,
3. `k_ext`: the factor applied to the difference between the interior temperature and the exterior temperature,
4. `offset_max`: the maximum correction (offset) that the regulation can apply,
5. `stabilization_threshold`: a stabilization threshold which, when reached by the error, resets the accumulation of errors to 0,
6. `accumulated_error_threshold`: the maximum for error accumulation.
For tuning, these observations must be taken into account:
1. `kp * error` will give the offset linked to the raw error. This offset is directly proportional to the error and will be 0 when the target is reached,
2. the accumulation of the error makes it possible to correct the stabilization of the curve while there remains an error. The error accumulates and the offset therefore gradually increases which should eventually stabilize at the target temperature. For this fundamental parameter to have an effect it must not be too small. An average value is 30
3. `ki * accumulated_error_threshold` will give the maximum offset linked to the accumulation of the error,
4. `k_ext` allows a correction to be applied immediately (without waiting for errors to accumulate) when the outside temperature is very different from the target temperature. If the stabilization is done too high when the temperature differences are significant, it is because this parameter is too high. It should be possible to cancel completely to let the first 2 offsets take place
The pre-programmed values are as follows:
Slow régulation :
kp: 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: 0.8 / 288.0 # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: 1.0 / 25.0 # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
accumulated_error_threshold: 2.0 * 288 # this allows up to 2°C long term offset in both directions
Light régulation :
kp: 0.2
ki: 0.05
k_ext: 0.05
offset_max: 1.5
stabilization_threshold: 0.1
accumulated_error_threshold: 10
Medium régulation :
kp: 0.3
ki: 0.05
k_ext: 0.1
offset_max: 2
stabilization_threshold: 0.1
accumulated_error_threshold: 20
Strong régulation :
"""Strong parameters for regulation
A set of parameters which doesn't take into account the external temp
and concentrate to internal temp error + accumulated error.
This should work for cold external conditions which else generates
high external_offset"""
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
To use Expert mode you must declare the values you want to use for each of these parameters in your `configuration.yaml` in the following form:
```
versatile_thermostat:
auto_regulation_expert:
kp: 0.4
ki: 0.08
k_ext: 0.0
offset_max: 5
stabilization_threshold: 0.1
accumulated_error_threshold: 50
```
and of course, configure the VTherm's self-regulation mode in **Expert** mode. All VTherms in Expert mode will use these same settings.
For the changes to be taken into account, you must either **completely restart Home Assistant** or just the **Versatile Thermostat integration** (Dev tools / Yaml / reloading the configuration / Versatile Thermostat).
### For a thermostat of type ```thermostat_over_valve```: ### For a thermostat of type ```thermostat_over_valve```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity3.png?raw=true)
You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves. You can choose up to domain entity ```number``` or ```ìnput_number``` which will control the valves.
@@ -417,8 +297,8 @@ This allows you to change the max power along time using a Scheduler or whatever
> 2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation. > 2. I use this to avoid exceeded the limit of my electrical power contract when an electrical vehicle is charging. This makes a kind of auto-regulation.
> 3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement. > 3. Always keep a margin, because max power can be briefly exceeded while waiting for the next cycle calculation typically or by not regulated equipement.
> 4. If you don't want to use this feature, just leave the entities id empty > 4. If you don't want to use this feature, just leave the entities id empty
> 5. If you control several heaters, the **power consumption of your heater** setup should be the sum of the power.
## Configure the presence or occupancy
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). 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:
@@ -458,7 +338,7 @@ See [example tuning](#examples-tuning) for common tuning examples
> 2. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "security" preset, > 2. Attention, two temperatures are needed: internal temperature and external temperature and each must give the temperature, otherwise the thermostat will be in "security" preset,
> 3. A service is available that allows you to set the 3 security parameters. This can be used to adapt the security function to your use. > 3. A service is available that allows you to set the 3 security parameters. This can be used to adapt the security function to your use.
> 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``, > 4. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
> 5. Thermostat of type ``thermostat_over_climate`` are not concerned by the security feature. > 5. When a ``thermostat_over_climate`` type thermostat goes into ``security`` mode it is turned off. The ``security_min_on_percent`` and ``security_default_on_percent`` parameters are then not used.
## Parameters synthesis ## Parameters synthesis
@@ -519,13 +399,9 @@ See [example tuning](#examples-tuning) for common tuning examples
| ``comfort_ac_away_temp`` | Temperature in Comfort preset when no presence in AC mode | X | X | X | | ``comfort_ac_away_temp`` | Temperature in Comfort preset when no presence in AC mode | X | X | X |
| ``boost_ac_away_temp`` | Temperature in Boost preset when no presence in AC mode | X | X | X | | ``boost_ac_away_temp`` | Temperature in Boost preset when no presence in AC mode | X | X | X |
| ``minimal_activation_delay`` | Minimal activation delay | X | - | X | | ``minimal_activation_delay`` | Minimal activation delay | X | - | X |
| ``security_delay_min`` | Security delay (in minutes) | X | - | X | | ``security_delay_min`` | Security delay (in minutes) | X | X | X |
| ``security_min_on_percent`` | Minimal power percent to enable security mode | X | - | X | | ``security_min_on_percent`` | Minimal power percent to enable security mode | X | X | X |
| ``security_default_on_percent`` | Power percent to use in security mode | X | - | X | | ``security_default_on_percent`` | Power percent to use in security mode | X | X | X |
| ``auto_regulation_mode`` | Le mode d'auto-régulation | - | X | - |
| ``auto_regulation_dtemp`` | La seuil d'auto-régulation | - | X | - |
| ``auto_regulation_period_min`` | La période minimale d'auto-régulation | - | X | - |
| ``inverse_switch_command`` | Inverse the switch command (for pilot wire switch) | X | - | - |
# Examples tuning # Examples tuning
@@ -772,8 +648,6 @@ Custom attributes are the following:
| ``friendly_name`` | The name of the thermostat | | ``friendly_name`` | The name of the thermostat |
| ``supported_features`` | A combination of all features supported by this thermostat. See official climate integration documentation for more informations | | ``supported_features`` | A combination of all features supported by this thermostat. See official climate integration documentation for more informations |
| ``valve_open_percent`` | The opening percentage of the valve | | ``valve_open_percent`` | The opening percentage of the valve |
| ``regulated_target_temperature`` | The self-regulated target temperature calculated |
| ``is_inversed`` | True if the command is inversed (pilot wire with diode) |
# Some results # Some results
@@ -800,11 +674,6 @@ Enjoy !
# Even better # Even better
## Much better with the Veersatile Thermostat UI Card
A special card for the Versatile Thermostat has been developed (based on the Better Thermostat). It is available here [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card) and offers a modern vision of all the VTherm statuses:
![image](https://github.com/jmcollin78/versatile-thermostat-ui-card/blob/master/assets/1.png?raw=true)
## Even Better with Scheduler Component ! ## Even Better with Scheduler Component !
In order to enjoy the full power of Versatile Thermostat, I invite you to use it with https://github.com/nielsfaber/scheduler-component In order to enjoy the full power of Versatile Thermostat, I invite you to use it with https://github.com/nielsfaber/scheduler-component
@@ -909,21 +778,11 @@ series:
name: Current temp name: Current temp
curve: smooth curve: smooth
yaxis_id: left yaxis_id: left
- entity: climate.thermostat_mythermostat <--- for over_switch - entity: climate.thermostat_mythermostat
attribute: on_percent attribute: on_percent
name: Power percent name: Power percent
curve: stepline curve: stepline
yaxis_id: right yaxis_id: right
- entity: climate.thermostat_mythermostat <--- for over_thermostast
attribute: regulated_target_temperature
name: Regulated temperature
curve: stepline
yaxis_id: left
- entity: climate.thermostat_mythermostat <--- for over_valve
attribute: valve_open_percent
name: Valve open percent
curve: stepline
yaxis_id: right
``` ```
## And always better and better with the NOTIFIER daemon app to notify events ## And always better and better with the NOTIFIER daemon app to notify events

View File

@@ -4,84 +4,16 @@ from __future__ import annotations
from typing import Dict from typing import Dict
import logging import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigType from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat
from .const import ( from .const import DOMAIN, PLATFORMS
DOMAIN,
PLATFORMS,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_EXPERT,
)
from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SELF_REGULATION_PARAM_SCHEMA = (
vol.Schema(
{
vol.Required("kp"): vol.Coerce(float),
vol.Required("ki"): vol.Coerce(float),
vol.Required("k_ext"): vol.Coerce(float),
vol.Required("offset_max"): vol.Coerce(float),
vol.Required("stabilization_threshold"): vol.Coerce(float),
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
}
),
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
CONF_AUTO_REGULATION_EXPERT: vol.Schema(
{
vol.Required("kp"): vol.Coerce(float),
vol.Required("ki"): vol.Coerce(float),
vol.Required("k_ext"): vol.Coerce(float),
vol.Required("offset_max"): vol.Coerce(float),
vol.Required("stabilization_threshold"): vol.Coerce(float),
vol.Required("accumulated_error_threshold"): vol.Coerce(float),
}
),
}
),
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(
hass: HomeAssistant, config: ConfigType
): # pylint: disable=unused-argument
"""Initialisation de l'intégration"""
_LOGGER.info(
"Initializing %s integration with config: %s",
DOMAIN,
config.get(DOMAIN),
)
hass.data.setdefault(DOMAIN, {})
# L'argument config contient votre fichier configuration.yaml
vtherm_config = config.get(DOMAIN)
if vtherm_config is not None:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
api.set_global_config(vtherm_config)
else:
_LOGGER.info("No global config from configuration.yaml available")
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Versatile Thermostat from a config entry.""" """Set up Versatile Thermostat from a config entry."""
@@ -92,7 +24,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data, entry.data,
) )
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) # hass.data.setdefault(DOMAIN, {})
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
if api is None:
api = VersatileThermostatAPI(hass)
api.add_entry(entry) api.add_entry(entry)
@@ -110,7 +46,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) api: VersatileThermostatAPI = hass.data.get(DOMAIN)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
if api: if api:
@@ -119,6 +55,43 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok return unload_ok
class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI"""
_hass: HomeAssistant
# _entries: Dict(str, ConfigEntry)
def __init__(self, hass: HomeAssistant) -> None:
_LOGGER.debug("building a VersatileThermostatAPI")
super().__init__()
self._hass = hass
# self._entries = dict()
# Add the API in hass.data
self._hass.data[DOMAIN] = self
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
_LOGGER.debug("Add the entry %s", entry.entry_id)
# self._entries[entry.entry_id] = entry
# Add the entry in hass.data
self._hass.data[DOMAIN][entry.entry_id] = entry
def remove_entry(self, entry: ConfigEntry):
"""Remove an entry"""
_LOGGER.debug("Remove the entry %s", entry.entry_id)
# self._entries.pop(entry.entry_id)
self._hass.data[DOMAIN].pop(entry.entry_id)
# If not more entries are preset, remove the API
if len(self) == 0:
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
self._hass.data.pop(DOMAIN)
@property
def hass(self):
"""Get the HomeAssistant object"""
return self._hass
# Example migration function # Example migration function
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry.""" """Migrate old entry."""

View File

@@ -113,17 +113,10 @@ from .underlyings import UnderlyingEntity
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
from .open_window_algorithm import WindowOpenDetectionAlgorithm from .open_window_algorithm import WindowOpenDetectionAlgorithm
from .ema import ExponentialMovingAverage
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
class BaseThermostat(ClimateEntity, RestoreEntity): class BaseThermostat(ClimateEntity, RestoreEntity):
"""Representation of a base class for all Versatile Thermostat device.""" """Representation of a base class for all Versatile Thermostat device."""
@@ -137,54 +130,47 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_motion_state: bool _motion_state: bool
_presence_state: bool _presence_state: bool
_window_auto_state: bool _window_auto_state: bool
#PR - Adding Window ByPass
_window_bypass_state: bool _window_bypass_state: bool
_underlyings: list[UnderlyingEntity] _underlyings: list[UnderlyingEntity]
_last_change_time: datetime _last_change_time: datetime
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = ClimateEntity._entity_component_unrecorded_attributes.union(frozenset(
ClimateEntity._entity_component_unrecorded_attributes.union( {
frozenset( "type",
{ "eco_temp",
"is_on", "boost_temp",
"type", "comfort_temp",
"eco_temp", "eco_away_temp",
"boost_temp", "boost_away_temp",
"comfort_temp", "comfort_away_temp",
"eco_away_temp", "power_temp",
"boost_away_temp", "ac_mode",
"comfort_away_temp", "current_power_max",
"power_temp", "saved_preset_mode",
"ac_mode", "saved_target_temp",
"current_power_max", "saved_hvac_mode",
"saved_preset_mode", "security_delay_min",
"saved_target_temp", "security_min_on_percent",
"saved_hvac_mode", "security_default_on_percent",
"security_delay_min", "last_temperature_datetime",
"security_min_on_percent", "last_ext_temperature_datetime",
"security_default_on_percent", "minimal_activation_delay_sec",
"last_temperature_datetime", "device_power",
"last_ext_temperature_datetime", "mean_cycle_power",
"minimal_activation_delay_sec", "last_update_datetime",
"device_power", "timezone",
"mean_cycle_power", "window_sensor_entity_id",
"last_update_datetime", "window_delay_sec",
"timezone", "window_auto_open_threshold",
"window_sensor_entity_id", "window_auto_close_threshold",
"window_delay_sec", "window_auto_max_duration",
"window_auto_open_threshold", "motion_sensor_entity_id",
"window_auto_close_threshold", "presence_sensor_entity_id",
"window_auto_max_duration", "power_sensor_entity_id",
"motion_sensor_entity_id", "max_power_sensor_entity_id",
"presence_sensor_entity_id", }
"power_sensor_entity_id", ))
"max_power_sensor_entity_id",
"temperature_unit",
"is_device_active",
"target_temperature_step",
}
)
)
)
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat.""" """Initialize the thermostat."""
@@ -253,8 +239,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._underlyings = [] self._underlyings = []
self._ema_temp = None
self._ema_algo = None
self.post_init(entry_infos) self.post_init(entry_infos)
def post_init(self, entry_infos): def post_init(self, entry_infos):
@@ -307,8 +291,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR) self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX) self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX)
self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN) self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN)
# Default value not configurable
self._attr_target_temperature_step = 0.1
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR) self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR) self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR) self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
@@ -356,7 +338,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._presence_on = self._presence_sensor_entity_id is not None self._presence_on = self._presence_sensor_entity_id is not None
if self._ac_mode: if self._ac_mode:
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF] self._hvac_list = [HVACMode.COOL, HVACMode.OFF]
else: else:
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF] self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
@@ -459,15 +441,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._total_energy = 0 self._total_energy = 0
self._ema_algo = ExponentialMovingAverage(
self.name,
self._cycle_min * 60,
# Needed for time calculation
get_tz(self._hass),
# two digits after the coma for temperature slope calculation
2,
)
_LOGGER.debug( _LOGGER.debug(
"%s - Creation of a new VersatileThermostat entity: unique_id=%s", "%s - Creation of a new VersatileThermostat entity: unique_id=%s",
self, self,
@@ -648,7 +621,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
): ):
self._window_state = window_state.state == STATE_ON self._window_state = window_state.state
_LOGGER.debug( _LOGGER.debug(
"%s - Window state have been retrieved: %s", "%s - Window state have been retrieved: %s",
self, self,
@@ -711,12 +684,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
EVENT_HOMEASSISTANT_START, _async_startup_internal EVENT_HOMEASSISTANT_START, _async_startup_internal
) )
def restore_specific_previous_state(self, old_state):
"""Should be overriden in each specific thermostat
if a specific previous state or attribute should be
restored
"""
async def get_my_previous_state(self): async def get_my_previous_state(self):
"""Try to get my previou state""" """Try to get my previou state"""
# Check If we have an old state # Check If we have an old state
@@ -762,8 +729,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY) old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY)
if old_total_energy: if old_total_energy:
self._total_energy = old_total_energy self._total_energy = old_total_energy
self.restore_specific_previous_state(old_state)
else: else:
# No previous state, try and restore defaults # No previous state, try and restore defaults
if self._target_temp is None: if self._target_temp is None:
@@ -797,17 +762,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
@property @property
def is_over_climate(self) -> bool: def is_over_climate(self) -> bool:
"""True if the Thermostat is over_climate""" """ True if the Thermostat is over_climate"""
return False return False
@property @property
def is_over_switch(self) -> bool: def is_over_switch(self) -> bool:
"""True if the Thermostat is over_switch""" """ True if the Thermostat is over_switch"""
return False return False
@property @property
def is_over_valve(self) -> bool: def is_over_valve(self) -> bool:
"""True if the Thermostat is over_valve""" """ True if the Thermostat is over_valve"""
return False return False
@property @property
@@ -933,6 +898,27 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""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."""
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach.
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
"""
return None
@property
def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach.
Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
"""
return None
@property @property
def is_aux_heat(self) -> bool | None: def is_aux_heat(self) -> bool | None:
"""Return true if aux heater. """Return true if aux heater.
@@ -947,7 +933,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if not self._device_power: if not self._device_power:
return None return None
return float(self._device_power * self._prop_algorithm.on_percent) return float(
self.nb_underlying_entities
* self._device_power
* self._prop_algorithm.on_percent
)
@property @property
def total_energy(self) -> float | None: def total_energy(self) -> float | None:
@@ -965,15 +955,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return self._overpowering_state return self._overpowering_state
@property @property
def window_state(self) -> str | None: def window_state(self) -> bool | None:
"""Get the window_state""" """Get the window_state"""
return STATE_ON if self._window_state else STATE_OFF return self._window_state
@property @property
def window_auto_state(self) -> str | None: def window_auto_state(self) -> bool | None:
"""Get the window_auto_state""" """Get the window_auto_state"""
return STATE_ON if self._window_auto_state else STATE_OFF return STATE_ON if self._window_auto_state else STATE_OFF
#PR - Adding Window ByPass
@property @property
def window_bypass_state(self) -> bool | None: def window_bypass_state(self) -> bool | None:
"""Get the Window Bypass""" """Get the Window Bypass"""
@@ -1043,11 +1034,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""Returns the number of underlying entities""" """Returns the number of underlying entities"""
return len(self._underlyings) return len(self._underlyings)
@property
def is_on(self) -> bool:
"""True if the VTherm is on (! HVAC_OFF)"""
return self.hvac_mode and self.hvac_mode != HVACMode.OFF
def underlying_entity_id(self, index=0) -> str | None: def underlying_entity_id(self, index=0) -> str | None:
"""The climate_entity_id. Added for retrocompatibility reason""" """The climate_entity_id. Added for retrocompatibility reason"""
if index < self.nb_underlying_entities: if index < self.nb_underlying_entities:
@@ -1233,9 +1219,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def _async_internal_set_temperature(self, temperature): async def _async_internal_set_temperature(self, temperature):
"""Set the target temperature and the target temperature of underlying climate if any """Set the target temperature and the target temperature of underlying climate if any"""
For testing purpose you can pass an event_timestamp.
"""
self._target_temp = temperature self._target_temp = temperature
return return
@@ -1322,34 +1306,33 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug( _LOGGER.debug(
"Window delay condition is not satisfied. Ignore window event" "Window delay condition is not satisfied. Ignore window event"
) )
self._window_state = old_state.state == STATE_ON self._window_state = old_state.state
return return
_LOGGER.debug("%s - Window delay condition is satisfied", self) _LOGGER.debug("%s - Window delay condition is satisfied", self)
# if not self._saved_hvac_mode: # if not self._saved_hvac_mode:
# self._saved_hvac_mode = self._hvac_mode # self._saved_hvac_mode = self._hvac_mode
if self._window_state == (new_state.state == STATE_ON): if self._window_state == new_state.state:
_LOGGER.debug("%s - no change in window state. Forget the event") _LOGGER.debug("%s - no change in window state. Forget the event")
return return
self._window_state = new_state.state == STATE_ON
# PR - Adding Window ByPass self._window_state = new_state.state
#PR - Adding Window ByPass
_LOGGER.debug("%s - Window ByPass is : %s", self, self._window_bypass_state) _LOGGER.debug("%s - Window ByPass is : %s", self, self._window_bypass_state)
if self._window_bypass_state: if self._window_bypass_state:
_LOGGER.info( _LOGGER.info("%s - Window ByPass is activated. Ignore window event", self)
"%s - Window ByPass is activated. Ignore window event", self
)
else: else:
if not self._window_state: if self._window_state == STATE_OFF:
_LOGGER.info( _LOGGER.info(
"%s - Window is closed. Restoring hvac_mode '%s'", "%s - Window is closed. Restoring hvac_mode '%s'",
self, self,
self._saved_hvac_mode, self._saved_hvac_mode,
) )
await self.restore_hvac_mode(True) await self.restore_hvac_mode(True)
elif self._window_state: elif self._window_state == STATE_ON:
_LOGGER.info( _LOGGER.info(
"%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF "%s - Window is open. Set hvac_mode to '%s'", self, HVACMode.OFF
) )
@@ -1494,11 +1477,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._last_temperature_mesure = self.get_state_date_or_now(state) self._last_temperature_mesure = self.get_state_date_or_now(state)
# calculate the smooth_temperature with EMA calculation
self._ema_temp = self._ema_algo.calculate_ema(
self._cur_temp, self._last_temperature_mesure
)
_LOGGER.debug( _LOGGER.debug(
"%s - After setting _last_temperature_mesure %s , state.last_changed.replace=%s", "%s - After setting _last_temperature_mesure %s , state.last_changed.replace=%s",
self, self,
@@ -1608,10 +1586,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def _async_update_presence(self, new_state): async def _async_update_presence(self, new_state):
_LOGGER.info("%s - Updating presence. New state is %s", self, new_state) _LOGGER.debug("%s - Updating presence. New state is %s", self, new_state)
self._presence_state = ( self._presence_state = new_state
STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF
)
if self._attr_preset_mode in HIDDEN_PRESETS or self._presence_on is False: if self._attr_preset_mode in HIDDEN_PRESETS or self._presence_on is False:
_LOGGER.info( _LOGGER.info(
"%s - Ignoring presence change cause in Power or Security preset or presence not configured", "%s - Ignoring presence change cause in Power or Security preset or presence not configured",
@@ -1628,6 +1604,24 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]: if self._attr_preset_mode not in [PRESET_BOOST, PRESET_COMFORT, PRESET_ECO]:
return return
# Change temperature with preset named _away
# new_temp = None
# if new_state == STATE_ON or new_state == STATE_HOME:
# new_temp = self._presets[self._attr_preset_mode]
# _LOGGER.info(
# "%s - Someone is back home. Restoring temperature to %.2f",
# self,
# new_temp,
# )
# else:
# new_temp = self._presets_away[
# self.get_preset_away_name(self._attr_preset_mode)
# ]
# _LOGGER.info(
# "%s - No one is at home. Apply temperature %.2f",
# self,
# new_temp,
# )
new_temp = self.find_preset_temp(self.preset_mode) new_temp = self.find_preset_temp(self.preset_mode)
if new_temp is not None: if new_temp is not None:
_LOGGER.debug( _LOGGER.debug(
@@ -1702,7 +1696,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
return return
slope = self._window_auto_algo.add_temp_measurement( slope = self._window_auto_algo.add_temp_measurement(
temperature=self._ema_temp, datetime_measure=self._last_temperature_mesure temperature=self._cur_temp, datetime_measure=self._last_temperature_mesure
) )
_LOGGER.debug( _LOGGER.debug(
"%s - Window auto is on, check the alert. last slope is %.3f", "%s - Window auto is on, check the alert. last slope is %.3f",
@@ -1720,8 +1714,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
and self.hvac_mode != HVACMode.OFF and self.hvac_mode != HVACMode.OFF
): ):
if ( if (
self.proportional_algorithm not self.proportional_algorithm
and self.proportional_algorithm.on_percent <= 0.0 or self.proportional_algorithm.on_percent <= 0.0
): ):
_LOGGER.info( _LOGGER.info(
"%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event", "%s - Start auto detection of open window slope=%.3f but no heating detected (on_percent<=0). Forget the event",
@@ -1832,15 +1826,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._device_power, self._device_power,
) )
if self.is_over_climate: ret = self._current_power + self._device_power >= self._current_power_max
power_consumption_max = self._device_power
else:
power_consumption_max = max(
self._device_power / self.nb_underlying_entities,
self._device_power * self._prop_algorithm.on_percent,
)
ret = (self._current_power + power_consumption_max) >= self._current_power_max
if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF: if not self._overpowering_state and ret and self._hvac_mode != HVACMode.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'",
@@ -1858,7 +1844,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"current_power": self._current_power, "current_power": self._current_power,
"device_power": self._device_power, "device_power": self._device_power,
"current_power_max": self._current_power_max, "current_power_max": self._current_power_max,
"current_power_consumption": power_consumption_max,
}, },
) )
@@ -1886,10 +1871,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
}, },
) )
if self._overpowering_state != ret: self._overpowering_state = ret
self._overpowering_state = ret
self.update_custom_attributes()
return self._overpowering_state return self._overpowering_state
async def check_security(self) -> bool: async def check_security(self) -> bool:
@@ -2095,9 +2077,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
force, force,
) )
# calculate the smooth_temperature with EMA calculation
await self._async_manage_window_auto()
self.update_custom_attributes() self.update_custom_attributes()
return True return True
@@ -2118,7 +2097,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"""Update the custom extra attributes for the entity""" """Update the custom extra attributes for the entity"""
self._attr_extra_state_attributes: dict(str, str) = { self._attr_extra_state_attributes: dict(str, str) = {
"is_on": self.is_on,
"hvac_action": self.hvac_action, "hvac_action": self.hvac_action,
"hvac_mode": self.hvac_mode, "hvac_mode": self.hvac_mode,
"preset_mode": self.preset_mode, "preset_mode": self.preset_mode,
@@ -2138,7 +2116,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"power_temp": self._power_temp, "power_temp": self._power_temp,
# Already in super class - "target_temp": self.target_temperature, # Already in super class - "target_temp": self.target_temperature,
# Already in super class - "current_temp": self._cur_temp, # Already in super class - "current_temp": self._cur_temp,
"target_temperature_step": self.target_temperature_step,
"ext_current_temperature": self._cur_ext_temp, "ext_current_temperature": self._cur_ext_temp,
"ac_mode": self._ac_mode, "ac_mode": self._ac_mode,
"current_power": self._current_power, "current_power": self._current_power,
@@ -2146,11 +2123,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"saved_preset_mode": self._saved_preset_mode, "saved_preset_mode": self._saved_preset_mode,
"saved_target_temp": self._saved_target_temp, "saved_target_temp": self._saved_target_temp,
"saved_hvac_mode": self._saved_hvac_mode, "saved_hvac_mode": self._saved_hvac_mode,
"window_state": self.window_state, "window_state": self._window_state,
"motion_state": self._motion_state, "motion_state": self._motion_state,
"overpowering_state": self.overpowering_state, "overpowering_state": self._overpowering_state,
"presence_state": self._presence_state, "presence_state": self._presence_state,
"window_auto_state": self.window_auto_state, "window_auto_state": self._window_auto_state,
#PR - Adding Window ByPass
"window_bypass_state": self._window_bypass_state, "window_bypass_state": self._window_bypass_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_min_on_percent": self._security_min_on_percent,
@@ -2179,9 +2157,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"presence_sensor_entity_id": self._presence_sensor_entity_id, "presence_sensor_entity_id": self._presence_sensor_entity_id,
"power_sensor_entity_id": self._power_sensor_entity_id, "power_sensor_entity_id": self._power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id, "max_power_sensor_entity_id": self._max_power_sensor_entity_id,
"temperature_unit": self.temperature_unit,
"is_device_active": self.is_device_active,
"ema_temp": self._ema_temp,
} }
@callback @callback
@@ -2272,6 +2247,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
await self.async_control_heating() await self.async_control_heating()
self.update_custom_attributes() self.update_custom_attributes()
#PR - Adding Window ByPass
async def service_set_window_bypass_state(self, window_bypass): async def service_set_window_bypass_state(self, window_bypass):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_window_bypass service: versatile_thermostat.set_window_bypass
@@ -2280,25 +2256,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
target: target:
entity_id: climate.thermostat_1 entity_id: climate.thermostat_1
""" """
_LOGGER.info( _LOGGER.info("%s - Calling service_set_window_bypass, window_bypass: %s", self, window_bypass)
"%s - Calling service_set_window_bypass, window_bypass: %s",
self,
window_bypass,
)
self._window_bypass_state = window_bypass self._window_bypass_state = window_bypass
if not self._window_bypass_state and self._window_state: if not self._window_bypass_state and self._window_state == STATE_ON:
_LOGGER.info( _LOGGER.info("%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'", self, HVACMode.OFF)
"%s - Last window state was open & ByPass is now off. Set hvac_mode to '%s'",
self,
HVACMode.OFF,
)
self.save_hvac_mode() self.save_hvac_mode()
await self.async_set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)
if self._window_bypass_state and self._window_state: if self._window_bypass_state and self._window_state == STATE_ON:
_LOGGER.info( _LOGGER.info("%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode", self)
"%s - Last window state was open & ByPass is now on. Set hvac_mode to last available mode",
self,
)
await self.restore_hvac_mode(True) await self.restore_hvac_mode(True)
self.update_custom_attributes() self.update_custom_attributes()

View File

@@ -24,12 +24,12 @@ from .const import (
SERVICE_SET_PRESENCE, SERVICE_SET_PRESENCE,
SERVICE_SET_PRESET_TEMPERATURE, SERVICE_SET_PRESET_TEMPERATURE,
SERVICE_SET_SECURITY, SERVICE_SET_SECURITY,
#PR - Adding Window ByPass
SERVICE_SET_WINDOW_BYPASS, SERVICE_SET_WINDOW_BYPASS,
SERVICE_SET_AUTO_REGULATION_MODE,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE CONF_THERMOSTAT_VALVE,
) )
from .thermostat_switch import ThermostatOverSwitch from .thermostat_switch import ThermostatOverSwitch
@@ -99,6 +99,7 @@ async def async_setup_entry(
"service_set_security", "service_set_security",
) )
#PR - Adding Window ByPass
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_WINDOW_BYPASS, SERVICE_SET_WINDOW_BYPASS,
{ {
@@ -107,11 +108,3 @@ async def async_setup_entry(
}, },
"service_set_window_bypass_state", "service_set_window_bypass_state",
) )
platform.async_register_entity_service(
SERVICE_SET_AUTO_REGULATION_MODE,
{
vol.Required("auto_regulation_mode"): vol.In(["None", "Light", "Medium", "Strong", "Slow"]),
},
"service_set_auto_regulation_mode",
)

View File

@@ -1,50 +1,18 @@
""" Some usefull commons class """ """ Some usefull commons class """
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta
from homeassistant.core import HomeAssistant, callback, Event from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event, async_call_later from homeassistant.helpers.event import async_track_state_change_event, async_call_later
from homeassistant.util import dt as dt_util
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat
from .const import DOMAIN, DEVICE_MANUFACTURER from .const import DOMAIN, DEVICE_MANUFACTURER
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
class NowClass:
""" For testing purpose only"""
@staticmethod
def get_now(hass: HomeAssistant) -> datetime:
""" A test function to get the now.
For testing purpose this method can be overriden to get a specific
timestamp.
"""
return datetime.now( get_tz(hass))
def round_to_nearest(n:float, x: float)->float:
""" Round a number to the nearest x (which should be decimal but not null)
Example:
nombre1 = 3.2
nombre2 = 4.7
x = 0.3
nombre_arrondi1 = round_to_nearest(nombre1, x)
nombre_arrondi2 = round_to_nearest(nombre2, x)
print(nombre_arrondi1) # Output: 3.3
print(nombre_arrondi2) # Output: 4.6
"""
assert x > 0
return round(n * (1/x)) / (1/x)
class VersatileThermostatBaseEntity(Entity): class VersatileThermostatBaseEntity(Entity):
"""A base class for all entities""" """A base class for all entities"""
@@ -130,7 +98,7 @@ class VersatileThermostatBaseEntity(Entity):
await try_find_climate(None) await try_find_climate(None)
@callback @callback
async def async_my_climate_changed(self, event: Event): # pylint: disable=unused-argument async def async_my_climate_changed(self, event: Event):
"""Called when my climate have change """Called when my climate have change
This method aims to be overriden to take the status change This method aims to be overriden to take the status change
""" """

View File

@@ -102,9 +102,6 @@ from .const import (
CONF_AUTO_REGULATION_MODES, CONF_AUTO_REGULATION_MODES,
CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH,
UnknownEntity, UnknownEntity,
WindowOpenDetectionMethod, WindowOpenDetectionMethod,
) )
@@ -242,7 +239,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
] ]
), ),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean, vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
vol.Optional(CONF_INVERSE_SWITCH, default=False): cv.boolean,
} }
) )
@@ -268,9 +264,6 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
options=CONF_AUTO_REGULATION_MODES, translation_key="auto_regulation_mode" options=CONF_AUTO_REGULATION_MODES, translation_key="auto_regulation_mode"
) )
), ),
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=0.5): vol.Coerce(float),
vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int
} }
) )

View File

@@ -17,7 +17,6 @@ from homeassistant.exceptions import HomeAssistantError
from .prop_algorithm import ( from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
) )
PRESET_AC_SUFFIX = "_ac" PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
@@ -84,16 +83,11 @@ CONF_VALVE = "valve_entity_id"
CONF_VALVE_2 = "valve_entity2_id" CONF_VALVE_2 = "valve_entity2_id"
CONF_VALVE_3 = "valve_entity3_id" CONF_VALVE_3 = "valve_entity3_id"
CONF_VALVE_4 = "valve_entity4_id" CONF_VALVE_4 = "valve_entity4_id"
CONF_AUTO_REGULATION_MODE = "auto_regulation_mode" CONF_AUTO_REGULATION_MODE= "auto_regulation_mode"
CONF_AUTO_REGULATION_NONE = "auto_regulation_none" CONF_AUTO_REGULATION_NONE= "auto_regulation_none"
CONF_AUTO_REGULATION_SLOW = "auto_regulation_slow" CONF_AUTO_REGULATION_LIGHT= "auto_regulation_light"
CONF_AUTO_REGULATION_LIGHT = "auto_regulation_light" CONF_AUTO_REGULATION_MEDIUM= "auto_regulation_medium"
CONF_AUTO_REGULATION_MEDIUM = "auto_regulation_medium" CONF_AUTO_REGULATION_STRONG= "auto_regulation_strong"
CONF_AUTO_REGULATION_STRONG = "auto_regulation_strong"
CONF_AUTO_REGULATION_EXPERT = "auto_regulation_expert"
CONF_AUTO_REGULATION_DTEMP = "auto_regulation_dtemp"
CONF_AUTO_REGULATION_PERIOD_MIN = "auto_regulation_periode_min"
CONF_INVERSE_SWITCH = "inverse_switch_command"
CONF_PRESETS = { CONF_PRESETS = {
p: f"{p}_temp" p: f"{p}_temp"
@@ -195,10 +189,7 @@ ALL_CONF = (
CONF_VALVE_2, CONF_VALVE_2,
CONF_VALVE_3, CONF_VALVE_3,
CONF_VALVE_4, CONF_VALVE_4,
CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_MODE
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH,
] ]
+ CONF_PRESETS_VALUES + CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES + CONF_PRESETS_AWAY_VALUES
@@ -210,28 +201,17 @@ CONF_FUNCTIONS = [
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
] ]
CONF_AUTO_REGULATION_MODES = [ CONF_AUTO_REGULATION_MODES = [CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_STRONG]
CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_EXPERT,
]
CONF_THERMOSTAT_TYPES = [ CONF_THERMOSTAT_TYPES = [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_VALVE]
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_VALVE,
]
SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE SUPPORT_FLAGS = ClimateEntityFeature.TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"
SERVICE_SET_SECURITY = "set_security" SERVICE_SET_SECURITY = "set_security"
#PR - Adding Window ByPass
SERVICE_SET_WINDOW_BYPASS = "set_window_bypass" SERVICE_SET_WINDOW_BYPASS = "set_window_bypass"
SERVICE_SET_AUTO_REGULATION_MODE = "set_auto_regulation_mode"
DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5 DEFAULT_SECURITY_MIN_ON_PERCENT = 0.5
DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1 DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
@@ -239,73 +219,33 @@ DEFAULT_SECURITY_DEFAULT_ON_PERCENT = 0.1
ATTR_TOTAL_ENERGY = "total_energy" ATTR_TOTAL_ENERGY = "total_energy"
ATTR_MEAN_POWER_CYCLE = "mean_cycle_power" ATTR_MEAN_POWER_CYCLE = "mean_cycle_power"
# A special regulation parameter suggested by @Maia here: https://github.com/jmcollin78/versatile_thermostat/discussions/154
class RegulationParamSlow:
"""Light parameters for slow latency regulation"""
kp: float = 0.2 # 20% of the current internal regulation offset are caused by the current difference of target temperature and room temperature
ki: float = (
0.8 / 288.0
) # 80% of the current internal regulation offset are caused by the average offset of the past 24 hours
k_ext: float = (
1.0 / 25.0
) # this will add 1°C to the offset when it's 25°C colder outdoor than indoor
offset_max: float = 2.0 # limit to a final offset of -2°C to +2°C
stabilization_threshold: float = 0.0 # this needs to be disabled as otherwise the long term accumulated error will always be reset when the temp briefly crosses from/to below/above the target
accumulated_error_threshold: float = (
2.0 * 288
) # this allows up to 2°C long term offset in both directions
class RegulationParamLight: class RegulationParamLight:
"""Light parameters for regulation""" """ Light parameters for regulation"""
kp:float = 0.2
kp: float = 0.2 ki:float = 0.05
ki: float = 0.05 k_ext:float = 0.1
k_ext: float = 0.05 offset_max:float = 2
offset_max: float = 1.5 stabilization_threshold:float = 0.1
stabilization_threshold: float = 0.1 accumulated_error_threshold:float = 20
accumulated_error_threshold: float = 10
class RegulationParamMedium: class RegulationParamMedium:
"""Light parameters for regulation""" """ Medium parameters for regulation"""
kp:float = 0.4
kp: float = 0.3 ki:float = 0.08
ki: float = 0.05 k_ext:float = 0.1
k_ext: float = 0.1 offset_max:float = 3
offset_max: float = 2 stabilization_threshold:float = 0.1
stabilization_threshold: float = 0.1 accumulated_error_threshold:float = 25
accumulated_error_threshold: float = 20
class RegulationParamStrong: class RegulationParamStrong:
"""Strong parameters for regulation """ Strong parameters for regulation"""
A set of parameters which doesn't take into account the external temp kp:float = 0.6
and concentrate to internal temp error + accumulated error. ki:float = 0.1
This should work for cold external conditions which else generates k_ext:float = 0.2
high external_offset""" offset_max:float = 4
stabilization_threshold:float = 0.1
kp: float = 0.4 accumulated_error_threshold:float = 30
ki: float = 0.08
k_ext: float = 0.0
offset_max: float = 5
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 50
# Not used now
class RegulationParamVeryStrong:
"""Strong parameters for regulation"""
kp: float = 0.6
ki: float = 0.1
k_ext: float = 0.2
offset_max: float = 4
stabilization_threshold: float = 0.1
accumulated_error_threshold: float = 30
class EventType(Enum): class EventType(Enum):
"""The event type that can be sent""" """The event type that can be sent"""
@@ -325,10 +265,8 @@ class UnknownEntity(HomeAssistantError):
class WindowOpenDetectionMethod(HomeAssistantError): class WindowOpenDetectionMethod(HomeAssistantError):
"""Error to indicate there is an error in the window open detection method given.""" """Error to indicate there is an error in the window open detection method given."""
class overrides: # pylint: disable=invalid-name
class overrides: # pylint: disable=invalid-name """ An annotation to inform overrides """
"""An annotation to inform overrides"""
def __init__(self, func): def __init__(self, func):
self.func = func self.func = func

View File

@@ -1,85 +0,0 @@
# pylint: disable=line-too-long
"""The Estimated Mobile Average calculation used for temperature slope
and maybe some others feature"""
import logging
import math
from datetime import datetime, tzinfo
_LOGGER = logging.getLogger(__name__)
MIN_TIME_DECAY_SEC = 0
# As for the EMA calculation of irregular time series, I've seen that it might be useful to
# have an upper limit for alpha in case the last measurement was too long ago.
# For example when using a half life of 10 minutes a measurement that is 60 minutes ago
# (if there's nothing inbetween) would contribute to the smoothed value with 1,5%,
# giving the current measurement 98,5% relevance. It could be wise to limit the alpha to e.g. 4x the half life (=0.9375).
MAX_ALPHA = 0.5
class ExponentialMovingAverage:
"""A class that will do the Estimated Mobile Average calculation"""
def __init__(
self, vterm_name: str, halflife: float, timezone: tzinfo, precision: int = 3
):
"""The halflife is the duration in secondes of a normal cycle"""
self._halflife: float = halflife
self._timezone = timezone
self._current_ema: float = None
self._last_timestamp: datetime = datetime.now(self._timezone)
self._name = vterm_name
self._precision = precision
def __str__(self) -> str:
return f"EMA-{self._name}"
def calculate_ema(self, measurement: float, timestamp: datetime) -> float | None:
"""Calculate the new EMA from a new measurement measured at timestamp
Return the EMA or None if all parameters are not initialized now
"""
if measurement is None or timestamp is None:
_LOGGER.warning(
"%s - Cannot calculate EMA: measurement and timestamp are mandatory. This message can be normal at startup but should not persist",
self,
)
return measurement
if self._current_ema is None:
_LOGGER.debug(
"%s - First init of the EMA",
self,
)
self._current_ema = measurement
self._last_timestamp = timestamp
return self._current_ema
time_decay = (timestamp - self._last_timestamp).total_seconds()
if time_decay < MIN_TIME_DECAY_SEC:
_LOGGER.debug(
"%s - time_decay %s is too small (< %s). Forget the measurement",
self,
time_decay,
MIN_TIME_DECAY_SEC,
)
return self._current_ema
alpha = 1 - math.exp(math.log(0.5) * time_decay / self._halflife)
# capping alpha to avoid gap if last measurement was long time ago
alpha = min(alpha, MAX_ALPHA)
new_ema = alpha * measurement + (1 - alpha) * self._current_ema
self._last_timestamp = timestamp
self._current_ema = new_ema
_LOGGER.debug(
"%s - timestamp=%s alpha=%.2f measurement=%.2f current_ema=%.2f new_ema=%.2f",
self,
timestamp,
alpha,
measurement,
self._current_ema,
new_ema,
)
return round(self._current_ema, self._precision)

View File

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

View File

@@ -1,4 +1,3 @@
# pylint: disable=line-too-long
""" This file implements the Open Window by temperature algorithm """ This file implements the Open Window by temperature algorithm
This algo works the following way: This algo works the following way:
- each time a new temperature is measured - each time a new temperature is measured
@@ -13,7 +12,7 @@ from datetime import datetime
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# To filter bad values # To filter bad values
MIN_DELTA_T_SEC = 15 # two temp mesure should be > 10 sec MIN_DELTA_T_SEC = 10 # two temp mesure should be > 10 sec
MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point MAX_SLOPE_VALUE = 2 # slope cannot be > 2 or < -2 -> else this is an aberrant point
@@ -72,10 +71,10 @@ class WindowOpenDetectionAlgorithm:
) )
return lspe return lspe
# if self._last_slope is None: if self._last_slope is None:
self._last_slope = round(new_slope, 4) self._last_slope = new_slope
# else: else:
# self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope) self._last_slope = (0.5 * self._last_slope) + (0.5 * new_slope)
self._last_datetime = datetime_measure self._last_datetime = datetime_measure
self._last_temperature = temperature self._last_temperature = temperature

View File

@@ -5,9 +5,8 @@ import logging
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class PITemperatureRegulator: class PITemperatureRegulator:
"""A class implementing a PI Algorithm """ A class implementing a PI Algorithm
PI algorithms calculate a target temperature by adding an offset which is calculating as follow: PI algorithms calculate a target temperature by adding an offset which is calculating as follow:
- offset = kp * error + ki * accumulated_error - offset = kp * error + ki * accumulated_error
@@ -17,54 +16,25 @@ class PITemperatureRegulator:
- call set_target_temp when the target temperature change. - call set_target_temp when the target temperature change.
""" """
def __init__( def __init__(self, target_temp: float, kp: float, ki: float, k_ext: float, offset_max: float, stabilization_threshold: float, accumulated_error_threshold: float):
self, self.target_temp:float = target_temp
target_temp: float, self.kp:float = kp # proportionnel gain
kp: float, self.ki:float = ki # integral gain
ki: float, self.k_ext:float = k_ext # exterior gain
k_ext: float, self.offset_max:float = offset_max
offset_max: float, self.stabilization_threshold:float = stabilization_threshold
stabilization_threshold: float, self.accumulated_error:float = 0
accumulated_error_threshold: float, self.accumulated_error_threshold:float = accumulated_error_threshold
):
self.target_temp: float = target_temp
self.kp: float = kp # proportionnel gain
self.ki: float = ki # integral gain
self.k_ext: float = k_ext # exterior gain
self.offset_max: float = offset_max
self.stabilization_threshold: float = stabilization_threshold
self.accumulated_error: float = 0
self.accumulated_error_threshold: float = accumulated_error_threshold
def reset_accumulated_error(self):
"""Reset the accumulated error"""
self.accumulated_error = 0
def set_accumulated_error(self, accumulated_error):
"""Allow to persist and restore the accumulated_error"""
self.accumulated_error = accumulated_error
def set_target_temp(self, target_temp): def set_target_temp(self, target_temp):
"""Set the new target_temp""" """ Set the new target_temp"""
self.target_temp = target_temp self.target_temp = target_temp
# Do not reset the accumulated error self.accumulated_error = 0
# Discussion #191. After a target change we should reset the accumulated error which is certainly wrong now.
if self.accumulated_error < 0:
self.accumulated_error = 0
def calculate_regulated_temperature( def calculate_regulated_temperature(self, internal_temp: float, external_temp:float): # pylint: disable=unused-argument
self, internal_temp: float, external_temp: float """ Calculate a new target_temp given some temperature"""
): # pylint: disable=unused-argument if internal_temp is None or external_temp is None:
"""Calculate a new target_temp given some temperature""" _LOGGER.warning("Internal_temp or external_temp are not set. Regulation will be suspended")
if internal_temp is None:
_LOGGER.warning(
"Temporarily skipping the self-regulation algorithm while the configured sensor for room temperature is unavailable"
)
return self.target_temp
if external_temp is None:
_LOGGER.warning(
"Temporarily skipping the self-regulation algorithm while the configured sensor for outdoor temperature is unavailable"
)
return self.target_temp return self.target_temp
# Calculate the error factor (P) # Calculate the error factor (P)
@@ -74,38 +44,27 @@ class PITemperatureRegulator:
self.accumulated_error += error self.accumulated_error += error
# Capping of the error # Capping of the error
self.accumulated_error = min( self.accumulated_error = min(self.accumulated_error_threshold, max(-self.accumulated_error_threshold, self.accumulated_error))
self.accumulated_error_threshold,
max(-self.accumulated_error_threshold, self.accumulated_error),
)
# Calculate the offset (proportionnel + intégral) # Calculate the offset (proportionnel + intégral)
offset = self.kp * error + self.ki * self.accumulated_error offset = self.kp * error + self.ki * self.accumulated_error
# Calculate the exterior offset # Calculate the exterior offset
# For Maia tests - use the internal_temp vs external_temp and not target_temp - external_temp offset_ext = self.k_ext * (self.target_temp - external_temp)
offset_ext = self.k_ext * (internal_temp - external_temp)
# Capping of offset_ext # Capping of offset_ext
total_offset = offset + offset_ext total_offset = offset + offset_ext
total_offset = min(self.offset_max, max(-self.offset_max, total_offset)) total_offset = min(self.offset_max, max(-self.offset_max, total_offset))
# If temperature is near the target_temp, reset the accumulated_error # If temperature is near the target_temp, reset the accumulated_error
# Issue #199 - don't reset the accumulation error if abs(error) < self.stabilization_threshold:
# if abs(error) < self.stabilization_threshold: _LOGGER.debug("Stabilisation")
# _LOGGER.debug("Stabilisation") self.accumulated_error = 0
# self.accumulated_error = 0
result = round(self.target_temp + total_offset, 1) result = round(self.target_temp + total_offset, 1)
_LOGGER.debug( _LOGGER.debug("PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f",
"PITemperatureRegulator - Error: %.2f accumulated_error: %.2f offset: %.2f offset_ext: %.2f target_tem: %.1f regulatedTemp: %.1f", error, self.accumulated_error, offset, offset_ext, self.target_temp, result)
error,
self.accumulated_error,
offset,
offset_ext,
self.target_temp,
result,
)
return result return result

View File

@@ -11,7 +11,7 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorDeviceClass, SensorDeviceClass,
SensorStateClass, SensorStateClass,
UnitOfTemperature, UnitOfTemperature
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -54,10 +54,7 @@ async def async_setup_entry(
] ]
if entry.data.get(CONF_DEVICE_POWER): if entry.data.get(CONF_DEVICE_POWER):
entities.append(EnergySensor(hass, unique_id, name, entry.data)) entities.append(EnergySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) in [ if entry.data.get(CONF_THERMOSTAT_TYPE) in [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_VALVE]:
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_VALVE,
]:
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data)) entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI: if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
@@ -205,9 +202,6 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
if self.my_climate and self.my_climate.proportional_algorithm if self.my_climate and self.my_climate.proportional_algorithm
else None else None
) )
if on_percent is None:
return
if math.isnan(on_percent) or math.isinf(on_percent): if math.isnan(on_percent) or math.isinf(on_percent):
raise ValueError(f"Sensor has illegal state {on_percent}") raise ValueError(f"Sensor has illegal state {on_percent}")
@@ -240,7 +234,6 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Return the suggested number of decimal digits for display.""" """Return the suggested number of decimal digits for display."""
return 1 return 1
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity): class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on percent sensor which exposes the on_percent in a cycle""" """Representation of a on percent sensor which exposes the on_percent in a cycle"""
@@ -302,10 +295,6 @@ class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
if self.my_climate and self.my_climate.proportional_algorithm if self.my_climate and self.my_climate.proportional_algorithm
else None else None
) )
if on_time is None:
return
if math.isnan(on_time) or math.isinf(on_time): if math.isnan(on_time) or math.isinf(on_time):
raise ValueError(f"Sensor has illegal state {on_time}") raise ValueError(f"Sensor has illegal state {on_time}")
@@ -351,9 +340,6 @@ class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
if self.my_climate and self.my_climate.proportional_algorithm if self.my_climate and self.my_climate.proportional_algorithm
else None else None
) )
if off_time is None:
return
if math.isnan(off_time) or math.isinf(off_time): if math.isnan(off_time) or math.isinf(off_time):
raise ValueError(f"Sensor has illegal state {off_time}") raise ValueError(f"Sensor has illegal state {off_time}")
@@ -490,7 +476,6 @@ class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Return the suggested number of decimal digits for display.""" """Return the suggested number of decimal digits for display."""
return 2 return 2
class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity): class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a Energy sensor which exposes the energy""" """Representation of a Energy sensor which exposes the energy"""
@@ -508,9 +493,7 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
if math.isnan(self.my_climate.regulated_target_temp) or math.isinf( if math.isnan(self.my_climate.regulated_target_temp) or math.isinf(
self.my_climate.regulated_target_temp self.my_climate.regulated_target_temp
): ):
raise ValueError( raise ValueError(f"Sensor has illegal state {self.my_climate.regulated_target_temp}")
f"Sensor has illegal state {self.my_climate.regulated_target_temp}"
)
old_state = self._attr_native_value old_state = self._attr_native_value
self._attr_native_value = round( self._attr_native_value = round(

View File

@@ -137,27 +137,4 @@ set_window_bypass:
advanced: false advanced: false
default: true default: true
selector: selector:
boolean: boolean:
set_auto_regulation_mode:
name: Set Auto Regulation mode
description: Change the mode of self-regulation (only for VTherm over climate)
target:
entity:
integration: versatile_thermostat
fields:
auto_regulation_mode:
name: Auto regulation mode
description: Possible values
required: true
advanced: false
default: true
selector:
select:
options:
- "None"
- "Light"
- "Medium"
- "Strong"
- "Slow"
- "Expert"

View File

@@ -39,10 +39,7 @@
"valve_entity2_id": "2nd valve number", "valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number", "valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation"
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -59,10 +56,7 @@
"valve_entity2_id": "2nd valve number entity id", "valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id", "valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature"
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -208,10 +202,7 @@
"valve_entity2_id": "2nd valve number", "valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number", "valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation"
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -228,10 +219,7 @@
"valve_entity2_id": "2nd valve number entity id", "valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id", "valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature"
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -348,11 +336,9 @@
}, },
"auto_regulation_mode": { "auto_regulation_mode": {
"options": { "options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong", "auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium", "auto_regulation_medium": "Medium",
"auto_regulation_light": "Light", "auto_regulation_light": "Light",
"auto_regulation_expert": "Expert",
"auto_regulation_none": "No auto-regulation" "auto_regulation_none": "No auto-regulation"
} }
} }

View File

@@ -1,90 +1,62 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long
""" A climate over switch classe """ """ A climate over switch classe """
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import ( from homeassistant.helpers.event import async_track_state_change_event, async_track_time_interval
async_track_state_change_event,
async_track_time_interval,
)
from homeassistant.components.climate import HVACAction, HVACMode from homeassistant.components.climate import HVACAction, HVACMode
from .commons import NowClass, round_to_nearest
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat
from .pi_algorithm import PITemperatureRegulator from .pi_algorithm import PITemperatureRegulator
from .const import ( from .const import (
overrides, overrides,
DOMAIN,
CONF_CLIMATE, CONF_CLIMATE,
CONF_CLIMATE_2, CONF_CLIMATE_2,
CONF_CLIMATE_3, CONF_CLIMATE_3,
CONF_CLIMATE_4, CONF_CLIMATE_4,
CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_SLOW,
CONF_AUTO_REGULATION_LIGHT, CONF_AUTO_REGULATION_LIGHT,
CONF_AUTO_REGULATION_MEDIUM, CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_STRONG,
CONF_AUTO_REGULATION_EXPERT,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
RegulationParamSlow,
RegulationParamLight, RegulationParamLight,
RegulationParamMedium, RegulationParamMedium,
RegulationParamStrong, RegulationParamStrong
) )
from .vtherm_api import VersatileThermostatAPI
from .underlyings import UnderlyingClimate from .underlyings import UnderlyingClimate
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverClimate(BaseThermostat): class ThermostatOverClimate(BaseThermostat):
"""Representation of a base class for a Versatile Thermostat over a climate""" """Representation of a base class for a Versatile Thermostat over a climate"""
_regulation_mode:str = None
_auto_regulation_mode: str = None
_regulation_algo = None _regulation_algo = None
_regulated_target_temp: float = None _regulated_target_temp: float = None
_auto_regulation_dtemp: float = None
_auto_regulation_period_min: int = None
_last_regulation_change: datetime = None
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
BaseThermostat._entity_component_unrecorded_attributes.union( {
frozenset( "is_over_climate", "start_hvac_action_date", "underlying_climate_0", "underlying_climate_1",
{ "underlying_climate_2", "underlying_climate_3", "regulation_accumulated_error"
"is_over_climate", }))
"start_hvac_action_date",
"underlying_climate_0",
"underlying_climate_1",
"underlying_climate_2",
"underlying_climate_3",
"regulation_accumulated_error",
"auto_regulation_mode",
}
)
)
)
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat over switch.""" """Initialize the thermostat over switch."""
# super.__init__ calls post_init at the end. So it must be called after regulation initialization # super.__init__ calls post_init at the end. So it must be called after regulation initialization
super().__init__(hass, unique_id, name, entry_infos) super().__init__(hass, unique_id, name, entry_infos)
self._regulated_target_temp = self.target_temperature self._regulated_target_temp = self.target_temperature
self._last_regulation_change = NowClass.get_now(hass)
@property @property
def is_over_climate(self) -> bool: def is_over_climate(self) -> bool:
"""True if the Thermostat is over_climate""" """ True if the Thermostat is over_climate"""
return True return True
@property @property
def hvac_action(self) -> HVACAction | None: def hvac_action(self) -> HVACAction | None:
"""Returns the current hvac_action by checking all hvac_action of the underlyings""" """ Returns the current hvac_action by checking all hvac_action of the underlyings """
# if one not IDLE or OFF -> return it # if one not IDLE or OFF -> return it
# else if one IDLE -> IDLE # else if one IDLE -> IDLE
@@ -108,65 +80,25 @@ class ThermostatOverClimate(BaseThermostat):
await super()._async_internal_set_temperature(temperature) await super()._async_internal_set_temperature(temperature)
self._regulation_algo.set_target_temp(self.target_temperature) self._regulation_algo.set_target_temp(self.target_temperature)
await self._send_regulated_temperature(force=True) await self._send_regulated_temperature()
async def _send_regulated_temperature(self, force=False): async def _send_regulated_temperature(self):
"""Sends the regulated temperature to all underlying""" """ Sends the regulated temperature to all underlying """
_LOGGER.info( new_regulated_temp = self._regulation_algo.calculate_regulated_temperature(self.current_temperature, self._cur_ext_temp)
"%s - Calling ThermostatClimate._send_regulated_temperature force=%s", if new_regulated_temp != self._regulated_target_temp:
self, _LOGGER.info("%s - Regulated temp have changed to %.1f. Resend it to underlyings", self, new_regulated_temp)
force, self._regulated_target_temp = new_regulated_temp
)
now: datetime = NowClass.get_now(self._hass) for under in self._underlyings:
period = float((now - self._last_regulation_change).total_seconds()) / 60.0 await under.set_temperature(
if not force and period < self._auto_regulation_period_min: self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
_LOGGER.info( )
"%s - period (%.1f) min is < %.0f min -> forget the regulation send", else:
self, _LOGGER.debug("%s - No change on regulated temperature (%.1f)", self, self._regulated_target_temp)
period,
self._auto_regulation_period_min,
)
return
if not self._regulated_target_temp:
self._regulated_target_temp = self.target_temperature
_LOGGER.info("%s - regulation calculation will be done", self)
self._last_regulation_change = now
new_regulated_temp = round_to_nearest(
self._regulation_algo.calculate_regulated_temperature(
self.current_temperature, self._cur_ext_temp
),
self._auto_regulation_dtemp,
)
dtemp = new_regulated_temp - self._regulated_target_temp
if not force and abs(dtemp) < self._auto_regulation_dtemp:
_LOGGER.info(
"%s - dtemp (%.1f) is < %.1f -> forget the regulation send",
self,
dtemp,
self._auto_regulation_dtemp,
)
return
self._regulated_target_temp = new_regulated_temp
_LOGGER.info(
"%s - Regulated temp have changed to %.1f. Resend it to underlyings",
self,
new_regulated_temp,
)
for under in self._underlyings:
await under.set_temperature(
self.regulated_target_temp, self._attr_max_temp, self._attr_min_temp
)
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, entry_infos):
"""Initialize the Thermostat""" """ Initialize the Thermostat"""
super().post_init(entry_infos) super().post_init(entry_infos)
for climate in [ for climate in [
@@ -184,27 +116,9 @@ class ThermostatOverClimate(BaseThermostat):
) )
) )
self.choose_auto_regulation_mode( self._regulation_mode = entry_infos.get(CONF_AUTO_REGULATION_MODE) if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None else CONF_AUTO_REGULATION_NONE
entry_infos.get(CONF_AUTO_REGULATION_MODE)
if entry_infos.get(CONF_AUTO_REGULATION_MODE) is not None
else CONF_AUTO_REGULATION_NONE
)
self._auto_regulation_dtemp = ( if self._regulation_mode == CONF_AUTO_REGULATION_LIGHT:
entry_infos.get(CONF_AUTO_REGULATION_DTEMP)
if entry_infos.get(CONF_AUTO_REGULATION_DTEMP) is not None
else 0.5
)
self._auto_regulation_period_min = (
entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN)
if entry_infos.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
else 5
)
def choose_auto_regulation_mode(self, auto_regulation_mode):
"""Choose or change the regulation mode"""
self._auto_regulation_mode = auto_regulation_mode
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
self._regulation_algo = PITemperatureRegulator( self._regulation_algo = PITemperatureRegulator(
self.target_temperature, self.target_temperature,
RegulationParamLight.kp, RegulationParamLight.kp,
@@ -212,9 +126,8 @@ class ThermostatOverClimate(BaseThermostat):
RegulationParamLight.k_ext, RegulationParamLight.k_ext,
RegulationParamLight.offset_max, RegulationParamLight.offset_max,
RegulationParamLight.stabilization_threshold, RegulationParamLight.stabilization_threshold,
RegulationParamLight.accumulated_error_threshold, RegulationParamLight.accumulated_error_threshold)
) elif self._regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_MEDIUM:
self._regulation_algo = PITemperatureRegulator( self._regulation_algo = PITemperatureRegulator(
self.target_temperature, self.target_temperature,
RegulationParamMedium.kp, RegulationParamMedium.kp,
@@ -222,9 +135,8 @@ class ThermostatOverClimate(BaseThermostat):
RegulationParamMedium.k_ext, RegulationParamMedium.k_ext,
RegulationParamMedium.offset_max, RegulationParamMedium.offset_max,
RegulationParamMedium.stabilization_threshold, RegulationParamMedium.stabilization_threshold,
RegulationParamMedium.accumulated_error_threshold, RegulationParamMedium.accumulated_error_threshold)
) elif self._regulation_mode == CONF_AUTO_REGULATION_STRONG:
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_STRONG:
self._regulation_algo = PITemperatureRegulator( self._regulation_algo = PITemperatureRegulator(
self.target_temperature, self.target_temperature,
RegulationParamStrong.kp, RegulationParamStrong.kp,
@@ -232,50 +144,11 @@ class ThermostatOverClimate(BaseThermostat):
RegulationParamStrong.k_ext, RegulationParamStrong.k_ext,
RegulationParamStrong.offset_max, RegulationParamStrong.offset_max,
RegulationParamStrong.stabilization_threshold, RegulationParamStrong.stabilization_threshold,
RegulationParamStrong.accumulated_error_threshold, RegulationParamStrong.accumulated_error_threshold)
) else:
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_SLOW:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
RegulationParamSlow.kp,
RegulationParamSlow.ki,
RegulationParamSlow.k_ext,
RegulationParamSlow.offset_max,
RegulationParamSlow.stabilization_threshold,
RegulationParamSlow.accumulated_error_threshold,
)
elif self._auto_regulation_mode == CONF_AUTO_REGULATION_EXPERT:
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(
self._hass
)
if api is not None:
if (expert_param := api.self_regulation_expert) is not None:
self._regulation_algo = PITemperatureRegulator(
self.target_temperature,
expert_param.get("kp"),
expert_param.get("ki"),
expert_param.get("k_ext"),
expert_param.get("offset_max"),
expert_param.get("stabilization_threshold"),
expert_param.get("accumulated_error_threshold"),
)
else:
_LOGGER.error(
"%s - Cannot initialize Expert self-regulation mode due to VTherm API doesn't exists. Please contact the publisher of the integration",
self,
)
else:
_LOGGER.error(
"%s - Cannot initialize Expert self-regulation mode cause the configuration in configuration.yaml have not been found. Please see readme documentation for %s",
self,
DOMAIN,
)
if not self._regulation_algo:
# A default empty algo (which does nothing) # A default empty algo (which does nothing)
self._regulation_algo = PITemperatureRegulator( self._regulation_algo = PITemperatureRegulator(
self.target_temperature, 0, 0, 0, 0, 0.1, 0 self.target_temperature, 0, 0, 0, 0, 0.1, 0)
)
@overrides @overrides
async def async_added_to_hass(self): async def async_added_to_hass(self):
@@ -302,51 +175,29 @@ class ThermostatOverClimate(BaseThermostat):
) )
) )
@overrides
def restore_specific_previous_state(self, old_state):
"""Restore my specific attributes from previous state"""
old_error = old_state.attributes.get("regulation_accumulated_error")
if old_error:
self._regulation_algo.set_accumulated_error(old_error)
_LOGGER.debug(
"%s - Old regulation accumulated_error have been restored to %f",
self,
old_error,
)
@overrides @overrides
def update_custom_attributes(self): def update_custom_attributes(self):
"""Custom attributes""" """ Custom attributes """
super().update_custom_attributes() super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["start_hvac_action_date"] = (
"start_hvac_action_date" self._underlying_climate_start_hvac_action_date)
] = self._underlying_climate_start_hvac_action_date self._attr_extra_state_attributes["underlying_climate_0"] = (
self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[ self._underlyings[0].entity_id)
0
].entity_id
self._attr_extra_state_attributes["underlying_climate_1"] = ( self._attr_extra_state_attributes["underlying_climate_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
) )
self._attr_extra_state_attributes["underlying_climate_2"] = ( self._attr_extra_state_attributes["underlying_climate_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
) )
self._attr_extra_state_attributes["underlying_climate_3"] = ( self._attr_extra_state_attributes["underlying_climate_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
) )
if self.is_regulated: if self.is_regulated:
self._attr_extra_state_attributes["is_regulated"] = self.is_regulated self._attr_extra_state_attributes["regulated_target_temp"] = self._regulated_target_temp
self._attr_extra_state_attributes[ self._attr_extra_state_attributes["regulation_accumulated_error"] = self._regulation_algo.accumulated_error
"regulated_target_temperature"
] = self._regulated_target_temp
self._attr_extra_state_attributes[
"auto_regulation_mode"
] = self.auto_regulation_mode
self._attr_extra_state_attributes[
"regulation_accumulated_error"
] = self._regulation_algo.accumulated_error
self.async_write_ha_state() self.async_write_ha_state()
_LOGGER.debug( _LOGGER.debug(
@@ -579,19 +430,19 @@ class ThermostatOverClimate(BaseThermostat):
return ret return ret
@property @property
def auto_regulation_mode(self): def regulation_mode(self):
"""Get the regulation mode""" """ Get the regulation mode """
return self._auto_regulation_mode return self._regulation_mode
@property @property
def regulated_target_temp(self): def regulated_target_temp(self):
"""Get the regulated target temperature""" """ Get the regulated target temperature """
return self._regulated_target_temp return self._regulated_target_temp
@property @property
def is_regulated(self): def is_regulated(self):
"""Check if the ThermostatOverClimate is regulated""" """ Check if the ThermostatOverClimate is regulated """
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE return self.regulation_mode != CONF_AUTO_REGULATION_NONE
@property @property
def hvac_modes(self): def hvac_modes(self):
@@ -766,32 +617,3 @@ class ThermostatOverClimate(BaseThermostat):
await under.set_swing_mode(swing_mode) await under.set_swing_mode(swing_mode)
self._swing_mode = swing_mode self._swing_mode = swing_mode
self.async_write_ha_state() self.async_write_ha_state()
async def service_set_auto_regulation_mode(self, auto_regulation_mode):
"""Called by a service call:
service: versatile_thermostat.set_auto_regulation_mode
data:
auto_regulation_mode: [None | Light | Medium | Strong]
target:
entity_id: climate.thermostat_1
"""
_LOGGER.info(
"%s - Calling service_set_auto_regulation_mode, auto_regulation_mode: %s",
self,
auto_regulation_mode,
)
if auto_regulation_mode == "None":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_NONE)
elif auto_regulation_mode == "Light":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_LIGHT)
elif auto_regulation_mode == "Medium":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_MEDIUM)
elif auto_regulation_mode == "Strong":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_STRONG)
elif auto_regulation_mode == "Slow":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_SLOW)
elif auto_regulation_mode == "Expert":
self.choose_auto_regulation_mode(CONF_AUTO_REGULATION_EXPERT)
await self._send_regulated_temperature()
self.update_custom_attributes()

View File

@@ -11,8 +11,7 @@ from .const import (
CONF_HEATER_2, CONF_HEATER_2,
CONF_HEATER_3, CONF_HEATER_3,
CONF_HEATER_4, CONF_HEATER_4,
CONF_INVERSE_SWITCH, overrides
overrides,
) )
from .base_thermostat import BaseThermostat from .base_thermostat import BaseThermostat
@@ -21,59 +20,29 @@ from .prop_algorithm import PropAlgorithm
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat): class ThermostatOverSwitch(BaseThermostat):
"""Representation of a base class for a Versatile Thermostat over a switch.""" """Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = ( _entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union(frozenset(
BaseThermostat._entity_component_unrecorded_attributes.union( {
frozenset( "is_over_switch", "underlying_switch_0", "underlying_switch_1",
{ "underlying_switch_2", "underlying_switch_3", "on_time_sec", "off_time_sec",
"is_over_switch", "cycle_min", "function", "tpi_coef_int", "tpi_coef_ext"
"is_inversed", }))
"underlying_switch_0",
"underlying_switch_1",
"underlying_switch_2",
"underlying_switch_3",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
}
)
)
)
# useless for now # useless for now
# def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: # def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
# """Initialize the thermostat over switch.""" # """Initialize the thermostat over switch."""
# super().__init__(hass, unique_id, name, entry_infos) # super().__init__(hass, unique_id, name, entry_infos)
_is_inversed: bool = None
@property @property
def is_over_switch(self) -> bool: def is_over_switch(self) -> bool:
"""True if the Thermostat is over_switch""" """ True if the Thermostat is over_switch"""
return True return True
@property
def is_inversed(self) -> bool:
"""True if the switch is inversed (for pilot wire and diode)"""
return self._is_inversed is True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
@overrides @overrides
def post_init(self, entry_infos): def post_init(self, entry_infos):
"""Initialize the Thermostat""" """ Initialize the Thermostat"""
super().post_init(entry_infos) super().post_init(entry_infos)
@@ -104,7 +73,6 @@ class ThermostatOverSwitch(BaseThermostat):
) )
) )
self._is_inversed = entry_infos.get(CONF_INVERSE_SWITCH) is True
self._should_relaunch_control_heating = False self._should_relaunch_control_heating = False
@overrides @overrides
@@ -120,34 +88,31 @@ class ThermostatOverSwitch(BaseThermostat):
async_track_state_change_event( async_track_state_change_event(
self.hass, [switch.entity_id], self._async_switch_changed self.hass, [switch.entity_id], self._async_switch_changed
) )
) )
self.hass.create_task(self.async_control_heating()) self.hass.create_task(self.async_control_heating())
@overrides @overrides
def update_custom_attributes(self): def update_custom_attributes(self):
"""Custom attributes""" """ Custom attributes """
super().update_custom_attributes() super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
self._attr_extra_state_attributes["is_inversed"] = self.is_inversed self._attr_extra_state_attributes["underlying_switch_0"] = (
self._attr_extra_state_attributes["underlying_switch_0"] = self._underlyings[ self._underlyings[0].entity_id)
0
].entity_id
self._attr_extra_state_attributes["underlying_switch_1"] = ( self._attr_extra_state_attributes["underlying_switch_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
) )
self._attr_extra_state_attributes["underlying_switch_2"] = ( self._attr_extra_state_attributes["underlying_switch_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
) )
self._attr_extra_state_attributes["underlying_switch_3"] = ( self._attr_extra_state_attributes["underlying_switch_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
) )
self._attr_extra_state_attributes[ self._attr_extra_state_attributes[
"on_percent" "on_percent"
] = self._prop_algorithm.on_percent ] = self._prop_algorithm.on_percent
self._attr_extra_state_attributes["power_percent"] = self.power_percent
self._attr_extra_state_attributes[ self._attr_extra_state_attributes[
"on_time_sec" "on_time_sec"
] = self._prop_algorithm.on_time_sec ] = self._prop_algorithm.on_time_sec
@@ -209,4 +174,3 @@ class ThermostatOverSwitch(BaseThermostat):
if old_state is None: if old_state is None:
self.hass.create_task(self._check_initial_state()) self.hass.create_task(self._check_initial_state())
self.async_write_ha_state() self.async_write_ha_state()
self.update_custom_attributes()

View File

@@ -39,10 +39,7 @@
"valve_entity2_id": "2nd valve number", "valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number", "valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation"
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -59,10 +56,7 @@
"valve_entity2_id": "2nd valve number entity id", "valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id", "valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature"
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -208,10 +202,7 @@
"valve_entity2_id": "2nd valve number", "valve_entity2_id": "2nd valve number",
"valve_entity3_id": "3rd valve number", "valve_entity3_id": "3rd valve number",
"valve_entity4_id": "4th valve number", "valve_entity4_id": "4th valve number",
"auto_regulation_mode": "Self-regulation", "auto_regulation_mode": "Self-regulation"
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -228,10 +219,7 @@
"valve_entity2_id": "2nd valve number entity id", "valve_entity2_id": "2nd valve number entity id",
"valve_entity3_id": "3rd valve number entity id", "valve_entity3_id": "3rd valve number entity id",
"valve_entity4_id": "4th valve number entity id", "valve_entity4_id": "4th valve number entity id",
"auto_regulation_mode": "Auto adjustment of the target temperature", "auto_regulation_mode": "Auto adjustment of the target temperature"
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -348,11 +336,9 @@
}, },
"auto_regulation_mode": { "auto_regulation_mode": {
"options": { "options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong", "auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium", "auto_regulation_medium": "Medium",
"auto_regulation_light": "Light", "auto_regulation_light": "Light",
"auto_regulation_expert": "Expert",
"auto_regulation_none": "No auto-regulation" "auto_regulation_none": "No auto-regulation"
} }
} }

View File

@@ -39,10 +39,7 @@
"valve_entity2_id": "2ème valve number", "valve_entity2_id": "2ème valve number",
"valve_entity3_id": "3ème valve number", "valve_entity3_id": "3ème valve number",
"valve_entity4_id": "4ème valve number", "valve_entity4_id": "4ème valve number",
"auto_regulation_mode": "Auto-régulation", "auto_regulation_mode": "Auto-régulation"
"auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation",
"inverse_switch_command": "Inverser la commande"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -59,10 +56,7 @@
"valve_entity2_id": "Entity id de la 2ème valve", "valve_entity2_id": "Entity id de la 2ème valve",
"valve_entity3_id": "Entity id de la 3ème valve", "valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve", "valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la température cible", "auto_regulation_mode": "Ajustement automatique de la température cible"
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
} }
}, },
"tpi": { "tpi": {
@@ -209,10 +203,7 @@
"valve_entity2_id": "2ème valve", "valve_entity2_id": "2ème valve",
"valve_entity3_id": "3ème valve", "valve_entity3_id": "3ème valve",
"valve_entity4_id": "4ème valve", "valve_entity4_id": "4ème valve",
"auto_regulation_mode": "Auto-regulation", "auto_regulation_mode": "Auto-regulation"
"auto_regulation_dtemp": "Seuil de régulation",
"auto_regulation_periode_min": "Période minimale de régulation",
"inverse_switch_command": "Inverser la commande"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -229,10 +220,7 @@
"valve_entity2_id": "Entity id de la 2ème valve", "valve_entity2_id": "Entity id de la 2ème valve",
"valve_entity3_id": "Entity id de la 3ème valve", "valve_entity3_id": "Entity id de la 3ème valve",
"valve_entity4_id": "Entity id de la 4ème valve", "valve_entity4_id": "Entity id de la 4ème valve",
"auto_regulation_mode": "Ajustement automatique de la consigne", "auto_regulation_mode": "Ajustement automatique de la consigne"
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode"
} }
}, },
"tpi": { "tpi": {
@@ -349,11 +337,9 @@
}, },
"auto_regulation_mode": { "auto_regulation_mode": {
"options": { "options": {
"auto_regulation_slow": "Lente",
"auto_regulation_strong": "Forte", "auto_regulation_strong": "Forte",
"auto_regulation_medium": "Moyenne", "auto_regulation_medium": "Moyenne",
"auto_regulation_light": "Légère", "auto_regulation_light": "Légère",
"auto_regulation_expert": "Expert",
"auto_regulation_none": "Aucune" "auto_regulation_none": "Aucune"
} }
} }

View File

@@ -30,17 +30,16 @@
"heater_entity3_id": "Terzo riscaldatore", "heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore", "heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo", "proportional_function": "Algoritmo",
"climate_entity_id": "Primo termostato", "climate_entity_id": "Termostato sottostante",
"climate_entity2_id": "Secondo termostato", "climate_entity2_id": "Secundo termostato sottostante",
"climate_entity3_id": "Terzo termostato", "climate_entity3_id": "Terzo termostato sottostante",
"climate_entity4_id": "Quarto termostato", "climate_entity4_id": "Quarto termostato sottostante",
"ac_mode": "AC mode ?", "ac_mode": "AC mode ?",
"valve_entity_id": "Prima valvola", "valve_entity_id": "Primo valvola numero",
"valve_entity2_id": "Seconda valvolao", "valve_entity2_id": "Secondo valvola numero",
"valve_entity3_id": "Terza valvola", "valve_entity3_id": "Terzo valvola numero",
"valve_entity4_id": "Quarta valvola", "valve_entity4_id": "Quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione", "auto_regulation_mode": "Autoregolamentazione"
"inverse_switch_command": "Comando inverso"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore", "heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -48,17 +47,16 @@
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del primo termostato", "climate_entity_id": "Entity id del termostato sottostante",
"climate_entity2_id": "Entity id del secondo termostato", "climate_entity2_id": "Entity id del secundo termostato sottostante",
"climate_entity3_id": "Entity id del terzo termostato", "climate_entity3_id": "Entity id del terzo termostato sottostante",
"climate_entity4_id": "Entity id del quarto termostato", "climate_entity4_id": "Entity id del quarto termostato sottostante",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?", "ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
"valve_entity_id": "Entity id della prima valvola", "valve_entity_id": "Entity id del primo valvola numero",
"valve_entity2_id": "Entity id della seconda valvola", "valve_entity2_id": "Entity id del secondo valvola numero",
"valve_entity3_id": "Entity id della terza valvola", "valve_entity3_id": "Entity id del terzo valvola numero",
"valve_entity4_id": "Entity id della quarta valvola", "valve_entity4_id": "Entity id del quarto valvola numero",
"auto_regulation_mode": "Regolazione automatica della temperatura target", "auto_regulation_mode": "Regolazione automatica della temperatura target"
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
} }
}, },
"tpi": { "tpi": {
@@ -188,17 +186,16 @@
"heater_entity3_id": "Terzo riscaldatore", "heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore", "heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo", "proportional_function": "Algoritmo",
"climate_entity_id": "Primo termostato", "climate_entity_id": "Termostato sottostante",
"climate_entity2_id": "Secondo termostato", "climate_entity2_id": "Secundo termostato sottostante",
"climate_entity3_id": "Terzo termostato", "climate_entity3_id": "Terzo termostato sottostante",
"climate_entity4_id": "Quarto termostato", "climate_entity4_id": "Quarto termostato sottostante",
"ac_mode": "AC mode ?", "ac_mode": "AC mode ?",
"valve_entity_id": "Prima valvola", "valve_entity_id": "Primo valvola numero",
"valve_entity2_id": "Seconda valvola", "valve_entity2_id": "Secondo valvola numero",
"valve_entity3_id": "Terza valvola", "valve_entity3_id": "Terzo valvola numero",
"valve_entity4_id": "Quarta valvola", "valve_entity4_id": "Quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione", "auto_regulation_mode": "Autoregolamentazione"
"inverse_switch_command": "Comando inverso"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore", "heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -206,17 +203,16 @@
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del primo termostato", "climate_entity_id": "Entity id del termostato sottostante",
"climate_entity2_id": "Entity id del secondo termostato", "climate_entity2_id": "Entity id del secundo termostato sottostante",
"climate_entity3_id": "Entity id del terzo termostato", "climate_entity3_id": "Entity id del terzo termostato sottostante",
"climate_entity4_id": "Entity id del quarto termostato", "climate_entity4_id": "Entity id del quarto termostato sottostante",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?", "ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?",
"valve_entity_id": "Entity id della prima valvola", "valve_entity_id": "Entity id del primo valvola numero",
"valve_entity2_id": "Entity id della seconda valvola", "valve_entity2_id": "Entity id del secondo valvola numero",
"valve_entity3_id": "Entity id della terza valvola", "valve_entity3_id": "Entity id del terzo valvola numero",
"valve_entity4_id": "Entity id della quarta valvola", "valve_entity4_id": "Entity id del quarto valvola numero",
"auto_regulation_mode": "Autoregolamentazione", "auto_regulation_mode": "Autoregolamentazione"
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo"
} }
}, },
"tpi": { "tpi": {
@@ -252,9 +248,9 @@
"data_description": { "data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra", "window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione", "window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato", "window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0 - Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato", "window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 minuti. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato" "window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
} }
}, },
"motion": { "motion": {
@@ -320,17 +316,15 @@
"thermostat_type": { "thermostat_type": {
"options": { "options": {
"thermostat_over_switch": "Termostato su un interruttore", "thermostat_over_switch": "Termostato su un interruttore",
"thermostat_over_climate": "Termostato su un climatizzatore", "thermostat_over_climate": "Termostato sopra un altro termostato",
"thermostat_over_valve": "Termostato su una valvola" "thermostat_over_valve": "Thermostato su una valvola"
} }
}, },
"auto_regulation_mode": { "auto_regulation_mode": {
"options": { "options": {
"auto_regulation_slow": "Lento",
"auto_regulation_strong": "Forte", "auto_regulation_strong": "Forte",
"auto_regulation_medium": "Media", "auto_regulation_medium": "Media",
"auto_regulation_light": "Leggera", "auto_regulation_light": "Leggera",
"auto_regulation_expert": "Esperto",
"auto_regulation_none": "Nessuna autoregolamentazione" "auto_regulation_none": "Nessuna autoregolamentazione"
} }
} }

View File

@@ -38,11 +38,7 @@
"valve_entity_id": "1. ventil číslo", "valve_entity_id": "1. ventil číslo",
"valve_entity2_id": "2. ventil číslo", "valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo", "valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo", "valve_entity4_id": "4. ventil číslo"
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "ID entity povinného ohrievača", "heater_entity_id": "ID entity povinného ohrievača",
@@ -58,11 +54,7 @@
"valve_entity_id": "1. ventil číslo entity id", "valve_entity_id": "1. ventil číslo entity id",
"valve_entity2_id": "2. ventil číslo entity id", "valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id", "valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id", "valve_entity4_id": "4. ventil číslo entity id"
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -207,11 +199,7 @@
"valve_entity_id": "1. ventil číslo", "valve_entity_id": "1. ventil číslo",
"valve_entity2_id": "2. ventil číslo", "valve_entity2_id": "2. ventil číslo",
"valve_entity3_id": "3. ventil číslo", "valve_entity3_id": "3. ventil číslo",
"valve_entity4_id": "4. ventil číslo", "valve_entity4_id": "4. ventil číslo"
"auto_regulation_mode": "Self-regulation",
"auto_regulation_dtemp": "Regulation threshold",
"auto_regulation_periode_min": "Regulation minimal period",
"inverse_switch_command": "Inverse switch command"
}, },
"data_description": { "data_description": {
"heater_entity_id": "ID entity povinného ohrievača", "heater_entity_id": "ID entity povinného ohrievača",
@@ -227,11 +215,7 @@
"valve_entity_id": "1. ventil číslo entity id", "valve_entity_id": "1. ventil číslo entity id",
"valve_entity2_id": "2. ventil číslo entity id", "valve_entity2_id": "2. ventil číslo entity id",
"valve_entity3_id": "3. ventil číslo entity id", "valve_entity3_id": "3. ventil číslo entity id",
"valve_entity4_id": "4. ventil číslo entity id", "valve_entity4_id": "4. ventil číslo entity id"
"auto_regulation_mode": "Auto adjustment of the target temperature",
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be send",
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command"
} }
}, },
"tpi": { "tpi": {
@@ -345,16 +329,6 @@
"thermostat_over_climate": "Termostat nad iným termostatom", "thermostat_over_climate": "Termostat nad iným termostatom",
"thermostat_over_valve": "Thermostat over a valve" "thermostat_over_valve": "Thermostat over a valve"
} }
},
"auto_regulation_mode": {
"options": {
"auto_regulation_slow": "Slow",
"auto_regulation_strong": "Strong",
"auto_regulation_medium": "Medium",
"auto_regulation_light": "Light",
"auto_regulation_expert": "Expert",
"auto_regulation_none": "No auto-regulation"
}
} }
}, },
"entity": { "entity": {

View File

@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.exceptions import ServiceNotFound from homeassistant.exceptions import ServiceNotFound
from homeassistant.core import HomeAssistant, CALLBACK_TYPE from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
@@ -104,27 +104,37 @@ class UnderlyingEntity:
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
return None return None
async def turn_off(self):
"""Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
# This may fails if called after shutdown
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_OFF,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_on(self):
"""Turn heater toggleable device on."""
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_ON,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def set_temperature(self, temperature, max_temp, min_temp): async def set_temperature(self, temperature, max_temp, min_temp):
"""Set the target temperature""" """Set the target temperature"""
return return
# This should be the correct way to handle turn_off and turn_on but this breaks the unit test
# will an not understandable error: TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self):
""" Turn off the underlying equipement.
Need to be overriden"""
return NotImplementedError
async def turn_on(self):
""" Turn off the underlying equipement.
Need to be overriden"""
return NotImplementedError
@property
def is_inversed(self):
""" Tells if the switch command should be inversed"""
return False
def remove_entity(self): def remove_entity(self):
"""Remove the underlying entity""" """Remove the underlying entity"""
return return
@@ -202,13 +212,6 @@ class UnderlyingSwitch(UnderlyingEntity):
"""The initial delay for this class""" """The initial delay for this class"""
return self._initial_delay_sec return self._initial_delay_sec
@overrides
@property
def is_inversed(self):
""" Tells if the switch command should be inversed"""
return self._thermostat.is_inversed
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool: async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change""" """Set the HVACmode. Returns true if something have change"""
@@ -226,41 +229,7 @@ class UnderlyingSwitch(UnderlyingEntity):
@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."""
real_state = self._hass.states.is_state(self._entity_id, STATE_ON) return self._hass.states.is_state(self._entity_id, STATE_ON)
return (self.is_inversed and not real_state) or (not self.is_inversed and real_state)
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self):
"""Turn heater toggleable device off."""
_LOGGER.debug("%s - Stopping underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_OFF if not self.is_inversed else SERVICE_TURN_ON
domain = self._entity_id.split('.')[0]
# This may fails if called after shutdown
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
domain,
command,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
async def turn_on(self):
"""Turn heater toggleable device on."""
_LOGGER.debug("%s - Starting underlying entity %s", self, self._entity_id)
command = SERVICE_TURN_ON if not self.is_inversed else SERVICE_TURN_OFF
domain = self._entity_id.split('.')[0]
try:
data = {ATTR_ENTITY_ID: self._entity_id}
await self._hass.services.async_call(
domain,
command,
data,
)
except ServiceNotFound as err:
_LOGGER.error(err)
@overrides @overrides
async def start_cycle( async def start_cycle(
@@ -411,7 +380,6 @@ class UnderlyingSwitch(UnderlyingEntity):
# increment energy at the end of the cycle # increment energy at the end of the cycle
self._thermostat.incremente_energy() self._thermostat.incremente_energy()
@overrides
def remove_entity(self): def remove_entity(self):
"""Remove the entity after stopping its cycle""" """Remove the entity after stopping its cycle"""
self._cancel_cycle() self._cancel_cycle()

View File

@@ -1,70 +0,0 @@
""" The API of Versatile Thermostat"""
import logging
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from .const import (
DOMAIN,
CONF_AUTO_REGULATION_EXPERT,
)
VTHERM_API_NAME = "vtherm_api"
_LOGGER = logging.getLogger(__name__)
class VersatileThermostatAPI(dict):
"""The VersatileThermostatAPI"""
_hass: HomeAssistant
# _entries: Dict(str, ConfigEntry)
@classmethod
def get_vtherm_api(cls, hass: HomeAssistant):
"""Get the eventual VTherm API class instance"""
ret = hass.data.get(DOMAIN).get(VTHERM_API_NAME)
if ret is None:
ret = VersatileThermostatAPI(hass)
hass.data[DOMAIN][VTHERM_API_NAME] = ret
return ret
def __init__(self, hass: HomeAssistant) -> None:
_LOGGER.debug("building a VersatileThermostatAPI")
super().__init__()
self._hass = hass
self._expert_params = None
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
_LOGGER.debug("Add the entry %s", entry.entry_id)
# self._entries[entry.entry_id] = entry
# Add the entry in hass.data
self._hass.data[DOMAIN][entry.entry_id] = entry
def remove_entry(self, entry: ConfigEntry):
"""Remove an entry"""
_LOGGER.debug("Remove the entry %s", entry.entry_id)
# self._entries.pop(entry.entry_id)
self._hass.data[DOMAIN].pop(entry.entry_id)
# If not more entries are preset, remove the API
if len(self) == 0:
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
self._hass.data.pop(DOMAIN)
def set_global_config(self, config):
"""Read the global configuration from configuration.yaml file"""
_LOGGER.info("Read global config from configuration.yaml")
self._expert_params = config.get(CONF_AUTO_REGULATION_EXPERT)
if self._expert_params:
_LOGGER.debug("We have found expert params %s", self._expert_params)
@property
def self_regulation_expert(self):
"""Get the self regulation params"""
return self._expert_params
@property
def hass(self):
"""Get the HomeAssistant object"""
return self._hass

View File

@@ -3,5 +3,5 @@
"content_in_root": false, "content_in_root": false,
"render_readme": true, "render_readme": true,
"hide_default_branch": false, "hide_default_branch": false,
"homeassistant": "2023.11.2" "homeassistant": "2023.10.3"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -1,2 +1,2 @@
homeassistant==2023.11.2 homeassistant==2023.10.3
ffmpeg ffmpeg

View File

@@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF, ATTR_TEMPERATURE
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.util import dt as dt_util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateEntity, ClimateEntity,
@@ -24,7 +25,6 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import from custom_components.versatile_thermostat.const import * # pylint: disable=wildcard-import, unused-wildcard-import
from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import from custom_components.versatile_thermostat.underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
from custom_components.versatile_thermostat.commons import get_tz, NowClass # pylint: disable=unused-import
from .const import ( # pylint: disable=unused-import from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_SWITCH_USER_CONFIG, MOCK_TH_OVER_SWITCH_USER_CONFIG,
@@ -478,6 +478,13 @@ async def send_presence_change_event(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
return ret return ret
def get_tz(hass: HomeAssistant):
"""Get the current timezone"""
return dt_util.get_time_zone(hass.config.time_zone)
async def send_climate_change_event( async def send_climate_change_event(
entity: BaseThermostat, entity: BaseThermostat,
new_hvac_mode: HVACMode, new_hvac_mode: HVACMode,

View File

@@ -51,11 +51,8 @@ from custom_components.versatile_thermostat.const import (
PRESET_AWAY_SUFFIX, PRESET_AWAY_SUFFIX,
CONF_CLIMATE, CONF_CLIMATE,
CONF_AUTO_REGULATION_MODE, CONF_AUTO_REGULATION_MODE,
CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_MEDIUM,
CONF_AUTO_REGULATION_NONE, CONF_AUTO_REGULATION_NONE,
CONF_AUTO_REGULATION_DTEMP,
CONF_AUTO_REGULATION_PERIOD_MIN,
CONF_INVERSE_SWITCH
) )
MOCK_TH_OVER_SWITCH_USER_CONFIG = { MOCK_TH_OVER_SWITCH_USER_CONFIG = {
CONF_NAME: "TheOverSwitchMockName", CONF_NAME: "TheOverSwitchMockName",
@@ -102,15 +99,13 @@ MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
MOCK_TH_OVER_SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_switch", CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False
CONF_INVERSE_SWITCH: False
} }
MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = { MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_air_conditioner", CONF_HEATER: "switch.mock_air_conditioner",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: True, CONF_AC_MODE: True,
CONF_INVERSE_SWITCH: False
} }
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = { MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
@@ -120,7 +115,6 @@ MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER_4: "switch.mock_4switch3", CONF_HEATER_4: "switch.mock_4switch3",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_INVERSE_SWITCH: False
} }
MOCK_TH_OVER_SWITCH_TPI_CONFIG = { MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
@@ -131,9 +125,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False, CONF_AC_MODE: False,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM
CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 2
} }
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
@@ -145,9 +137,7 @@ MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG = {
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: True, CONF_AC_MODE: True,
CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_STRONG, CONF_AUTO_REGULATION_MODE: CONF_AUTO_REGULATION_MEDIUM
CONF_AUTO_REGULATION_DTEMP: 0.5,
CONF_AUTO_REGULATION_PERIOD_MIN: 1
} }
MOCK_PRESETS_CONFIG = { MOCK_PRESETS_CONFIG = {

View File

@@ -1,7 +1,7 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long # pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
""" Test the normal start of a Thermostat """ """ Test the normal start of a Thermostat """
from unittest.mock import patch # , call from unittest.mock import patch #, call
from datetime import datetime, timedelta from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -14,18 +14,13 @@ from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DO
from pytest_homeassistant_custom_component.common import MockConfigEntry from pytest_homeassistant_custom_component.common import MockConfigEntry
# from custom_components.versatile_thermostat.base_thermostat import BaseThermostat # from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.thermostat_climate import ( from custom_components.versatile_thermostat.thermostat_climate import ThermostatOverClimate
ThermostatOverClimate,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation( async def test_over_climate_regulation(hass: HomeAssistant, skip_hass_states_is_state):
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the regulation of an over climate thermostat""" """Test the regulation of an over climate thermostat"""
entry = MockConfigEntry( entry = MockConfigEntry(
@@ -42,12 +37,8 @@ async def test_over_climate_regulation(
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}) fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
# Creates the regulated VTherm over climate # Creates the regulated VTherm over climate
# change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=10)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
return_value=event_timestamp,
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
@@ -63,7 +54,7 @@ async def test_over_climate_regulation(
if entity.entity_id == entity_id: if entity.entity_id == entity_id:
return entity return entity
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname") entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)
@@ -82,59 +73,45 @@ async def test_over_climate_regulation(
] ]
assert entity.preset_mode is PRESET_NONE assert entity.preset_mode is PRESET_NONE
# Activate the heating by changing HVACMode and temperature # Activate the heating by changing HVACMode and temperature
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
):
# Select a hvacmode, presence and preset # Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF assert entity.hvac_action == HVACAction.OFF
assert entity.regulated_target_temp == entity.min_temp # change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=10)
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp) await send_ext_temperature_change_event(entity, 10, event_timestamp)
# set manual target temp (at now - 7) -> the regulation should occurs
event_timestamp = now - timedelta(minutes=7)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=18)
fake_underlying_climate.set_hvac_action( # set manual target temp
HVACAction.HEATING await entity.async_set_temperature(temperature=18)
) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be greater fake_underlying_climate.set_hvac_action(HVACAction.HEATING) # simulate under heating
assert entity.regulated_target_temp > entity.target_temperature assert entity.hvac_action == HVACAction.HEATING
# In medium we could go up to +3 degre assert entity.preset_mode == PRESET_NONE # Manual mode
# normally the calcul gives 18 + 2.2 but we round the result to the nearest 0.5 which is 2.0
assert entity.regulated_target_temp == 18 + 1.5 # the regulated temperature should be greater
assert entity.hvac_action == HVACAction.HEATING assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 18+2.9 # In medium we could go up to +3 degre
assert entity.hvac_action == HVACAction.HEATING
# change temperature so that the regulated temperature should slow down # change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=5) event_timestamp = now - timedelta(minutes=9)
with patch( await send_temperature_change_event(entity, 19, event_timestamp)
"custom_components.versatile_thermostat.commons.NowClass.get_now", await send_ext_temperature_change_event(entity, 18, event_timestamp)
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 23, event_timestamp)
await send_ext_temperature_change_event(entity, 19, event_timestamp)
# the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature
assert (
entity.regulated_target_temp == 18 - 2
) # normally 0.6 but round_to_nearest gives 0.5
# the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature
assert entity.regulated_target_temp == 18-0.1
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_ac_mode( async def test_over_climate_regulation_ac_mode(hass: HomeAssistant, skip_hass_states_is_state):
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the regulation of an over climate thermostat""" """Test the regulation of an over climate thermostat"""
entry = MockConfigEntry( entry = MockConfigEntry(
@@ -151,12 +128,8 @@ async def test_over_climate_regulation_ac_mode(
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}) fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
# Creates the regulated VTherm over climate # Creates the regulated VTherm over climate
# change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=10)
with patch( with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now", "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
return_value=event_timestamp,
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate, return_value=fake_underlying_climate,
@@ -172,7 +145,7 @@ async def test_over_climate_regulation_ac_mode(
if entity.entity_id == entity_id: if entity.entity_id == entity_id:
return entity return entity
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname") entity:ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity assert entity
assert isinstance(entity, ThermostatOverClimate) assert isinstance(entity, ThermostatOverClimate)
@@ -191,184 +164,47 @@ async def test_over_climate_regulation_ac_mode(
] ]
assert entity.preset_mode is PRESET_NONE assert entity.preset_mode is PRESET_NONE
# Activate the heating by changing HVACMode and temperature # Activate the heating by changing HVACMode and temperature
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
):
# Select a hvacmode, presence and preset # Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF assert entity.hvac_action == HVACAction.OFF
# change temperature so that the heating will start # change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=10)
await send_temperature_change_event(entity, 30, event_timestamp) await send_temperature_change_event(entity, 30, event_timestamp)
await send_ext_temperature_change_event(entity, 35, event_timestamp) await send_ext_temperature_change_event(entity, 35, event_timestamp)
# set manual target temp # set manual target temp
event_timestamp = now - timedelta(minutes=7) await entity.async_set_temperature(temperature=25)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=25)
fake_underlying_climate.set_hvac_action( fake_underlying_climate.set_hvac_action(HVACAction.COOLING) # simulate under heating
HVACAction.COOLING assert entity.hvac_action == HVACAction.COOLING
) # simulate under heating assert entity.preset_mode == PRESET_NONE # Manual mode
assert entity.hvac_action == HVACAction.COOLING
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be lower # the regulated temperature should be lower
assert entity.regulated_target_temp < entity.target_temperature assert entity.regulated_target_temp < entity.target_temperature
assert ( assert entity.regulated_target_temp == 25-3 # In medium we could go up to -3 degre
entity.regulated_target_temp == 25 - 2.5 assert entity.hvac_action == HVACAction.COOLING
) # In medium we could go up to -3 degre
assert entity.hvac_action == HVACAction.COOLING
# change temperature so that the regulated temperature should slow down # change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=5) event_timestamp = now - timedelta(minutes=9)
with patch( await send_temperature_change_event(entity, 26, event_timestamp)
"custom_components.versatile_thermostat.commons.NowClass.get_now", await send_ext_temperature_change_event(entity, 35, event_timestamp)
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 26, event_timestamp)
await send_ext_temperature_change_event(entity, 35, event_timestamp)
# the regulated temperature should be under # the regulated temperature should be under
assert entity.regulated_target_temp < entity.target_temperature assert entity.regulated_target_temp < entity.target_temperature
assert ( assert entity.regulated_target_temp == 25-2.7
entity.regulated_target_temp == 25 - 1
) # +2.3 without round_to_nearest
# change temperature so that the regulated temperature should slow down # change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=9)
with patch( await send_temperature_change_event(entity, 20, event_timestamp)
"custom_components.versatile_thermostat.commons.NowClass.get_now", await send_ext_temperature_change_event(entity, 30, event_timestamp)
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 25, event_timestamp)
# the regulated temperature should be greater # the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature assert entity.regulated_target_temp > entity.target_temperature
assert ( assert entity.regulated_target_temp == 25+1.8
entity.regulated_target_temp == 25 + 3
) # +0.4 without round_to_nearest
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_limitations(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the limitations of the regulation of an over climate thermostat:
1. test the period_min parameter: do not send regulation event too frequently
2. test the dtemp parameter: do not send regulation event if offset temp is lower than dtemp
"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# This is include a medium regulation, dtemp=0.5, period_min=2
data=PARTIAL_CLIMATE_CONFIG,
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
# Creates the regulated VTherm over climate at t-20
# change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=20)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.is_regulated is True
# Activate the heating by changing HVACMode and temperature
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
# it is cold today
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# set manual target temp (at now - 19) -> the regulation should be ignored because too early
event_timestamp = now - timedelta(minutes=19)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=18)
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under heating
assert entity.hvac_action == HVACAction.HEATING
# the regulated temperature will change because when we set temp manually it is forced
assert entity.regulated_target_temp == 19.5
# set manual target temp (at now - 18) -> the regulation should be taken into account
event_timestamp = now - timedelta(minutes=18)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=17)
assert entity.regulated_target_temp > entity.target_temperature
assert (
entity.regulated_target_temp == 18 + 0
) # In strong we could go up to +3 degre. 0.7 without round_to_nearest
old_regulated_temp = entity.regulated_target_temp
# change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=15)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 16, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be under
assert entity.regulated_target_temp <= old_regulated_temp
# change temperature so that dtemp > 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=12)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 16, event_timestamp)
await send_ext_temperature_change_event(entity, 12, event_timestamp)
# the regulated should have been done
assert entity.regulated_target_temp != old_regulated_temp
assert entity.regulated_target_temp >= entity.target_temperature
assert (
entity.regulated_target_temp == 17 + 0.5
) # 0.7 without round_to_nearest

View File

@@ -241,7 +241,7 @@ async def test_window_binary_sensors(
await entity.async_set_preset_mode(PRESET_COMFORT) await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now) await send_temperature_change_event(entity, 15, now)
assert entity.window_state is STATE_OFF assert entity.window_state is None
await window_binary_sensor.async_my_climate_changed() await window_binary_sensor.async_my_climate_changed()
assert window_binary_sensor.state is STATE_OFF assert window_binary_sensor.state is STATE_OFF

View File

@@ -243,7 +243,7 @@ async def test_bug_66(
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
# Open the window and let the thermostat shut down # Open the window and let the thermostat shut down
with patch( with patch(

View File

@@ -1,4 +1,3 @@
# pylint: disable=unused-argument, line-too-long
""" Test the Versatile Thermostat config flow """ """ Test the Versatile Thermostat config flow """
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
@@ -18,7 +17,7 @@ async def test_show_form(hass: HomeAssistant) -> None:
# Init the API # Init the API
# hass.data["custom_components"] = None # hass.data["custom_components"] = None
# loader.async_get_custom_components(hass) # loader.async_get_custom_components(hass)
# BaseThermostatAPI(hass) # VersatileThermostatAPI(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
@@ -30,11 +29,7 @@ async def test_show_form(hass: HomeAssistant) -> None:
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
# Disable this test which don't work anymore (kill the pytest !) async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
@pytest.mark.skip
async def test_user_config_flow_over_switch(
hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_switch features""" """Test the config flow with all thermostat_over_switch features"""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
@@ -133,9 +128,7 @@ async def test_user_config_flow_over_switch(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_climate( async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get): # pylint: disable=unused-argument
hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument
"""Test the config flow with all thermostat_over_climate features and no additional features""" """Test the config flow with all thermostat_over_climate features and no additional features"""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
@@ -191,9 +184,7 @@ async def test_user_config_flow_over_climate(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_window_auto_ok( async def test_user_config_flow_window_auto_ok(
hass: HomeAssistant, hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
skip_hass_states_get,
skip_control_heating, # pylint: disable=unused-argument
): ):
"""Test the config flow with only window auto feature""" """Test the config flow with only window auto feature"""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@@ -362,9 +353,7 @@ async def test_user_config_flow_window_auto_ko(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_4_switches( async def test_user_config_flow_over_4_switches(
hass: HomeAssistant, hass: HomeAssistant, skip_hass_states_get, skip_control_heating # pylint: disable=unused-argument
skip_hass_states_get,
skip_control_heating, # pylint: disable=unused-argument
): ):
"""Test the config flow with 4 switchs thermostat_over_switch features""" """Test the config flow with 4 switchs thermostat_over_switch features"""
@@ -438,11 +427,14 @@ async def test_user_config_flow_over_4_switches(
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result[ assert (
"data" result["data"]
] == SOURCE_CONFIG | TYPE_CONFIG | MOCK_TH_OVER_SWITCH_TPI_CONFIG | MOCK_PRESETS_CONFIG | MOCK_ADVANCED_CONFIG | { == SOURCE_CONFIG
CONF_INVERSE_SWITCH: False | TYPE_CONFIG
} | MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
assert result["result"] assert result["result"]
assert result["result"].domain == DOMAIN assert result["result"].domain == DOMAIN
assert result["result"].version == 1 assert result["result"].version == 1

View File

@@ -1,54 +0,0 @@
# pylint: disable=line-too-long
""" Tests de EMA calculation"""
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant
from custom_components.versatile_thermostat.ema import ExponentialMovingAverage
from .commons import get_tz
def test_ema_basics(hass: HomeAssistant):
"""Test the EMA calculation with basic features"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
the_ema = ExponentialMovingAverage(
"test",
# 5 minutes
300,
# Needed for time calculation
get_tz(hass),
1,
)
assert the_ema
current_timestamp = now
# First initialization
assert the_ema.calculate_ema(20, current_timestamp) == 20
current_timestamp = current_timestamp + timedelta(minutes=1)
# One minute later, same temperature. EMA temperature should not have change
assert the_ema.calculate_ema(20, current_timestamp) == 20
# Too short measurement should be ignored
assert the_ema.calculate_ema(2000, current_timestamp) == 20
current_timestamp = current_timestamp + timedelta(seconds=4)
assert the_ema.calculate_ema(20, current_timestamp) == 20
# a new normal measurement 5 minutes later
current_timestamp = current_timestamp + timedelta(minutes=5)
ema = the_ema.calculate_ema(25, current_timestamp)
assert ema > 20
assert ema == 22.5
# a big change in a short time does have a limited effect
current_timestamp = current_timestamp + timedelta(seconds=5)
ema = the_ema.calculate_ema(30, current_timestamp)
assert ema > 22.5
assert ema < 23
assert ema == 22.6

View File

@@ -1,124 +0,0 @@
# pylint: disable=unused-argument, line-too-long, protected-access
""" Test the Window management """
import asyncio
import logging
from unittest.mock import patch, call
from datetime import datetime, timedelta
from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_inverted_switch(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window auto management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 0.1,
CONF_WINDOW_AUTO_MAX_DURATION: 0, # Should be 0 for test
CONF_INVERSE_SWITCH: True
},
)
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=True # switch is On
):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.is_inversed
tz = get_tz(hass) # pylint: disable=invalid-name
now = datetime.now(tz)
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 21
assert entity.is_device_active is False
assert mock_service_call.call_count == 0
# 1. Make the temperature down to activate the switch
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=True # switch is Off
):
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 19, event_timestamp)
# The heater turns on
assert entity.hvac_mode is HVACMode.HEAT
# not updated cause mocked assert entity.is_device_active is True
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls([
call.async_call('switch', SERVICE_TURN_OFF, {'entity_id': 'switch.mock_switch'}),
])
# 2. Make the temperature up to deactivate the switch
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.is_state", return_value=False # switch is On -> it should turned off
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 25, event_timestamp)
# The heater turns on
assert entity.hvac_mode is HVACMode.HEAT
# not updated cause mocked assert entity.is_device_active is False
# there is no change because the cycle is currenlty running.
# we should simulate the end of the cycle to see oif underlying switch turns on
await entity._underlyings[0].turn_off()
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls([
call.async_call('switch', SERVICE_TURN_ON, {'entity_id': 'switch.mock_switch'}),
])
# Clean the entity
entity.remove_thermostat()

View File

@@ -1,13 +1,11 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
""" Test the Window management """ """ Test the Window management """
import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from unittest.mock import patch from unittest.mock import patch, call, PropertyMock
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -56,7 +54,7 @@ async def test_movement_management_time_not_enough(
}, },
) )
entity: BaseThermostat = await create_thermostat( entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -253,7 +251,7 @@ async def test_movement_management_time_enough_and_presence(
}, },
) )
entity: BaseThermostat = await create_thermostat( entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -385,7 +383,7 @@ async def test_movement_management_time_enoughand_not_presence(
}, },
) )
entity: BaseThermostat = await create_thermostat( entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -519,7 +517,7 @@ async def test_movement_management_with_stop_during_condition(
}, },
) )
entity: BaseThermostat = await create_thermostat( entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -599,3 +597,4 @@ async def test_movement_management_with_stop_during_condition(
assert entity.target_temperature == 19 # Boost assert entity.target_temperature == 19 # Boost
assert entity.motion_state is "on" # switch to movement on assert entity.motion_state is "on" # switch to movement on
assert entity.presence_state is "off" # Non change assert entity.presence_state is "off" # Non change

View File

@@ -1,12 +1,9 @@
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable
""" Test the Multiple switch management """ """ Test the Multiple switch management """
import asyncio import asyncio
from unittest.mock import patch, call, ANY from unittest.mock import patch, call, ANY
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -53,7 +50,7 @@ async def test_one_switch_cycle(
}, },
) )
entity: BaseThermostat = await create_thermostat( entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname" hass, entry, "climate.theover4switchmockname"
) )
assert entity assert entity
@@ -69,7 +66,7 @@ async def test_one_switch_cycle(
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
@@ -163,9 +160,7 @@ async def test_one_switch_cycle(
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True # assert entity.underlying_entity(0)._should_relaunch_control_heating is True
# Simulate the relaunch # Simulate the relaunch
await entity.underlying_entity( await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
0
)._turn_on_later( # pylint: disable=protected-access
None None
) )
# wait restart # wait restart
@@ -186,9 +181,7 @@ async def test_one_switch_cycle(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True, return_value=True,
) as mock_device_active: ) as mock_device_active:
await entity.underlying_entity( await entity.underlying_entity(0)._turn_off_later( # pylint: disable=protected-access
0
)._turn_off_later( # pylint: disable=protected-access
None None
) )
@@ -211,9 +204,7 @@ async def test_one_switch_cycle(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True, return_value=True,
) as mock_device_active: ) as mock_device_active:
await entity.underlying_entity( await entity.underlying_entity(0)._turn_on_later( # pylint: disable=protected-access
0
)._turn_on_later( # pylint: disable=protected-access
None None
) )
@@ -269,7 +260,7 @@ async def test_multiple_switchs(
}, },
) )
entity: BaseThermostat = await create_thermostat( entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname" hass, entry, "climate.theover4switchmockname"
) )
assert entity assert entity
@@ -288,7 +279,7 @@ async def test_multiple_switchs(
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
@@ -386,7 +377,6 @@ async def test_multiple_climates(
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17, "eco_temp": 17,
@@ -406,7 +396,7 @@ async def test_multiple_climates(
}, },
) )
entity: BaseThermostat = await create_thermostat( entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname" hass, entry, "climate.theover4climatemockname"
) )
assert entity assert entity
@@ -425,7 +415,7 @@ async def test_multiple_climates(
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
@@ -450,7 +440,7 @@ async def test_multiple_climates(
assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_mode is HVACMode.OFF
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
@@ -487,7 +477,6 @@ async def test_multiple_climates_underlying_changes(
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17, "eco_temp": 17,
@@ -507,7 +496,7 @@ async def test_multiple_climates_underlying_changes(
}, },
) )
entity: BaseThermostat = await create_thermostat( entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname" hass, entry, "climate.theover4climatemockname"
) )
assert entity assert entity
@@ -526,7 +515,7 @@ async def test_multiple_climates_underlying_changes(
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp) await send_temperature_change_event(entity, 15, event_timestamp)
@@ -599,139 +588,3 @@ async def test_multiple_climates_underlying_changes(
assert entity.hvac_mode == HVACMode.HEAT assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.IDLE assert entity.hvac_action == HVACAction.IDLE
assert entity.is_device_active is False # pylint: disable=protected-access assert entity.is_device_active is False # pylint: disable=protected-access
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switch_power_management(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4SwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
assert entity.nb_underlying_entities == 4
tpi_algo = entity._prop_algorithm
assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is None
assert entity.target_temperature == 19
# 1. Send power mesurement
await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
assert await entity.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
assert entity.overpowering_state is False
# 2. Send power max mesurement too low and HVACMode is on
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
# 100 of the device / 4 -> 25, current power 50 so max is 75
await send_max_power_change_event(entity, 74, datetime.now())
assert await entity.check_overpowering() is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
assert entity.overpowering_state is True
assert entity.target_temperature == 12
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
call.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": 50,
"device_power": 100,
"current_power_max": 74,
"current_power_consumption": 25.0,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 4 # The fourth are shutdown
# 3. change PRESET
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
# No change
assert entity.overpowering_state is True
# 4. Send hugh power max mesurement to release overpowering
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
await send_max_power_change_event(entity, 150, datetime.now())
assert await entity.check_overpowering() is False
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_ECO
assert entity.overpowering_state is False
assert entity.target_temperature == 17
assert (
mock_heater_on.call_count == 0
) # The fourth are not restarted because temperature is enought
assert mock_heater_off.call_count == 0

View File

@@ -3,19 +3,10 @@
from custom_components.versatile_thermostat.pi_algorithm import PITemperatureRegulator from custom_components.versatile_thermostat.pi_algorithm import PITemperatureRegulator
def test_pi_algorithm_basics(): def test_pi_algorithm_basics():
"""Test the PI algorithm""" """ Test the PI algorithm """
the_algo = PITemperatureRegulator( the_algo = PITemperatureRegulator(target_temp=20, kp=0.2, ki=0.05, k_ext=0.1, offset_max=2, stabilization_threshold=0.1, accumulated_error_threshold=20)
target_temp=20,
kp=0.2,
ki=0.05,
k_ext=0.1,
offset_max=2,
stabilization_threshold=0.1,
accumulated_error_threshold=20,
)
assert the_algo assert the_algo
@@ -25,115 +16,95 @@ def test_pi_algorithm_basics():
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the accumulator threshold effect and offset_max # Test the accumulator threshold effect and offset_max
assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2 assert the_algo.calculate_regulated_temperature(10, 10) == 22 # +2
assert the_algo.calculate_regulated_temperature(10, 10) == 22 assert the_algo.calculate_regulated_temperature(10, 10) == 22
assert the_algo.calculate_regulated_temperature(10, 10) == 22 assert the_algo.calculate_regulated_temperature(10, 10) == 22
# Will keep infinitly 22.0 # Will keep infinitly 22.0
# to reset the accumulated error # to reset the accumulated erro
the_algo.reset_accumulated_error() the_algo.set_target_temp(20)
assert the_algo.calculate_regulated_temperature(18, 10) == 21.3 # +1.5 assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.4 # +1.6 assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.4 # +1.6 assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.5 # +1.7 assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(19, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5 assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.8 assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.7 assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 # +0.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.4 # +0.7 assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 12) == 21.2 # +0.8 assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(20, 15) == 20.9 # +0.5 assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
assert the_algo.calculate_regulated_temperature(20, 18) == 20.6 # +0.2 assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
assert the_algo.calculate_regulated_temperature(20, 20) == 20.4 # = assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
def test_pi_algorithm_light(): def test_pi_algorithm_light():
"""Test the PI algorithm""" """ Test the PI algorithm """
the_algo = PITemperatureRegulator( the_algo = PITemperatureRegulator(target_temp=20, kp=0.2, ki=0.05, k_ext=0.1, offset_max=2, stabilization_threshold=0.1, accumulated_error_threshold=20)
target_temp=20,
kp=0.2,
ki=0.05,
k_ext=0.1,
offset_max=2,
stabilization_threshold=0.1,
accumulated_error_threshold=20,
)
assert the_algo assert the_algo
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
assert the_algo.calculate_regulated_temperature(18, 10) == 21.3 # +1.5 assert the_algo.calculate_regulated_temperature(18, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.4 # +1.6 assert the_algo.calculate_regulated_temperature(18.1, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.4 # +1.6 assert the_algo.calculate_regulated_temperature(18.3, 10) == 21.6 # +1.6
assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.5 # +1.7 assert the_algo.calculate_regulated_temperature(18.5, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(18.7, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(19, 10) == 21.6 # +1.7 assert the_algo.calculate_regulated_temperature(19, 10) == 21.7 # +1.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5 assert the_algo.calculate_regulated_temperature(20, 10) == 21.5 # +1.5
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.8 assert the_algo.calculate_regulated_temperature(21, 10) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 # +0.7 assert the_algo.calculate_regulated_temperature(21, 10) == 20.7 # +0.7
assert the_algo.calculate_regulated_temperature(20, 10) == 21.4 # +0.7 assert the_algo.calculate_regulated_temperature(20, 10) == 20.9 # +0.7
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 12) == 21.2 # +0.8 assert the_algo.calculate_regulated_temperature(20, 12) == 20.8 # +0.8
assert the_algo.calculate_regulated_temperature(20, 15) == 20.9 # +0.5 assert the_algo.calculate_regulated_temperature(20, 15) == 20.5 # +0.5
assert the_algo.calculate_regulated_temperature(20, 18) == 20.6 # +0.2 assert the_algo.calculate_regulated_temperature(20, 18) == 20.2 # +0.2
assert the_algo.calculate_regulated_temperature(20, 20) == 20.4 # = assert the_algo.calculate_regulated_temperature(20, 20) == 20.0 # =
def test_pi_algorithm_medium(): def test_pi_algorithm_medium():
"""Test the PI algorithm""" """ Test the PI algorithm """
the_algo = PITemperatureRegulator( the_algo = PITemperatureRegulator(target_temp=20, kp=0.5, ki=0.1, k_ext=0.1, offset_max=3, stabilization_threshold=0.1, accumulated_error_threshold=30)
target_temp=20,
kp=0.5,
ki=0.1,
k_ext=0.1,
offset_max=3,
stabilization_threshold=0.1,
accumulated_error_threshold=30,
)
assert the_algo assert the_algo
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
assert the_algo.calculate_regulated_temperature(18, 10) == 22.0 assert the_algo.calculate_regulated_temperature(18, 10) == 22.2
assert the_algo.calculate_regulated_temperature(18.1, 10) == 22.1 assert the_algo.calculate_regulated_temperature(18.1, 10) == 22.3
assert the_algo.calculate_regulated_temperature(18.3, 10) == 22.2 assert the_algo.calculate_regulated_temperature(18.3, 10) == 22.4
assert the_algo.calculate_regulated_temperature(18.5, 10) == 22.3 assert the_algo.calculate_regulated_temperature(18.5, 10) == 22.5
assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.4 assert the_algo.calculate_regulated_temperature(18.7, 10) == 22.5
assert the_algo.calculate_regulated_temperature(19, 10) == 22.3 assert the_algo.calculate_regulated_temperature(19, 10) == 22.4
assert the_algo.calculate_regulated_temperature(20, 10) == 21.9 assert the_algo.calculate_regulated_temperature(20, 10) == 21.9
assert the_algo.calculate_regulated_temperature(21, 10) == 21.4 assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
assert the_algo.calculate_regulated_temperature(21, 10) == 21.3 assert the_algo.calculate_regulated_temperature(21, 10) == 20.3
assert the_algo.calculate_regulated_temperature(20, 10) == 21.7 assert the_algo.calculate_regulated_temperature(20, 10) == 20.8
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 8) == 21.9 assert the_algo.calculate_regulated_temperature(20, 8) == 21.2
assert the_algo.calculate_regulated_temperature(20, 6) == 22.1 assert the_algo.calculate_regulated_temperature(20, 6) == 21.4
assert the_algo.calculate_regulated_temperature(20, 4) == 22.3 assert the_algo.calculate_regulated_temperature(20, 4) == 21.6
assert the_algo.calculate_regulated_temperature(20, 2) == 22.5 assert the_algo.calculate_regulated_temperature(20, 2) == 21.8
assert the_algo.calculate_regulated_temperature(20, 0) == 22.7 assert the_algo.calculate_regulated_temperature(20, 0) == 22.0
assert the_algo.calculate_regulated_temperature(20, -2) == 22.9 assert the_algo.calculate_regulated_temperature(20, -2) == 22.2
assert the_algo.calculate_regulated_temperature(20, -4) == 23.0 assert the_algo.calculate_regulated_temperature(20, -4) == 22.4
assert the_algo.calculate_regulated_temperature(20, -6) == 23.0 assert the_algo.calculate_regulated_temperature(20, -6) == 22.6
assert the_algo.calculate_regulated_temperature(20, -8) == 23.0 assert the_algo.calculate_regulated_temperature(20, -8) == 22.8
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the error acculation effect # Test the error acculation effect
assert the_algo.calculate_regulated_temperature(19, 5) == 22.0
assert the_algo.calculate_regulated_temperature(19, 5) == 22.1 assert the_algo.calculate_regulated_temperature(19, 5) == 22.1
assert the_algo.calculate_regulated_temperature(19, 5) == 22.2 assert the_algo.calculate_regulated_temperature(19, 5) == 22.2
assert the_algo.calculate_regulated_temperature(19, 5) == 22.3 assert the_algo.calculate_regulated_temperature(19, 5) == 22.3
@@ -146,46 +117,38 @@ def test_pi_algorithm_medium():
assert the_algo.calculate_regulated_temperature(19, 5) == 23 assert the_algo.calculate_regulated_temperature(19, 5) == 23
assert the_algo.calculate_regulated_temperature(19, 5) == 23 assert the_algo.calculate_regulated_temperature(19, 5) == 23
assert the_algo.calculate_regulated_temperature(19, 5) == 23 assert the_algo.calculate_regulated_temperature(19, 5) == 23
assert the_algo.calculate_regulated_temperature(19, 5) == 23
def test_pi_algorithm_strong(): def test_pi_algorithm_strong():
"""Test the PI algorithm""" """ Test the PI algorithm """
the_algo = PITemperatureRegulator( the_algo = PITemperatureRegulator(target_temp=20, kp=0.6, ki=0.2, k_ext=0.2, offset_max=4, stabilization_threshold=0.1, accumulated_error_threshold=40)
target_temp=20,
kp=0.6,
ki=0.2,
k_ext=0.2,
offset_max=4,
stabilization_threshold=0.1,
accumulated_error_threshold=40,
)
assert the_algo assert the_algo
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
assert the_algo.calculate_regulated_temperature(18, 10) == 23.2 assert the_algo.calculate_regulated_temperature(18, 10) == 23.6
assert the_algo.calculate_regulated_temperature(18.1, 10) == 23.5 assert the_algo.calculate_regulated_temperature(18.1, 10) == 23.9
assert the_algo.calculate_regulated_temperature(18.3, 10) == 23.8 assert the_algo.calculate_regulated_temperature(18.3, 10) == 24.0
assert the_algo.calculate_regulated_temperature(18.5, 10) == 24 assert the_algo.calculate_regulated_temperature(18.5, 10) == 24
assert the_algo.calculate_regulated_temperature(18.7, 10) == 24 assert the_algo.calculate_regulated_temperature(18.7, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24 assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(20, 10) == 23.9 assert the_algo.calculate_regulated_temperature(20, 10) == 23.9
assert the_algo.calculate_regulated_temperature(21, 10) == 23.3 assert the_algo.calculate_regulated_temperature(21, 10) == 21.2
assert the_algo.calculate_regulated_temperature(21, 10) == 23.1 assert the_algo.calculate_regulated_temperature(21, 10) == 21
assert the_algo.calculate_regulated_temperature(21, 10) == 22.9 assert the_algo.calculate_regulated_temperature(21, 10) == 20.8
assert the_algo.calculate_regulated_temperature(21, 10) == 22.7 assert the_algo.calculate_regulated_temperature(21, 10) == 20.6
assert the_algo.calculate_regulated_temperature(21, 10) == 22.5 assert the_algo.calculate_regulated_temperature(21, 10) == 20.4
assert the_algo.calculate_regulated_temperature(21, 10) == 22.3 assert the_algo.calculate_regulated_temperature(21, 10) == 20.2
assert the_algo.calculate_regulated_temperature(21, 10) == 22.1 assert the_algo.calculate_regulated_temperature(21, 10) == 20
# Test temperature external # Test temperature external
assert the_algo.calculate_regulated_temperature(20, 8) == 22.9 assert the_algo.calculate_regulated_temperature(20, 8) == 21.0
assert the_algo.calculate_regulated_temperature(20, 6) == 23.3 assert the_algo.calculate_regulated_temperature(20, 6) == 22.8
assert the_algo.calculate_regulated_temperature(20, 4) == 23.7 assert the_algo.calculate_regulated_temperature(20, 4) == 23.2
assert the_algo.calculate_regulated_temperature(20, 2) == 24 assert the_algo.calculate_regulated_temperature(20, 2) == 23.6
assert the_algo.calculate_regulated_temperature(20, 0) == 24 assert the_algo.calculate_regulated_temperature(20, 0) == 24
assert the_algo.calculate_regulated_temperature(20, -2) == 24 assert the_algo.calculate_regulated_temperature(20, -2) == 24
assert the_algo.calculate_regulated_temperature(20, -4) == 24 assert the_algo.calculate_regulated_temperature(20, -4) == 24
@@ -194,16 +157,15 @@ def test_pi_algorithm_strong():
# to reset the accumulated erro # to reset the accumulated erro
the_algo.set_target_temp(20) the_algo.set_target_temp(20)
the_algo.reset_accumulated_error()
# Test the error acculation effect # Test the error acculation effect
assert the_algo.calculate_regulated_temperature(19, 10) == 22.6
assert the_algo.calculate_regulated_temperature(19, 10) == 22.8 assert the_algo.calculate_regulated_temperature(19, 10) == 22.8
assert the_algo.calculate_regulated_temperature(19, 10) == 23.0 assert the_algo.calculate_regulated_temperature(19, 10) == 23
assert the_algo.calculate_regulated_temperature(19, 10) == 23.2 assert the_algo.calculate_regulated_temperature(19, 10) == 23.2
assert the_algo.calculate_regulated_temperature(19, 10) == 23.4 assert the_algo.calculate_regulated_temperature(19, 10) == 23.4
assert the_algo.calculate_regulated_temperature(19, 10) == 23.6 assert the_algo.calculate_regulated_temperature(19, 10) == 23.6
assert the_algo.calculate_regulated_temperature(19, 10) == 23.8 assert the_algo.calculate_regulated_temperature(19, 10) == 23.8
assert the_algo.calculate_regulated_temperature(19, 10) == 24.0 assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24.0 assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24.0 assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24.0 assert the_algo.calculate_regulated_temperature(19, 10) == 24
assert the_algo.calculate_regulated_temperature(19, 10) == 24

View File

@@ -4,11 +4,8 @@ from unittest.mock import patch, call
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from custom_components.versatile_thermostat.thermostat_switch import ( from custom_components.versatile_thermostat.thermostat_switch import ThermostatOverSwitch
ThermostatOverSwitch,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@@ -188,7 +185,6 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"current_power": 50, "current_power": 50,
"device_power": 100, "device_power": 100,
"current_power_max": 149, "current_power_max": 149,
"current_power_consumption": 100.0,
}, },
), ),
], ],
@@ -260,7 +256,6 @@ async def test_power_management_energy_over_switch(
CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch", CONF_HEATER: "switch.mock_switch",
CONF_HEATER_2: "switch.mock_switch2",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01, CONF_TPI_COEF_EXT: 0.01,
@@ -283,7 +278,6 @@ async def test_power_management_energy_over_switch(
assert tpi_algo assert tpi_algo
assert entity.total_energy == 0 assert entity.total_energy == 0
assert entity.nb_underlying_entities == 2
# set temperature to 15 so that on_percent will be set # set temperature to 15 so that on_percent will be set
with patch( with patch(
@@ -303,7 +297,7 @@ async def test_power_management_energy_over_switch(
assert entity.current_temperature == 15 assert entity.current_temperature == 15
assert tpi_algo.on_percent == 1 assert tpi_algo.on_percent == 1
assert entity.device_power == 100.0 assert entity.mean_cycle_power == 100.0
assert mock_send_event.call_count == 2 assert mock_send_event.call_count == 2
assert mock_heater_on.call_count == 1 assert mock_heater_on.call_count == 1

View File

@@ -56,7 +56,7 @@ async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_i
assert entity.ac_mode is True assert entity.ac_mode is True
assert entity.hvac_action is HVACAction.OFF assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_modes == [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF] assert entity.hvac_modes == [HVACMode.COOL, HVACMode.OFF]
assert entity.target_temperature == entity.max_temp assert entity.target_temperature == entity.max_temp
assert entity.preset_modes == [ assert entity.preset_modes == [
PRESET_NONE, PRESET_NONE,
@@ -138,15 +138,3 @@ async def test_over_switch_ac_full_start(hass: HomeAssistant, skip_hass_states_i
assert entity.hvac_mode is HVACMode.COOL assert entity.hvac_mode is HVACMode.COOL
assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE) assert (entity.hvac_action is HVACAction.OFF or entity.hvac_action is HVACAction.IDLE)
assert entity.target_temperature == 27 # eco_ac_away assert entity.target_temperature == 27 # eco_ac_away
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode is PRESET_COMFORT
assert entity.target_temperature == 26
# switch to Eco
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
assert entity.target_temperature == 27

View File

@@ -64,7 +64,7 @@ async def test_window_management_time_not_enough(
assert entity.overpowering_state is None assert entity.overpowering_state is None
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
# Open the window, but condition of time is not satisfied and check the thermostat don't turns off # Open the window, but condition of time is not satisfied and check the thermostat don't turns off
with patch( with patch(
@@ -152,7 +152,7 @@ async def test_window_management_time_enough(
assert entity.overpowering_state is None assert entity.overpowering_state is None
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
# change temperature to force turning on the heater # change temperature to force turning on the heater
with patch( with patch(
@@ -242,7 +242,7 @@ async def test_window_management_time_enough(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state): async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Window management""" """Test the Power management"""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@@ -294,7 +294,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.overpowering_state is None assert entity.overpowering_state is None
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is None
# Make the temperature down # Make the temperature down
with patch( with patch(
@@ -430,11 +430,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="TheOverClimateMockName", title="TheOverSwitchMockName",
unique_id="uniqueId", unique_id="uniqueId",
data={ data={
CONF_NAME: "TheOverClimateMockName", CONF_NAME: "TheOverSwitchMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor", CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor", CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
@@ -447,7 +447,10 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "switch.mock_climate", CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,
@@ -458,7 +461,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
) )
entity: BaseThermostat = await create_thermostat( entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverclimatemockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
@@ -466,7 +469,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
now = datetime.now(tz) now = datetime.now(tz)
tpi_algo = entity._prop_algorithm tpi_algo = entity._prop_algorithm
assert tpi_algo is None assert tpi_algo
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
@@ -475,22 +478,24 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert entity.overpowering_state is None assert entity.overpowering_state is None
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is None
# Make the temperature down # Make the temperature down
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_set_hvac_mode, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True, return_value=True,
): ):
event_timestamp = now - timedelta(minutes=4) event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 19, event_timestamp) await send_temperature_change_event(entity, 19, event_timestamp)
# The climate turns on but was alredy on # The heater turns on
assert mock_set_hvac_mode.call_count == 0 assert mock_heater_on.call_count == 1
assert entity.last_temperature_slope is None assert entity.last_temperature_slope is None
assert entity._window_auto_algo.is_window_open_detected() is False assert entity._window_auto_algo.is_window_open_detected() is False
assert entity._window_auto_algo.is_window_close_detected() is False assert entity._window_auto_algo.is_window_close_detected() is False
@@ -500,8 +505,10 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_set_hvac_mode, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True, return_value=True,
): ):
@@ -524,7 +531,8 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
], ],
any_order=True, any_order=True,
) )
assert mock_set_hvac_mode.call_count >= 1 assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count >= 1
assert entity.last_temperature_slope == -1 assert entity.last_temperature_slope == -1
assert entity._window_auto_algo.is_window_open_detected() is True assert entity._window_auto_algo.is_window_open_detected() is True
assert entity._window_auto_algo.is_window_close_detected() is False assert entity._window_auto_algo.is_window_close_detected() is False
@@ -535,14 +543,17 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
with patch( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_set_hvac_mode, patch( ) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False, return_value=False,
): ):
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
assert mock_set_hvac_mode.call_count == 1 assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert round(entity.last_temperature_slope, 3) == -1 assert round(entity.last_temperature_slope, 3) == -1
# Because the algorithm is not aware of the expiration, for the algo we are still in alert # Because the algorithm is not aware of the expiration, for the algo we are still in alert
assert entity._window_auto_algo.is_window_open_detected() is True assert entity._window_auto_algo.is_window_open_detected() is True
@@ -612,7 +623,7 @@ async def test_window_auto_no_on_percent(
assert entity.overpowering_state is None assert entity.overpowering_state is None
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is None
# Make the temperature down # Make the temperature down
with patch( with patch(
@@ -663,11 +674,12 @@ async def test_window_auto_no_on_percent(
# Clean the entity # Clean the entity
entity.remove_thermostat() entity.remove_thermostat()
#PR - Adding Window Bypass
# PR - Adding Window Bypass
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state): async def test_window_bypass(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Window management when bypass enabled""" """Test the Window management when bypass enabled"""
entry = MockConfigEntry( entry = MockConfigEntry(
@@ -716,7 +728,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.overpowering_state is None assert entity.overpowering_state is None
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
# change temperature to force turning on the heater # change temperature to force turning on the heater
with patch( with patch(
@@ -798,8 +810,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
# Clean the entity # Clean the entity
entity.remove_thermostat() entity.remove_thermostat()
#PR - Adding Window bypass for window auto algorithm
# PR - Adding Window bypass for window auto algorithm
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state): async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state):
@@ -855,7 +866,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
assert entity.overpowering_state is None assert entity.overpowering_state is None
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is None
# Make the temperature down # Make the temperature down
with patch( with patch(
@@ -910,8 +921,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
# Clean the entity # Clean the entity
entity.remove_thermostat() entity.remove_thermostat()
#PR - Adding Window bypass AFTER detection have been done should reactivate the heater
# PR - Adding Window bypass AFTER detection have been done should reactivate the heater
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is_state): async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is_state):
@@ -963,7 +973,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
assert entity.overpowering_state is None assert entity.overpowering_state is None
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is None
# change temperature to force turning on the heater # change temperature to force turning on the heater
with patch( with patch(
@@ -1039,4 +1049,4 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
) )
# Clean the entity # Clean the entity
entity.remove_thermostat() entity.remove_thermostat()