Compare commits

..

30 Commits
3.2.0 ... 3.3.5

Author SHA1 Message Date
Jean-Marc Collin
2bebe3e210 Issue #95 - the integration would switch ac on and off rapidly and lock up home assistant if outside temp is NaN 2023-08-05 20:02:54 +02:00
Jean-Marc Collin
aa3b87762d FIX AC climate stops even if already stopped 2023-07-30 21:25:20 +02:00
Jean-Marc Collin
f4cabbf2c0 FIX unit tests 2023-07-30 18:09:42 +02:00
Jean-Marc Collin
24b59e545b FIX cancel_timer await 2023-07-29 11:22:50 +02:00
Jean-Marc Collin
5997a26c73 FIX #90 WarCOzes remark 2023-07-23 09:08:56 +02:00
Jean-Marc Collin
fe4b9ced81 Documentation 2023-07-22 17:23:42 +02:00
Jean-Marc Collin
c4fc976007 Terminate Add AC mode and fix remove_thermostat sync 2023-07-22 17:05:54 +02:00
Jean-Marc Collin
31d862acab With config_flow ok 2023-07-22 12:21:54 +02:00
Jean-Marc Collin
9709a9eed0 Add AC mode configFlow 2023-07-22 10:47:10 +02:00
Jean-Marc Collin
61eae8c066 FIX #89 _is_aux_heat error 2023-06-25 12:27:00 +02:00
Jean-Marc Collin
e16daa3d53 Add hacs.yaml github workflow 2023-05-14 11:11:55 +02:00
Jean-Marc Collin
90a6c926e3 Resolve actions warning 2023-05-14 11:02:29 +02:00
Jean-Marc Collin
64ce3aa0ad Issue #81 - recursive loop when security should be set 2023-04-28 12:12:38 +02:00
Jean-Marc Collin
3f498ffbd3 Add BuyMeCoffee contributors (fr) 2023-04-26 08:23:03 +02:00
Jean-Marc Collin
3236be6c3b Update README.md
Add BuyMeCoffee contributors
2023-04-26 08:21:54 +02:00
Jean-Marc Collin
be86fd3ac0 Merge branch 'main' of github.com:jmcollin78/versatile_thermostat into main 2023-04-22 11:06:12 +02:00
Jean-Marc Collin
e35ba57bd7 Fix compilation warnings 2023-04-22 11:05:38 +02:00
Bergoglio
72d7803ffa Add files via upload (#79)
Thanks @Bergoglio !
2023-04-22 10:29:48 +02:00
Jean-Marc Collin
4dd7c62a42 Migration to 2023.4 and fix lingering tasks and timers tests errors 2023-04-22 09:24:57 +02:00
Jean-Marc Collin
429ff47269 Typo in README 2023-04-14 08:34:43 +02:00
Jean-Marc Collin
a17423d470 FIX #73, #72. thanks to Salabur 2023-04-14 08:24:01 +02:00
Jean-Marc Collin
81a467b8c3 Translations typos 2023-04-02 17:40:40 +02:00
Jean-Marc Collin
4e3ee0703b Typo 2023-04-02 17:33:59 +02:00
Jean-Marc Collin
539ec4a6bd Isseu #70. Build release. 2023-04-02 16:44:57 +02:00
Jean-Marc Collin
9b085f1264 FIX overpowering ko 2023-03-28 10:06:35 +02:00
Jean-Marc Collin
637367bd65 FIX circular call on over_climate 2023-03-28 09:33:50 +02:00
Jean-Marc Collin
bcc0a32b6a Release 3.2 beta1 - an error in multi-testu 2023-03-26 18:39:08 +02:00
Jean-Marc Collin
80fa977c15 With all testu developed and ok 2023-03-26 12:40:38 +02:00
Jean-Marc Collin
67d20dd083 Testu ok 2023-03-25 13:02:03 +01:00
Jean-Marc Collin
e2e8499bdb First commit not completed 2023-03-25 12:32:51 +01:00
36 changed files with 3030 additions and 835 deletions

View File

@@ -1,182 +1,196 @@
default_config:
logger:
default: info
logs:
custom_components.versatile_thermostat: debug
default: info
logs:
custom_components.versatile_thermostat: info
custom_components.versatile_thermostat.underlyings: debug
custom_components.versatile_thermostat.climate: debug
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy:
start: true
wait: false
port: 5678
start: true
wait: false
port: 5678
input_number:
fake_temperature_sensor1:
name: Temperature
min: 0
max: 35
step: .1
icon: mdi:thermometer
unit_of_measurement: °C
mode: box
fake_external_temperature_sensor1:
name: Ext Temperature
min: -10
max: 35
step: .1
icon: mdi:home-thermometer
unit_of_measurement: °C
mode: box
fake_current_power:
name: Current power
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_current_power_max:
name: Current power max threshold
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_temperature_sensor1:
name: Temperature
min: 0
max: 35
step: .1
icon: mdi:thermometer
unit_of_measurement: °C
mode: box
fake_external_temperature_sensor1:
name: Ext Temperature
min: -10
max: 35
step: .1
icon: mdi:home-thermometer
unit_of_measurement: °C
mode: box
fake_current_power:
name: Current power
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_current_power_max:
name: Current power max threshold
min: 0
max: 1000
step: 10
icon: mdi:flash
unit_of_measurement: kW
input_boolean:
# input_boolean to simulate the windows entity. Only for development environment.
fake_window_sensor1:
name: Window 1
icon: mdi:window-closed-variant
# input_boolean to simulate the heater entity switch. Only for development environment.
fake_heater_switch1:
name: Heater 1 (Linear)
icon: mdi:radiator
fake_heater_switch2:
name: Heater (TPI with presence preset)
icon: mdi:radiator
fake_heater_switch3:
name: Heater (TPI with offset)
icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1:
name: Motion Sensor 1
icon: mdi:run
# input_boolean to simulate the presence sensor entity. Only for development environment.
fake_presence_sensor1:
name: Presence Sensor 1
icon: mdi:home
# input_boolean to simulate the windows entity. Only for development environment.
fake_window_sensor1:
name: Window 1
icon: mdi:window-closed-variant
# input_boolean to simulate the heater entity switch. Only for development environment.
fake_heater_switch3:
name: Heater 3
icon: mdi:radiator
fake_heater_switch2:
name: Heater 2
icon: mdi:radiator
fake_heater_switch1:
name: Heater 1
icon: mdi:radiator
fake_heater_4switch1:
name: Heater (multiswitch1)
icon: mdi:radiator
fake_heater_4switch2:
name: Heater (multiswitch2)
icon: mdi:radiator
fake_heater_4switch3:
name: Heater (multiswitch3)
icon: mdi:radiator
fake_heater_4switch4:
name: Heater (multiswitch4)
icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1:
name: Motion Sensor 1
icon: mdi:run
# input_boolean to simulate the presence sensor entity. Only for development environment.
fake_presence_sensor1:
name: Presence Sensor 1
icon: mdi:home
climate:
- platform: generic_thermostat
name: Underlying thermostat1
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat4
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat5
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat6
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat7
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat8
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat1
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat4
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat5
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat6
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat7
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat8
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat
name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1
recorder:
include:
domains:
- input_boolean
- input_number
- switch
- climate
- sensor
include:
domains:
- input_boolean
- input_number
- switch
- climate
- sensor
template:
- binary_sensor:
- name: maison_occupee
unique_id: maison_occupee
state: "{{is_state('person.jmc', 'home') }}"
device_class: occupancy
- sensor:
- name: "Total énergie switch1"
unique_id: total_energie_switch1
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_switch_1', 'total_energy') %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }}
{% endif %}
- name: "Total énergie climate 2"
unique_id: total_energie_climate2
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_climate_2', 'total_energy') %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }}
{% endif %}
- name: "Total énergie chambre"
unique_id: total_energie_chambre
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_chambre', 'total_energy') %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }}
{% endif %}
- binary_sensor:
- name: maison_occupee
unique_id: maison_occupee
state: "{{is_state('person.jmc', 'home') }}"
device_class: occupancy
- sensor:
- name: "Total énergie switch1"
unique_id: total_energie_switch1
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_switch_1', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
- name: "Total énergie climate 2"
unique_id: total_energie_climate2
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_climate_2', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
- name: "Total énergie chambre"
unique_id: total_energie_chambre
unit_of_measurement: "kWh"
device_class: energy
state_class: total_increasing
state: >
{% set energy = state_attr('climate.thermostat_chambre', 'total_energy') | float(default=-1) %}
{% if energy < 0 %}{{none}}{% else %}
{{ energy | round(2, default=0) }}
{% endif %}
switch:
- platform: template
switches:
pilote_sdb_rdc:
friendly_name: "Pilote chauffage SDB RDC"
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
turn_on:
service: select.select_option
data:
option: comfort
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
- platform: template
switches:
pilote_sdb_rdc:
friendly_name: "Pilote chauffage SDB RDC"
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
turn_on:
service: select.select_option
data:
option: comfort
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off:
service: select.select_option
data:
option: comfort-2
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off:
service: select.select_option
data:
option: comfort-2
target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
frontend:
themes:
versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B"
state-binary_sensor-power-on-color: "#FF0B0B"
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen"
themes:
versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B"
state-binary_sensor-power-on-color: "#FF0B0B"
state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen"

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"

17
.github/workflows/hacs.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: HACS Action
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
jobs:
hacs:
name: HACS Action
runs-on: "ubuntu-latest"
steps:
- name: HACS Action
uses: "hacs/action@main"
with:
category: "integration"

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"
@@ -23,8 +23,8 @@ jobs:
runs-on: "ubuntu-latest"
name: Check style formatting
steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
- uses: "actions/checkout@v3.5.2"
- uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.x"
- run: python3 -m pip install black
@@ -35,9 +35,9 @@ jobs:
name: Run tests
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
uses: "actions/checkout@v3.5.2"
- name: Setup Python
uses: "actions/setup-python@v1"
uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.8"
- name: Install requirements

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- uses: "actions/checkout@v3.5.2"
- name: HACS validation
uses: "hacs/action@main"
@@ -26,8 +26,8 @@ jobs:
runs-on: "ubuntu-latest"
name: Check style formatting
steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
- uses: "actions/checkout@v3.5.2"
- uses: "actions/setup-python@v4.6.0"
with:
python-version: "3.x"
- run: python3 -m pip install black

View File

@@ -6,8 +6,9 @@
![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true)
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true?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. ;-).
- [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)
- [Pourquoi une nouvelle implémentation du thermostat ?](#pourquoi-une-nouvelle-implémentation-du-thermostat-)
- [Comment installer cet incroyable Thermostat Versatile ?](#comment-installer-cet-incroyable-thermostat-versatile-)
@@ -15,7 +16,7 @@
- [Installation manuelle](#installation-manuelle)
- [Configuration](#configuration)
- [Choix des attributs de base](#choix-des-attributs-de-base)
- [Sélectionnez l'entité pilotée](#sélectionnez-lentité-pilotée)
- [Sélectionnez des entités pilotées](#sélectionnez-des-entités-pilotées)
- [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 les portes/fenêtres en allumant/éteignant les thermostats](#configurer-les-portesfenêtres-en-allumantéteignant-les-thermostats)
@@ -53,12 +54,18 @@ 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*_
> > * **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.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 2.3** : ajout de la mesure de puissance et d'énergie du radiateur piloté par le thermostat.
> * **release 2.2** : ajout de fonction de sécurité permettant de ne pas laisser éternellement en chauffe un radiateur en cas de panne du thermomètre
> * **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.
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Un grand merci à @salabur, @pvince83 and @bergoglio pour les bières. Ca fait très plaisir.
# Quand l'utiliser et ne pas l'utiliser
Ce thermostat peut piloter 2 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 :
@@ -139,12 +146,16 @@ Donnez les principaux attributs obligatoires :
1. avec le type ```thermostat_over_swutch```, les calculs sont effectués à chaque cycle. Donc en cas de changement de conditions, il faudra attendre le prochain cycle pour voir un changement. Pour cette raison, le cycle ne doit pas être trop long. **5 min est une bonne valeur**,
2. si le cycle est trop court, le radiateur ne pourra jamais atteindre la température cible en effet pour le radiateur à accumulation et il sera sollicité inutilement
## Sélectionnez l'entité pilotée
En fonction de votre choix sur le type de thermostat, vous devrez choisir une entité de type switch ou une entité de type climate. Seules les entités compatibles sont présentées.
## Sélectionnez des entités pilotées
En fonction de votre choix sur le type de thermostat, vous devrez choisir une ou plusieurs entités de type switch ou une entité de type climate. Seules les entités compatibles sont présentées.
Pour un thermostat de type ```thermostat_over_switch```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme)
L'algorithme à utiliser est aujourd'hui limité à TPI est disponible. Voir [algorithme](#algorithme).
Si plusieurs entités de type sont configurées, la thermostat décale les activations afin de minimiser le nombre de switch actif à un instant t. Ca permet une meilleure répartition de la puissance puisque chaque radiateur va s'allumer à son tour.
Exemple de déclenchement synchronisé :
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true)
Pour un thermostat de type ```thermostat_over_climate```:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)

View File

@@ -6,8 +6,9 @@
![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/icon.png?raw=true)
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true?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 ;-).
- [Thanks for the beer buymecoffee](#thanks-for-the-beer-buymecoffee)
- [When to use / not use](#when-to-use--not-use)
- [Why another thermostat implementation ?](#why-another-thermostat-implementation-)
- [How to install this incredible Versatile Thermostat ?](#how-to-install-this-incredible-versatile-thermostat-)
@@ -52,12 +53,18 @@
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*_
> * **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.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.
> * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat.
> * **release 2.2**: addition of a safety function allowing a radiator not to be left heating forever in the event of a thermometer failure
> * **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.
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Many thanks to @salabur, @pvince83 and @bergoglio for the beers. It's very pleasing.
# When to use / not use
This thermostat can control 2 types of equipment:
1. a heater that only works in on/off mode (named ```thermostat_over_switch```). The minimum configuration required to use this type of thermostat is:
@@ -141,6 +148,9 @@ Depending on your choice on the type of thermostat, you will have to choose a sw
For a ```thermostat_over_switch``` thermostat:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity.png?raw=true)
The algorithm to be used today is limited to TPI is available. See [algorithm](#algorithm)
If several type entities are configured, the thermostat staggers the activations in order to minimize the number of active switches at a time t. This allows a better distribution of power since each radiator will turn on in turn.
Example of synchronized triggering:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/multi-switch-activation.png?raw=true)
For a ```thermostat_over_climate``` thermostat:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-linked-entity2.png?raw=true)

View File

@@ -27,6 +27,7 @@ fi
if [ "$command" == "hassfest" ]; then
echo "Running container start"
python3 -m script.hassfest
# python -m script.hassfest --requirements --action validate --integration-path config/custom_components/versatile_thermostat/
fi
if [ "$command" == "restart" ]; then

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ class VersatileThermostatBaseEntity(Entity):
_my_climate: VersatileThermostat
hass: HomeAssistant
_config_id: str
_devince_name: str
_device_name: str
def __init__(self, hass: HomeAssistant, config_id, device_name) -> None:
"""The CTOR"""

View File

@@ -41,6 +41,9 @@ from .const import (
DOMAIN,
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR,
@@ -58,7 +61,9 @@ from .const import (
CONF_CYCLE_MIN,
CONF_PRESET_POWER,
CONF_PRESETS,
CONF_PRESETS_WITH_AC,
CONF_PRESETS_AWAY,
CONF_PRESETS_AWAY_WITH_AC,
CONF_PRESETS_SELECTIONABLE,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_EXT,
@@ -80,6 +85,7 @@ from .const import (
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_AC_MODE,
CONF_THERMOSTAT_TYPES,
UnknownEntity,
WindowOpenDetectionMethod,
@@ -129,35 +135,6 @@ def add_suggested_values_to_schema(
return vol.Schema(schema)
# def is_temperature_sensor(sensor: RegistryEntry):
# """Check if a registryEntry is a temperature sensor or assimilable to a temperature sensor"""
# if not sensor.entity_id.startswith(
# INPUT_NUMBER_DOMAIN
# ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
# return False
# return (
# sensor.device_class == TEMPERATURE
# or sensor.original_device_class == TEMPERATURE
# or sensor.unit_of_measurement in TEMPERATURE_UNITS
# )
#
#
# def is_power_sensor(sensor: RegistryEntry):
# """Check if a registryEntry is a power sensor or assimilable to a temperature sensor"""
# if not sensor.entity_id.startswith(
# INPUT_NUMBER_DOMAIN
# ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
# return False
# return (
# sensor.unit_of_measurement
# in [
# UnitOfPower.KILO_WATT,
# UnitOfPower.WATT,
# UnitOfPower.BTU_PER_HOUR,
# ]
# )
class VersatileThermostatBaseConfigFlow(FlowHandler):
"""The base Config flow class. Used to put some code in commons."""
@@ -224,6 +201,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
),
vol.Optional(CONF_HEATER_2): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
),
vol.Optional(CONF_HEATER_3): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
),
vol.Optional(CONF_HEATER_4): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[SWITCH_DOMAIN, INPUT_BOOLEAN_DOMAIN]
),
),
vol.Required(
CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI
): vol.In(
@@ -239,6 +231,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
vol.Required(CONF_CLIMATE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
}
)
@@ -256,6 +249,15 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
}
)
self.STEP_PRESETS_WITH_AC_DATA_SCHEMA = ( # pylint: disable=invalid-name
vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(v, default=0.0): vol.Coerce(float)
for (k, v) in CONF_PRESETS_WITH_AC.items()
}
)
)
self.STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
@@ -322,6 +324,27 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
}
)
self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA = ( # pylint: disable=invalid-name
vol.Schema(
{
vol.Optional(CONF_PRESENCE_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=[
PERSON_DOMAIN,
BINARY_SENSOR_DOMAIN,
INPUT_BOOLEAN_DOMAIN,
]
),
),
}
).extend(
{
vol.Optional(v, default=17): vol.Coerce(float)
for (k, v) in CONF_PRESETS_AWAY_WITH_AC.items()
}
)
)
self.STEP_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(
@@ -471,9 +494,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
)
if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps"""
@@ -524,9 +550,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESENCE_DATA_SCHEMA
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
schema,
user_input,
self.async_step_advanced,
)
@@ -658,9 +689,12 @@ class VersatileThermostatOptionsFlowHandler(
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
)
if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps"""
@@ -718,9 +752,14 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
)
if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESENCE_DATA_SCHEMA
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
schema,
user_input,
self.async_step_advanced,
)

View File

@@ -11,6 +11,11 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
)
PRESET_AC_SUFFIX = "_ac"
PRESET_ECO_AC = PRESET_ECO + PRESET_AC_SUFFIX
PRESET_COMFORT_AC = PRESET_COMFORT + PRESET_AC_SUFFIX
PRESET_BOOST_AC = PRESET_BOOST + PRESET_AC_SUFFIX
from homeassistant.exceptions import HomeAssistantError
from .prop_algorithm import (
@@ -30,6 +35,9 @@ DOMAIN = "versatile_thermostat"
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.SENSOR]
CONF_HEATER = "heater_entity_id"
CONF_HEATER_2 = "heater_entity2_id"
CONF_HEATER_3 = "heater_entity3_id"
CONF_HEATER_4 = "heater_entity4_id"
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
CONF_POWER_SENSOR = "power_sensor_entity_id"
@@ -61,6 +69,7 @@ CONF_USE_WINDOW_FEATURE = "use_window_feature"
CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
CONF_USE_POWER_FEATURE = "use_power_feature"
CONF_AC_MODE = "ac_mode"
CONF_WINDOW_AUTO_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
@@ -74,14 +83,39 @@ CONF_PRESETS = {
)
}
CONF_PRESETS_WITH_AC = {
p: f"{p}_temp"
for p in (
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ECO_AC,
PRESET_COMFORT_AC,
PRESET_BOOST_AC,
)
}
PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = {
p: f"{p}_temp"
for p in (
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
)
}
CONF_PRESETS_AWAY_WITH_AC = {
p: f"{p}_temp"
for p in (
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_ECO_AC + PRESET_AWAY_SUFFIX,
PRESET_COMFORT_AC + PRESET_AWAY_SUFFIX,
PRESET_BOOST_AC + PRESET_AWAY_SUFFIX,
)
}
@@ -89,6 +123,8 @@ CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values())
CONF_PRESETS_WITH_AC_VALUES = list(CONF_PRESETS_WITH_AC.values())
CONF_PRESETS_AWAY_WITH_AC_VALUES = list(CONF_PRESETS_AWAY_WITH_AC.values())
ALL_CONF = (
[
@@ -127,9 +163,12 @@ ALL_CONF = (
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_AC_MODE,
]
+ CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES,
+ CONF_PRESETS_AWAY_VALUES
+ CONF_PRESETS_WITH_AC_VALUES
+ CONF_PRESETS_AWAY_WITH_AC_VALUES,
)
CONF_FUNCTIONS = [

View File

@@ -54,7 +54,7 @@ class WindowOpenDetectionAlgorithm:
delta_t_sec = float((datetime_measure - self._last_datetime).total_seconds())
delta_t = delta_t_sec / 60.0
if delta_t_sec <= MIN_DELTA_T_SEC:
_LOGGER.warning(
_LOGGER.debug(
"Delta t is %d < %d which should be not possible. We don't consider this value",
delta_t_sec,
MIN_DELTA_T_SEC,
@@ -64,7 +64,7 @@ class WindowOpenDetectionAlgorithm:
delta_temp = float(temperature - self._last_temperature)
new_slope = delta_temp / delta_t
if new_slope > MAX_SLOPE_VALUE or new_slope < -MAX_SLOPE_VALUE:
_LOGGER.warning(
_LOGGER.debug(
"New_slope is abs(%.2f) > %.2f which should be not possible. We don't consider this value",
new_slope,
MAX_SLOPE_VALUE,

View File

@@ -1 +1,2 @@
homeassistant
homeassistant
ffmpeg

View File

@@ -22,12 +22,25 @@
}
},
"type": {
"title": "Linked entity",
"description": "Linked entity attributes",
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater entity id",
"heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch",
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat",
"ac_mode": "AC mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying thermostat entity id"
"climate_entity_id": "Underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode"
}
},
"tpi": {
@@ -44,7 +57,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -91,7 +107,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -142,12 +161,25 @@
}
},
"type": {
"title": "Linked entity",
"description": "Linked entity attributes",
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater entity id",
"heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch",
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat",
"ac_mode": "AC mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying thermostat entity id"
"climate_entity_id": "Underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode"
}
},
"tpi": {
@@ -164,7 +196,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -211,7 +246,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -262,16 +300,5 @@
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"none": "Manual"
}
}
}
}
}

View File

@@ -1,5 +1,8 @@
""" Some common resources """
import asyncio
import logging
from unittest.mock import patch, MagicMock
import pytest # pylint: disable=unused-import
from homeassistant.core import HomeAssistant, Event, EVENT_STATE_CHANGED, State
from homeassistant.const import UnitOfTemperature, STATE_ON, STATE_OFF
@@ -19,11 +22,14 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
from ..climate import VersatileThermostat
from ..const import * # pylint: disable=wildcard-import, unused-wildcard-import
from ..underlyings import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import ( # pylint: disable=unused-import
MOCK_TH_OVER_SWITCH_USER_CONFIG,
MOCK_TH_OVER_4SWITCH_USER_CONFIG,
MOCK_TH_OVER_CLIMATE_USER_CONFIG,
MOCK_TH_OVER_SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG,
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG,
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
MOCK_PRESETS_CONFIG,
@@ -59,6 +65,20 @@ PARTIAL_CLIMATE_CONFIG = (
| MOCK_ADVANCED_CONFIG
)
FULL_4SWITCH_CONFIG = (
MOCK_TH_OVER_4SWITCH_USER_CONFIG
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_WINDOW_CONFIG
| MOCK_MOTION_CONFIG
| MOCK_POWER_CONFIG
| MOCK_PRESENCE_CONFIG
| MOCK_ADVANCED_CONFIG
)
_LOGGER = logging.getLogger(__name__)
class MockClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
@@ -180,8 +200,16 @@ def search_entity(hass: HomeAssistant, entity_id, domain) -> Entity:
return None
async def send_temperature_change_event(entity: VersatileThermostat, new_temp, date):
async def send_temperature_change_event(
entity: VersatileThermostat, new_temp, date, sleep=True
):
"""Sending a new temperature event simulating a change on temperature sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
new_temp,
date,
entity,
)
temp_event = Event(
EVENT_STATE_CHANGED,
{
@@ -193,13 +221,21 @@ async def send_temperature_change_event(entity: VersatileThermostat, new_temp, d
)
},
)
return await entity._async_temperature_changed(temp_event)
await entity._async_temperature_changed(temp_event)
if sleep:
await asyncio.sleep(0.1)
async def send_ext_temperature_change_event(
entity: VersatileThermostat, new_temp, date
entity: VersatileThermostat, new_temp, date, sleep=True
):
"""Sending a new external temperature event simulating a change on temperature sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_temp=%.2f date=%s on %s",
new_temp,
date,
entity,
)
temp_event = Event(
EVENT_STATE_CHANGED,
{
@@ -211,11 +247,21 @@ async def send_ext_temperature_change_event(
)
},
)
return await entity._async_ext_temperature_changed(temp_event)
await entity._async_ext_temperature_changed(temp_event)
if sleep:
await asyncio.sleep(0.1)
async def send_power_change_event(entity: VersatileThermostat, new_power, date):
async def send_power_change_event(
entity: VersatileThermostat, new_power, date, sleep=True
):
"""Sending a new power event simulating a change on power sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_power=%.2f date=%s on %s",
new_power,
date,
entity,
)
power_event = Event(
EVENT_STATE_CHANGED,
{
@@ -227,11 +273,21 @@ async def send_power_change_event(entity: VersatileThermostat, new_power, date):
)
},
)
return await entity._async_power_changed(power_event)
await entity._async_power_changed(power_event)
if sleep:
await asyncio.sleep(0.1)
async def send_max_power_change_event(entity: VersatileThermostat, new_power_max, date):
async def send_max_power_change_event(
entity: VersatileThermostat, new_power_max, date, sleep=True
):
"""Sending a new power max event simulating a change on power max sensor"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_power_max=%.2f date=%s on %s",
new_power_max,
date,
entity,
)
power_event = Event(
EVENT_STATE_CHANGED,
{
@@ -243,13 +299,22 @@ async def send_max_power_change_event(entity: VersatileThermostat, new_power_max
)
},
)
return await entity._async_max_power_changed(power_event)
await entity._async_max_power_changed(power_event)
if sleep:
await asyncio.sleep(0.1)
async def send_window_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new window event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
window_event = Event(
EVENT_STATE_CHANGED,
{
@@ -268,13 +333,22 @@ async def send_window_change_event(
},
)
ret = await entity._async_windows_changed(window_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_motion_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new motion event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
motion_event = Event(
EVENT_STATE_CHANGED,
{
@@ -293,13 +367,22 @@ async def send_motion_change_event(
},
)
ret = await entity._async_motion_changed(motion_event)
if sleep:
await asyncio.sleep(0.1)
return ret
async def send_presence_change_event(
entity: VersatileThermostat, new_state: bool, old_state: bool, date
entity: VersatileThermostat, new_state: bool, old_state: bool, date, sleep=True
):
"""Sending a new presence event simulating a change on the window state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_state=%s old_state=%s date=%s on %s",
new_state,
old_state,
date,
entity,
)
presence_event = Event(
EVENT_STATE_CHANGED,
{
@@ -318,6 +401,8 @@ async def send_presence_change_event(
},
)
ret = await entity._async_presence_changed(presence_event)
if sleep:
await asyncio.sleep(0.1)
return ret
@@ -334,8 +419,18 @@ async def send_climate_change_event(
new_hvac_action: HVACAction,
old_hvac_action: HVACAction,
date,
sleep=True,
):
"""Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s on %s",
new_hvac_mode,
old_hvac_mode,
new_hvac_action,
old_hvac_action,
date,
entity,
)
climate_event = Event(
EVENT_STATE_CHANGED,
{
@@ -356,4 +451,14 @@ async def send_climate_change_event(
},
)
ret = await entity._async_climate_changed(climate_event)
if sleep:
await asyncio.sleep(0.1)
return ret
def cancel_switchs_cycles(entity: VersatileThermostat):
"""This method will cancel all running cycle on all underlying switch entity"""
if entity._is_over_climate:
return
for under in entity._underlyings:
under._cancel_cycle()

View File

@@ -55,9 +55,9 @@ def skip_notifications_fixture():
def skip_turn_on_off_heater():
"""Skip turning on and off the heater"""
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingEntity.turn_on"
), patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingEntity.turn_off"
):
yield
@@ -87,6 +87,15 @@ def skip_control_heating_fixture():
yield
@pytest.fixture(name="skip_find_underlying_climate")
def skip_find_underlying_climate_fixture():
"""Skip the find_underlying_climate of VersatileThermostat"""
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate"
):
yield
@pytest.fixture(name="skip_hass_states_is_state")
def skip_hass_states_is_state_fixture():
"""Skip the is_state in HomeAssistant"""

View File

@@ -9,9 +9,13 @@ from homeassistant.components.climate.const import ( # pylint: disable=unused-i
from custom_components.versatile_thermostat.const import (
CONF_NAME,
CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_TYPE,
CONF_AC_MODE,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN,
@@ -62,6 +66,21 @@ MOCK_TH_OVER_SWITCH_USER_CONFIG = {
CONF_USE_PRESENCE_FEATURE: True,
}
MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
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,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
}
MOCK_TH_OVER_CLIMATE_USER_CONFIG = {
CONF_NAME: "TheOverClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
@@ -79,6 +98,14 @@ MOCK_TH_OVER_SWITCH_TYPE_CONFIG = {
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
}
MOCK_TH_OVER_4SWITCH_TYPE_CONFIG = {
CONF_HEATER: "switch.mock_4switch0",
CONF_HEATER_2: "switch.mock_4switch1",
CONF_HEATER_3: "switch.mock_4switch2",
CONF_HEATER_4: "switch.mock_4switch3",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
}
MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.1,
@@ -86,6 +113,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
}
MOCK_PRESETS_CONFIG = {

View File

@@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from pytest_homeassistant_custom_component.common import MockConfigEntry
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from ..climate import VersatileThermostat
from ..binary_sensor import (
SecurityBinarySensor,
@@ -21,6 +22,8 @@ from ..binary_sensor import (
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -96,6 +99,8 @@ async def test_security_binary_sensors(
assert security_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_overpowering_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -178,6 +183,8 @@ async def test_overpowering_binary_sensors(
assert overpowering_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -264,6 +271,8 @@ async def test_window_binary_sensors(
assert window_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_motion_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -350,6 +359,8 @@ async def test_motion_binary_sensors(
assert motion_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_presence_binary_sensors(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -432,6 +443,8 @@ async def test_presence_binary_sensors(
assert presence_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_binary_sensors_over_climate_minimal(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -442,7 +455,7 @@ async def test_binary_sensors_over_climate_minimal(
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying,
):
entry = MockConfigEntry(

View File

@@ -8,6 +8,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_56(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -18,7 +20,7 @@ async def test_bug_56(
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=None, # dont find the underlying climate
):
entry = MockConfigEntry(
@@ -53,7 +55,7 @@ async def test_bug_56(
assert entity
# cause the underlying climate was not found
assert entity.is_over_climate is True
assert entity._underlying_climate is None
assert entity.underlying_entity(0)._underlying_climate is None
# Should not failed
entity.update_custom_attributes()
@@ -70,7 +72,7 @@ async def test_bug_56(
# This time the underlying will be found
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying, # dont find the underlying climate
):
# try to call _async_control_heating
@@ -85,6 +87,8 @@ async def test_bug_56(
entity.update_custom_attributes()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_63(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -135,6 +139,8 @@ async def test_bug_63(
# Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64
# Repro case not evident
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_64(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -180,6 +186,8 @@ async def test_bug_64(
assert entity
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_66(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -241,24 +249,26 @@ async def test_bug_66(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
await send_temperature_change_event(entity, 15, now)
try_window_condition = await send_window_change_event(entity, True, False, now)
try_window_condition = await send_window_change_event(
entity, True, False, now, False
)
# simulate the call to try_window_condition
await try_window_condition(None)
assert mock_send_event.call_count == 1
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 2
assert mock_heater_off.call_count >= 1
assert mock_condition.call_count == 1
assert entity.window_state == STATE_ON
@@ -267,13 +277,13 @@ async def test_bug_66(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
event_timestamp = now + timedelta(minutes=1)
@@ -290,13 +300,13 @@ async def test_bug_66(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
try_window_condition = await send_window_change_event(
@@ -313,13 +323,13 @@ async def test_bug_66(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
event_timestamp = now + timedelta(minutes=2)

View File

@@ -6,9 +6,12 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntry
from custom_components.versatile_thermostat.const import DOMAIN
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_show_form(hass: HomeAssistant) -> None:
"""Test that the form is served with no input"""
# Init the API
@@ -24,6 +27,8 @@ async def test_show_form(hass: HomeAssistant) -> None:
assert result["step_id"] == SOURCE_USER
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_states_get):
"""Test the config flow with all thermostat_over_switch features"""
result = await hass.config_entries.flow.async_init(
@@ -121,6 +126,8 @@ async def test_user_config_flow_over_switch(hass: HomeAssistant, skip_hass_state
assert isinstance(result["result"], ConfigEntry)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_states_get):
"""Test the config flow with all thermostat_over_climate features and no additional features"""
result = await hass.config_entries.flow.async_init(
@@ -206,6 +213,8 @@ async def test_user_config_flow_over_climate(hass: HomeAssistant, skip_hass_stat
assert isinstance(result["result"], ConfigEntry)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_window_auto_ok(
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
):
@@ -301,6 +310,8 @@ async def test_user_config_flow_window_auto_ok(
assert isinstance(result["result"], ConfigEntry)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_window_auto_ko(
hass: HomeAssistant, skip_hass_states_get
):
@@ -369,3 +380,94 @@ async def test_user_config_flow_window_auto_ko(
assert result["errors"] == {
"window_sensor_entity_id": "window_open_detection_method"
}
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_user_config_flow_over_4_switches(
hass: HomeAssistant, skip_hass_states_get, skip_control_heating
):
"""Test the config flow with 4 switchs thermostat_over_switch features"""
SOURCE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
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: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_DEVICE_POWER: 1,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
}
TYPE_CONFIG = { # pylint: disable=wildcard-import, invalid-name
CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
}
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=SOURCE_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "type"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=TYPE_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "tpi"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_TH_OVER_SWITCH_TPI_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "presets"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_PRESETS_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "advanced"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_ADVANCED_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert (
result["data"]
== SOURCE_CONFIG
| TYPE_CONFIG
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
| MOCK_PRESETS_CONFIG
| MOCK_ADVANCED_CONFIG
)
assert result["result"]
assert result["result"].domain == DOMAIN
assert result["result"].version == 1
assert result["result"].title == "TheOver4SwitchMockName"
assert isinstance(result["result"], ConfigEntry)

View File

@@ -0,0 +1,407 @@
""" Test the Window management """
import asyncio
from unittest.mock import patch, call, PropertyMock
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta
import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_movement_management_time_not_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
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": 19,
"eco_away_temp": 17,
"comfort_away_temp": 18,
"boost_away_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
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_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state is "on"
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch(
"homeassistant.helpers.condition.state", return_value=False
):
event_timestamp = now - timedelta(minutes=3)
await send_motion_change_event(entity, True, False, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
# Change is not confirmed
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition:
event_timestamp = now - timedelta(minutes=2)
await send_motion_change_event(entity, False, True, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is "off"
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
# Change is not confirmed
assert mock_heater_on.call_count == 0
# Because device is active
assert mock_heater_off.call_count == 1
assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_movement_management_time_enough_and_presence(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
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": 19,
"eco_away_temp": 17,
"comfort_away_temp": 18,
"boost_away_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
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_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, event_timestamp)
assert entity.presence_state is "on"
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=3)
await send_motion_change_event(entity, True, False, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 19
assert entity.motion_state is "on"
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
await send_motion_change_event(entity, False, True, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18
assert entity.motion_state is "off"
assert entity.presence_state is "on"
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
# Because heating is no more necessary
assert mock_heater_off.call_count == 1
assert mock_send_event.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_movement_management_time_enoughand_not_presence(
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Presence management when time is not enough"""
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": 19,
"eco_away_temp": 17.1,
"comfort_away_temp": 18.1,
"boost_away_temp": 19.1,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
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_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
CONF_MOTION_PRESET: "boost",
CONF_NO_MOTION_PRESET: "comfort",
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_ACTIVITY)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet and presence is unknown
assert entity.target_temperature == 18
assert entity.motion_state is None
assert entity.presence_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, event_timestamp)
assert entity.presence_state is "off"
# starts detecting motion
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
), patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=3)
await send_motion_change_event(entity, True, False, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost away mode
assert entity.target_temperature == 19.1
assert entity.motion_state is "on"
assert entity.presence_state is "off"
assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started
assert mock_heater_on.call_count == 1
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# stop detecting motion with confirmation of stop
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
), patch(
"homeassistant.helpers.condition.state", return_value=True
):
event_timestamp = now - timedelta(minutes=2)
await send_motion_change_event(entity, False, True, event_timestamp)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet
assert entity.target_temperature == 18.1
assert entity.motion_state is "off"
assert entity.presence_state is "off"
assert mock_send_event.call_count == 0
# 18.1 starts heating with a low on_percent
assert mock_heater_on.call_count == 1
assert entity.proportional_algorithm.on_percent == 0.11
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0

View File

@@ -0,0 +1,341 @@
""" Test the Multiple switch management """
import asyncio
from unittest.mock import patch, call, ANY
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta
import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_one_switch_cycle(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
):
"""Test that when multiple switch are configured the activation is distributed"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4SwitchMockName",
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: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch1",
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,
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Checks that all heaters are off
with patch(
"homeassistant.core.StateMachine.is_state", return_value=False
) as mock_is_state:
assert entity._is_device_active is False
# Should be call for the Switch
assert mock_is_state.call_count == 1
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
return_value=None,
) as mock_call_later:
await send_ext_temperature_change_event(entity, 5, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be on but because call_later is mocked heater_on is not called
# assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0
# There is no check if active
assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle
assert mock_call_later.call_count == 1
mock_call_later.assert_has_calls(
[
call.call_later(hass, 0.0, ANY),
]
)
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be turned on but is already on but because above we mock call_later the heater is not on. But this time it will be really on
assert mock_heater_on.call_count == 1
# Set another temperature at middle level
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await send_temperature_change_event(entity, 18.1, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The heater is already on cycle. So we wait that the cycle ends and no heater action is done
assert mock_heater_on.call_count == 0
# assert entity.underlying_entity(0)._should_relaunch_control_heating is True
# Simulate the relaunch
await entity.underlying_entity(0)._turn_on_later(None)
# wait restart
await asyncio.sleep(0.1)
assert mock_heater_on.call_count == 1
# TODO normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the end of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await entity.underlying_entity(0)._turn_off_later(None)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
# The heater should be turned off this time
assert mock_heater_off.call_count == 1
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the start of heater on cycle
event_timestamp = now - timedelta(minutes=3)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
) as mock_device_active:
await entity.underlying_entity(0)._turn_on_later(None)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 1
# The heater should be turned off this time
assert mock_heater_off.call_count == 0
# assert entity.underlying_entity(0)._should_relaunch_control_heating is False
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switchs(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
):
"""Test that when multiple switch are configured the activation is distributed"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4SwitchMockName",
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: False,
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,
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Checks that all heaters are off
with patch(
"homeassistant.core.StateMachine.is_state", return_value=False
) as mock_is_state:
assert entity._is_device_active is False
# Should be call for all Switch
assert mock_is_state.call_count == 4
# Set temperature to a low level
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.call_later",
return_value=None,
) as mock_call_later:
await send_ext_temperature_change_event(entity, 5, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be on but because call_later is mocked heater_on is not called
# assert mock_heater_on.call_count == 1
assert mock_heater_on.call_count == 0
# There is no check if active
assert mock_device_active.call_count == 0
# 4 calls dispatched along the cycle
assert mock_call_later.call_count == 4
mock_call_later.assert_has_calls(
[
call.call_later(hass, 0.0, ANY),
call.call_later(hass, 120.0, ANY),
call.call_later(hass, 240.0, ANY),
call.call_later(hass, 360.0, ANY),
]
)
# Set a temperature at middle level
event_timestamp = now - timedelta(minutes=4)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
) as mock_device_active:
await send_temperature_change_event(entity, 18, event_timestamp)
# No special event
assert mock_send_event.call_count == 0
assert mock_heater_off.call_count == 0
# The first heater should be turned on but is already on but because call_later is mocked, it is only turned on here
assert mock_heater_on.call_count == 1

View File

@@ -10,6 +10,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_off(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -81,9 +83,9 @@ async def test_power_management_hvac_off(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.check_overpowering() is True
@@ -96,6 +98,8 @@ async def test_power_management_hvac_off(
assert mock_heater_off.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
@@ -160,9 +164,9 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.check_overpowering() is True
@@ -194,9 +198,9 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_power_change_event(entity, 48, datetime.now())
assert await entity.check_overpowering() is False
@@ -226,6 +230,8 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
assert mock_heater_off.call_count == 0
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_switch(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -278,13 +284,13 @@ async def test_power_management_energy_over_switch(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_temperature_change_event(entity, 15, datetime.now())
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
await send_temperature_change_event(entity, 15, datetime.now())
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
@@ -307,9 +313,9 @@ async def test_power_management_energy_over_switch(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_temperature_change_event(entity, 18, datetime.now())
assert tpi_algo.on_percent == 0.3
@@ -329,9 +335,9 @@ async def test_power_management_energy_over_switch(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_temperature_change_event(entity, 20, datetime.now())
assert tpi_algo.on_percent == 0.0
@@ -350,6 +356,8 @@ async def test_power_management_energy_over_switch(
assert round(entity.total_energy, 2) == round((2.0 + 0.6) * 100 * 5 / 60.0, 2)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_climate(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -357,7 +365,7 @@ async def test_power_management_energy_over_climate(
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying,
):
entry = MockConfigEntry(

View File

@@ -9,6 +9,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the security feature and https://github.com/jmcollin78/versatile_thermostat/issues/49:
1. creates a thermostat and check that security is off
@@ -87,7 +89,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
@@ -134,7 +136,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
await entity.async_set_preset_mode(PRESET_BOOST)
@@ -149,7 +151,7 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = datetime.now()
@@ -185,4 +187,5 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
any_order=True,
)
assert mock_heater_on.call_count == 0
# Heater is now on
assert mock_heater_on.call_count == 1

View File

@@ -26,6 +26,8 @@ from ..sensor import (
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_sensors_over_switch(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -179,7 +181,11 @@ async def test_sensors_over_switch(
)
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
cancel_switchs_cycles(entity)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_sensors_over_climate(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -190,7 +196,7 @@ async def test_sensors_over_climate(
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying,
):
entry = MockConfigEntry(
@@ -314,6 +320,8 @@ async def test_sensors_over_climate(
assert last_ext_temperature_sensor.device_class == SensorDeviceClass.TIMESTAMP
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_sensors_over_climate_minimal(
hass: HomeAssistant,
skip_hass_states_is_state,
@@ -324,7 +332,7 @@ async def test_sensors_over_climate_minimal(
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=the_mock_underlying,
):
entry = MockConfigEntry(

View File

@@ -15,6 +15,8 @@ from ..climate import VersatileThermostat
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
@@ -76,6 +78,8 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_climate type"""
@@ -91,7 +95,7 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.find_underlying_climate",
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
@@ -139,7 +143,78 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call("climate.mock_climate")
mock_find_climate.assert_has_calls(
[call.find_underlying_entity("climate.mock_climate")]
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the normal full start of a thermostat in thermostat_over_switch with 4 switches type"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4SwitchMockName",
unique_id="uniqueId",
data=FULL_4SWITCH_CONFIG,
)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity: VersatileThermostat = find_my_entity("climate.theover4switchmockname")
assert entity
assert entity.name == "TheOver4SwitchMockName"
assert entity._is_over_climate is False
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
PRESET_ACTIVITY,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
assert entity._window_state is None
assert entity._motion_state is None
assert entity._presence_state is None
assert entity._prop_algorithm is not None
assert entity.nb_underlying_entities == 4
# Checks that we have the 4 UnderlyingEntity correctly configured
for idx in range(4):
under = entity.underlying_entity(idx)
assert under is not None
assert isinstance(under, UnderlyingSwitch)
assert under.entity_id == "switch.mock_4switch" + str(idx)
assert under.initial_delay_sec == 8 * 60 / 4 * idx
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)

View File

@@ -3,6 +3,8 @@
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_tpi_calculation(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the TPI calculation"""

View File

@@ -9,6 +9,8 @@ import logging
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_management_time_not_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -66,18 +68,16 @@ async def test_window_management_time_not_enough(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=False
) as mock_condition:
await send_temperature_change_event(entity, 15, datetime.now())
try_window_condition = await send_window_change_event(
entity, True, False, datetime.now()
)
# simulate the call to try_window_condition
await try_window_condition(None)
await send_window_change_event(entity, True, False, datetime.now())
# simulate the call to try_window_condition. No need due to 0 WINDOW_DELAY and sleep after event is sent
# await try_window_condition(None)
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 1
@@ -94,7 +94,11 @@ async def test_window_management_time_not_enough(
await try_window_condition(None)
assert entity.window_state == STATE_OFF
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_management_time_enough(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -148,49 +152,79 @@ async def test_window_management_time_enough(
assert entity.window_state is None
# Open the window, but condition of time is not satisfied and check the thermostat don't turns off
# change temperature to force turning on the heater
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
await send_temperature_change_event(entity, 15, datetime.now())
# Heater shoud turn-on
assert mock_heater_on.call_count >= 1
assert mock_heater_off.call_count == 0
assert mock_send_event.call_count == 0
# Open the window, condition of time is satisfied, check the thermostat and heater turns off
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
await send_temperature_change_event(entity, 15, datetime.now())
try_window_condition = await send_window_change_event(
entity, True, False, datetime.now()
)
# simulate the call to try_window_condition
await try_window_condition(None)
await send_window_change_event(entity, True, False, datetime.now())
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF})]
)
assert mock_heater_on.call_count == 1
# One call in turn_oiff and one call in the control_heating
# Heater should not be on
assert mock_heater_on.call_count == 0
# One call in set_hvac_mode turn_off and one call in the control_heating for security
assert mock_heater_off.call_count == 2
assert mock_condition.call_count == 1
assert entity.hvac_mode is HVACMode.OFF
assert entity.window_state == STATE_ON
# Close the window
try_window_condition = await send_window_change_event(
entity, False, True, datetime.now()
# Close the window
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"homeassistant.helpers.condition.state", return_value=True
) as mock_condition, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
try_function = await send_window_change_event(
entity, False, True, datetime.now(), sleep=False
)
# simulate the call to try_window_condition
await try_window_condition(None)
await try_function(None)
# Wait for initial delay of heater
await asyncio.sleep(0.3)
assert entity.window_state == STATE_OFF
assert mock_heater_on.call_count == 2
assert mock_send_event.call_count == 2
assert mock_heater_on.call_count == 1
assert mock_send_event.call_count == 1
mock_send_event.assert_has_calls(
[
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
call.send_event(
EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.HEAT}
),
@@ -199,7 +233,12 @@ async def test_window_management_time_enough(
)
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
@@ -259,11 +298,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=4)
@@ -281,11 +320,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=3)
@@ -316,11 +355,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
@@ -341,11 +380,11 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
return_value=False,
):
@@ -378,7 +417,12 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
@@ -438,11 +482,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=4)
@@ -459,18 +503,32 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=3)
await send_temperature_change_event(entity, 18, event_timestamp)
await send_temperature_change_event(entity, 18, event_timestamp, sleep=False)
# The heater turns on
assert mock_send_event.call_count == 2
# The heater turns off
mock_send_event.assert_has_calls(
[
call.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}),
call.send_event(
EventType.WINDOW_AUTO_EVENT,
{
"type": "start",
"cause": "slope alert",
"curve_slope": -1.0,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count >= 1
assert entity.last_temperature_slope == -1
@@ -483,11 +541,11 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=False,
):
await asyncio.sleep(0.3)
@@ -502,7 +560,12 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
# Clean the entity
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_window_auto_no_on_percent(
hass: HomeAssistant, skip_hass_states_is_state
):
@@ -564,11 +627,11 @@ async def test_window_auto_no_on_percent(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=4)
@@ -586,11 +649,11 @@ async def test_window_auto_no_on_percent(
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_heater_turn_on"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_underlying_entity_turn_off"
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off, patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._is_device_active",
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
return_value=True,
):
event_timestamp = now - timedelta(minutes=3)
@@ -605,3 +668,6 @@ async def test_window_auto_no_on_percent(
assert entity._window_auto_algo.is_window_close_detected() is False
assert entity.window_auto_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity
entity.remove_thermostat()

View File

@@ -22,12 +22,25 @@
}
},
"type": {
"title": "Linked entity",
"description": "Linked entity attributes",
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater entity id",
"heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch",
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat",
"ac_mode": "AC mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying thermostat entity id"
"climate_entity_id": "Underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode"
}
},
"tpi": {
@@ -44,7 +57,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -91,7 +107,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -142,12 +161,25 @@
}
},
"type": {
"title": "Linked entity",
"description": "Linked entity attributes",
"title": "Linked entities",
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater entity id",
"heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch",
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat",
"ac_mode": "AC mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
"heater_entity2_id": "Optional 2nd Heater entity id. Leave empty if not used",
"heater_entity3_id": "Optional 3rd Heater entity id. Leave empty if not used",
"heater_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying thermostat entity id"
"climate_entity_id": "Underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode"
}
},
"tpi": {
@@ -164,7 +196,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -211,7 +246,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -262,16 +300,5 @@
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Shedding",
"security": "Security",
"none": "Manual"
}
}
}
}
}

View File

@@ -21,12 +21,25 @@
}
},
"type": {
"title": "Entité liée",
"description": "Attributs de l'entité liée",
"title": "Entité(s) liée(s)",
"description": "Attributs de(s) l'entité(s) liée(s)",
"data": {
"heater_entity_id": "Radiateur entity id",
"heater_entity_id": "1er radiateur",
"heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent",
"ac_mode": "AC mode ?"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Thermostat sous-jacent entity id"
"climate_entity_id": "Entity id du thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)"
}
},
"tpi": {
@@ -43,7 +56,10 @@
"data": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost"
"boost_temp": "Température en preset Boost",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
}
},
"window": {
@@ -90,7 +106,10 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
"boost_away_temp": "Température en preset Boost en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
}
},
"advanced": {
@@ -142,12 +161,25 @@
}
},
"type": {
"title": "Entité liée",
"description": "Attributs de l'entité liée",
"title": "Entité(s) liée(s)",
"description": "Attributs de(s) l'entité(s) liée(s)",
"data": {
"heater_entity_id": "Radiateur entity id",
"heater_entity_id": "1er radiateur",
"heater_entity2_id": "2ème radiateur",
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent",
"ac_mode": "AC mode ?"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
"heater_entity2_id": "Optionnel entity id du 2ème radiateur",
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Thermostat sous-jacent entity id"
"climate_entity_id": "Entity id du thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)"
}
},
"tpi": {
@@ -164,7 +196,10 @@
"data": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost"
"boost_temp": "Température en preset Boost",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
}
},
"window": {
@@ -211,7 +246,10 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
"boost_away_temp": "Température en preset Boost en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
}
},
"advanced": {
@@ -262,16 +300,5 @@
}
}
}
},
"state_attributes": {
"_": {
"preset_mode": {
"state": {
"power": "Délestage",
"security": "Sécurité",
"none": "Manuel"
}
}
}
}
}

View File

@@ -0,0 +1,304 @@
{
"title": "Configurazione Versatile Thermostat",
"config": {
"flow_title": "Configurazione Versatile Thermostat",
"step": {
"user": {
"title": "Aggiungi un nuovo Versatile Thermostat",
"description": "Principali parametri obbligatori",
"data": {
"name": "Nome",
"thermostat_type": "Tipologia di termostato",
"temperature_sensor_entity_id": "Entity id sensore temperatura",
"external_temperature_sensor_entity_id": "Entity id sensore temperatura esterna",
"cycle_min": "Durata del ciclo (minuti)",
"temp_min": "Temperatura minima ammessa",
"temp_max": "Temperatura massima ammessa",
"device_power": "Potenza dispositivo (kW)",
"use_window_feature": "Usa il rilevamento della finestra",
"use_motion_feature": "Usa il rilevamento del movimento",
"use_power_feature": "Usa la gestione della potenza",
"use_presence_feature": "Usa il rilevamento della presenza"
}
},
"type": {
"title": "Entità collegate",
"description": "Parametri entità collegate",
"data": {
"heater_entity_id": "Primo riscaldatore",
"heater_entity2_id": "Secondo riscaldatore",
"heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo",
"climate_entity_id": "Termostato sottostante",
"ac_mode": "AC mode ?"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del termostato sottostante",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
}
},
"tpi": {
"title": "TPI",
"description": "Parametri del Time Proportional Integral",
"data": {
"tpi_coef_int": "Coefficiente per il delta della temperatura interna",
"tpi_coef_ext": "Coefficiente per il delta della temperatura esterna"
}
},
"presets": {
"title": "Presets",
"description": "Per ogni preset, impostare la temperatura desiderata (0 per ignorare il preset)",
"data": {
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost",
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
}
},
"window": {
"title": "Gestione della finestra",
"description": "Gestione della finestra aperta.\nLasciare vuoto l'entity_id corrispondente se non utilizzato\nÈ inoltre possibile configurare il rilevamento automatico della finestra aperta in base alla diminuzione della temperatura",
"data": {
"window_sensor_entity_id": "Entity id sensore finestra",
"window_delay": "Ritardo sensore finestra (secondi)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
},
"data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
}
},
"motion": {
"title": "Gestione movimento",
"description": "Gestione sensore movimento. Il preset può cambiare automaticamente a seconda di un rilevamento di movimento\nLasciare vuoto l'entity_id corrispondente se non utilizzato.\nmotion_preset e no_motion_preset devono essere impostati con il nome del preset corrispondente",
"data": {
"motion_sensor_entity_id": "Entity id sensore di movimento",
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
}
},
"power": {
"title": "Gestione dell'energia",
"description": "Parametri di gestione dell'energia.\nInserire la potenza massima disponibile e l'entity_id del sensore che la misura.\nQuindi inserire il consumo del riscaldatore quando è in funzione.\nTutti i parametri devono essere nella stessa unità di misura (kW o W).\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"power_sensor_entity_id": "Entity id sensore potenza",
"max_power_sensor_entity_id": "Entity id sensore di massima potenza",
"power_temp": "Temperatura in caso di distribuzione del carico"
}
},
"presence": {
"title": "Gestione della presenza",
"description": "Parametri di gestione della presenza.\nInserire un sensore di presenza (true se è presente qualcuno).\nQuindi specificare il preset o la riduzione di temperatura da utilizzare quando il sensore di presenza è in false.\nSe è impostato il preset, la riduzione non sarà utilizzata.\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
}
},
"advanced": {
"title": "Parametri avanzati",
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
"data": {
"minimal_activation_delay": "Ritardo minimo di accensione",
"security_delay_min": "Ritardo di sicurezza (in minuti)",
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
},
"data_description": {
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
}
}
},
"error": {
"unknown": "Errore inatteso",
"unknown_entity": "Entity id sconosciuta",
"window_open_detection_method": "Può essere utilizzato un solo metodo di rilevamento finestra aperta. Utilizzare il sensore od il rilevamento automatico ma non entrambi"
},
"abort": {
"already_configured": "Il dispositivo è già configurato"
}
},
"options": {
"flow_title": "Configurazione di Versatile Thermostat",
"step": {
"user": {
"title": "Aggiungi un nuovo Versatile Thermostat",
"description": "Principali attributi obbligatori",
"data": {
"name": "Nome",
"thermostat_type": "Tipologia termostato",
"temperature_sensor_entity_id": "Entity id sensore di temperatura",
"external_temperature_sensor_entity_id": "Entity id sensore temperatura esterna",
"cycle_min": "Durata del ciclo (minuti)",
"temp_min": "Temperatura minima consentita",
"temp_max": "Temperatura massima consentita",
"device_power": "Potenza dispositivo (kW)",
"use_window_feature": "Usa il rilevamento della finestra",
"use_motion_feature": "Usa il rilevamento del movimento",
"use_power_feature": "Usa la gestione della potenza",
"use_presence_feature": "Usa il rilevamento della presenza"
}
},
"type": {
"title": "Entità collegate",
"description": "Attributi delle entità collegate",
"data": {
"heater_entity_id": "Interruttore riscaldatore",
"heater_entity2_id": "Secondo interruttore riscaldatore",
"heater_entity3_id": "Terzo interruttore riscaldatore",
"heater_entity4_id": "Quarto interruttore riscaldatore",
"proportional_function": "Algoritmo",
"climate_entity_id": "Termostato sottostante",
"ac_mode": "AC mode ?"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
"heater_entity2_id": "Entity id del secondo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del termostato sottostante",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
}
},
"tpi": {
"title": "TPI",
"description": "Parametri del Time Proportional Integral",
"data": {
"tpi_coef_int": "Coefficiente per il delta della temperatura interna",
"tpi_coef_ext": "Coefficiente per il delta della temperatura esterna"
}
},
"presets": {
"title": "Presets",
"description": "Per ogni preset, impostare la temperatura desiderata (0 per ignorare il preset)",
"data": {
"eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost",
"eco_ac_temp": "Temperatura nel preset Eco (AC mode)",
"comfort_ac_temp": "Temperatura nel preset Comfort (AC mode)",
"boost_ac_temp": "Temperatura nel preset Boost (AC mode)"
}
},
"window": {
"title": "Gestione della finestra",
"description": "Gestione della finestra aperta.\nLasciare vuoto l'entity_id corrispondente se non utilizzato\nÈ inoltre possibile configurare il rilevamento automatico della finestra aperta in base alla diminuzione della temperatura",
"data": {
"window_sensor_entity_id": "Entity id sensore finestra",
"window_delay": "Ritardo sensore finestra (secondi)",
"window_auto_open_threshold": "Soglia di diminuzione della temperatura per il rilevamento automatico della finestra aperta (in °/min)",
"window_auto_close_threshold": "Soglia di aumento della temperatura per la fine del rilevamento automatico (in °/min)",
"window_auto_max_duration": "Durata massima del rilevamento automatico della finestra aperta (in min)"
},
"data_description": {
"window_sensor_entity_id": "Lasciare vuoto se non deve essere utilizzato alcun sensore finestra",
"window_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"window_auto_open_threshold": "Valore consigliato: tra 0.05 e 0.1. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_close_threshold": "Valore consigliato: 0. Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato",
"window_auto_max_duration": "Valore consigliato: 60 (un'ora). Lasciare vuoto se il rilevamento automatico della finestra aperta non è utilizzato"
}
},
"motion": {
"title": "Gestione movimento",
"description": "Gestione sensore movimento. Il preset può cambiare automaticamente a seconda di un rilevamento di movimento\nLasciare vuoto l'entity_id corrispondente se non utilizzato.\nmotion_preset e no_motion_preset devono essere impostati con il nome del preset corrispondente",
"data": {
"motion_sensor_entity_id": "Entity id sensore di movimento",
"motion_delay": "Ritardo in secondi prima che il rilevamento del sensore sia preso in considerazione",
"motion_preset": "Preset da utilizzare quando viene rilevato il movimento",
"no_motion_preset": "Preset da utilizzare quando non viene rilevato il movimento"
}
},
"power": {
"title": "Gestione dell'energia",
"description": "Parametri di gestione dell'energia.\nInserire la potenza massima disponibile e l'entity_id del sensore che la misura.\nQuindi inserire il consumo del riscaldatore quando è in funzione.\nTutti i parametri devono essere nella stessa unità di misura (kW o W).\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"power_sensor_entity_id": "Entity id sensore potenza",
"max_power_sensor_entity_id": "Entity id sensore di massima potenza",
"power_temp": "Temperatura in caso di distribuzione del carico"
}
},
"presence": {
"title": "Gestione della presenza",
"description": "Parametri di gestione della presenza.\nInserire un sensore di presenza (true se è presente qualcuno).\nQuindi specificare il preset o la riduzione di temperatura da utilizzare quando il sensore di presenza è in false.\nSe è impostato il preset, la riduzione non sarà utilizzata.\nLasciare vuoto l'entity_id corrispondente se non utilizzato.",
"data": {
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza",
"eco_ac_away_temp": "Temperatura al preset Eco in caso d'assenza (AC mode)",
"comfort_ac_away_temp": "Temperatura al preset Comfort in caso d'assenza (AC mode)",
"boost_ac_away_temp": "Temperatura al preset Boost in caso d'assenza (AC mode)"
}
},
"advanced": {
"title": "Parametri avanzati",
"description": "Configurazione avanzata dei parametri. Lasciare i valori predefiniti se non conoscete cosa state modificando.\nQuesti parametri possono determinare una pessima gestione della temperatura e della potenza.",
"data": {
"minimal_activation_delay": "Ritardo minimo di accensione",
"security_delay_min": "Ritardo di sicurezza (in minuti)",
"security_min_on_percent": "Percentuale minima di potenza per la modalità di sicurezza",
"security_default_on_percent": "Percentuale di potenza per la modalità di sicurezza"
},
"data_description": {
"minimal_activation_delay": "Ritardo in secondi al di sotto del quale l'apparecchiatura non verrà attivata",
"security_delay_min": "Ritardo massimo consentito in minuti tra due misure di temperatura. Al di sopra di questo ritardo, il termostato passerà allo stato di sicurezza",
"security_min_on_percent": "Soglia percentuale minima di riscaldamento al di sotto della quale il preset di sicurezza non verrà mai attivato",
"security_default_on_percent": "Valore percentuale predefinito della potenza di riscaldamento nella modalità di sicurezza. Impostare a 0 per spegnere il riscaldatore nella modalità di sicurezza"
}
}
},
"error": {
"unknown": "Errore inatteso",
"unknown_entity": "Entity id sconosciuta",
"window_open_detection_method": "Può essere utilizzato un solo metodo di rilevamento finestra aperta. Utilizzare il sensore od il rilevamento automatico ma non entrambi"
},
"abort": {
"already_configured": "Il dispositivo è già configurato"
}
},
"selector": {
"thermostat_type": {
"options": {
"thermostat_over_switch": "Termostato su un interruttore",
"thermostat_over_climate": "Termostato sopra un altro termostato"
}
}
},
"entity": {
"climate": {
"versatile_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"power": "Ripartizione",
"security": "Sicurezza",
"none": "Manuale"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,621 @@
""" Underlying entities classes """
import logging
from typing import Any
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.exceptions import ServiceNotFound
from homeassistant.backports.enum import StrEnum
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
DOMAIN as CLIMATE_DOMAIN,
HVACMode,
HVACAction,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HUMIDITY,
SERVICE_SET_SWING_MODE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_call_later
from .const import UnknownEntity
_LOGGER = logging.getLogger(__name__)
# remove this
# _LOGGER.setLevel(logging.DEBUG)
class UnderlyingEntityType(StrEnum):
"""All underlying device type"""
# A switch
SWITCH = "switch"
# a climate
CLIMATE = "climate"
class UnderlyingEntity:
"""Represent a underlying device which could be a switch or a climate"""
_hass: HomeAssistant
# Cannot import VersatileThermostat due to circular reference
_thermostat: Any
_entity_id: str
_type: UnderlyingEntityType
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
entity_type: UnderlyingEntityType,
entity_id: str,
) -> None:
"""Initialize the underlying entity"""
self._hass = hass
self._thermostat = thermostat
self._type = entity_type
self._entity_id = entity_id
def __str__(self):
return str(self._thermostat) + "-" + self._entity_id
@property
def entity_id(self):
"""The entiy id represented by this class"""
return self._entity_id
@property
def entity_type(self) -> UnderlyingEntityType:
"""The entity type represented by this class"""
return self._type
@property
def is_initialized(self) -> bool:
"""True if the underlying is initialized"""
return True
def startup(self):
"""Startup the Entity"""
return
async def set_hvac_mode(self, hvac_mode: HVACMode):
"""Set the HVACmode"""
return
@property
def is_device_active(self) -> bool | None:
"""If the toggleable device is currently active."""
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):
"""Set the target temperature"""
return
def remove_entity(self):
"""Remove the underlying entity"""
return
# override to be able to mock the call
def call_later(
self, hass: HomeAssistant, delay_sec: int, called_method
) -> CALLBACK_TYPE:
"""Call the method after a delay"""
return async_call_later(hass, delay_sec, called_method)
class UnderlyingSwitch(UnderlyingEntity):
"""Represent a underlying switch"""
_initialDelaySec: int
_on_time_sec: int
_off_time_sec: int
_hvac_mode: HVACMode
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
switch_entity_id: str,
initial_delay_sec: int,
) -> None:
"""Initialize the underlying switch"""
super().__init__(
hass=hass,
thermostat=thermostat,
entity_type=UnderlyingEntityType.SWITCH,
entity_id=switch_entity_id,
)
self._initial_delay_sec = initial_delay_sec
self._async_cancel_cycle = None
self._should_relaunch_control_heating = False
self._on_time_sec = 0
self._off_time_sec = 0
self._hvac_mode = None
@property
def initial_delay_sec(self):
"""The initial delay for this class"""
return self._initial_delay_sec
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode. Returns true if something have change"""
if hvac_mode == HVACMode.OFF:
if self.is_device_active:
await self.turn_off()
self._cancel_cycle()
if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode
return True
else:
return False
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
return self._hass.states.is_state(self._entity_id, STATE_ON)
async def check_initial_state(self, hvac_mode: HVACMode):
"""Prevent the heater to be on but thermostat is off"""
if hvac_mode == HVACMode.OFF and self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is OFF, but the switch device is ON. Turning off device %s",
self,
self._entity_id,
)
await self.turn_off()
async def start_cycle(
self,
hvac_mode: HVACMode,
on_time_sec: int,
off_time_sec: int,
force=False,
):
"""Starting cycle for switch"""
_LOGGER.debug(
"%s - Starting new cycle hvac_mode=%s on_time_sec=%d off_time_sec=%d force=%s",
self,
hvac_mode,
on_time_sec,
off_time_sec,
force,
)
self._on_time_sec = on_time_sec
self._off_time_sec = off_time_sec
self._hvac_mode = hvac_mode
# Cancel eventual previous cycle if any
if self._async_cancel_cycle is not None:
if force:
_LOGGER.debug("%s - we force a new cycle", self)
self._cancel_cycle()
else:
_LOGGER.debug(
"%s - A previous cycle is alredy running and no force -> waits for its end",
self,
)
# self._should_relaunch_control_heating = True
_LOGGER.debug("%s - End of cycle (2)", self)
return
# If we should heat, starts the cycle with delay
if self._hvac_mode == HVACMode.HEAT and on_time_sec > 0:
# Starts the cycle after the initial delay
self._async_cancel_cycle = self.call_later(
self._hass, self._initial_delay_sec, self._turn_on_later
)
_LOGGER.debug("%s - _async_cancel_cycle=%s", self, self._async_cancel_cycle)
# if we not heat but device is active
elif self.is_device_active:
_LOGGER.info(
"%s - stop heating (2) for %d min %d sec",
self,
off_time_sec // 60,
off_time_sec % 60,
)
await self.turn_off()
else:
_LOGGER.debug("%s - nothing to do", self)
def _cancel_cycle(self):
"""Cancel the cycle"""
if self._async_cancel_cycle:
self._async_cancel_cycle()
self._async_cancel_cycle = None
_LOGGER.debug("%s - Stopping cycle during calculation", self)
async def _turn_on_later(self, _):
"""Turn the heater on after a delay"""
_LOGGER.debug(
"%s - calling turn_on_later hvac_mode=%s, should_relaunch_later=%s off_time_sec=%d",
self,
self._hvac_mode,
self._should_relaunch_control_heating,
self._on_time_sec,
)
self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
if self.is_device_active:
await self.turn_off()
return
if await self._thermostat.check_overpowering():
_LOGGER.debug("%s - End of cycle (3)", self)
return
# Security mode could have change the on_time percent
await self._thermostat.check_security()
time = self._on_time_sec
action_label = "start"
# if self._should_relaunch_control_heating:
# _LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
# self._should_relaunch_control_heating = False
# # self.hass.create_task(self._async_control_heating())
# await self.start_cycle(
# self._hvac_mode, self._on_time_sec, self._off_time_sec
# )
# _LOGGER.debug("%s - End of cycle (3)", self)
# return
if time > 0:
_LOGGER.info(
"%s - %s heating for %d min %d sec",
self,
action_label,
time // 60,
time % 60,
)
await self.turn_on()
else:
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
self._async_cancel_cycle = self.call_later(
self._hass,
time,
self._turn_off_later,
)
async def _turn_off_later(self, _):
"""Turn the heater off and call the next cycle after the delay"""
_LOGGER.debug(
"%s - calling turn_off_later hvac_mode=%s, should_relaunch_later=%s off_time_sec=%d",
self,
self._hvac_mode,
self._should_relaunch_control_heating,
self._off_time_sec,
)
self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
if self.is_device_active:
await self.turn_off()
return
action_label = "stop"
# if self._should_relaunch_control_heating:
# _LOGGER.debug("Don't %s cause a cycle have to be relaunch", action_label)
# self._should_relaunch_control_heating = False
# # self.hass.create_task(self._async_control_heating())
# await self.start_cycle(
# self._hvac_mode, self._on_time_sec, self._off_time_sec
# )
# _LOGGER.debug("%s - End of cycle (3)", self)
# return
time = self._off_time_sec
if time > 0:
_LOGGER.info(
"%s - %s heating for %d min %d sec",
self,
action_label,
time // 60,
time % 60,
)
await self.turn_off()
else:
_LOGGER.debug("%s - No action on heater cause duration is 0", self)
self._async_cancel_cycle = self.call_later(
self._hass,
time,
self._turn_on_later,
)
# increment energy at the end of the cycle
self._thermostat.incremente_energy()
def remove_entity(self):
"""Remove the entity after stopping its cycle"""
self._cancel_cycle()
class UnderlyingClimate(UnderlyingEntity):
"""Represent a underlying climate"""
_underlying_climate: ClimateEntity
def __init__(
self,
hass: HomeAssistant,
thermostat: Any,
climate_entity_id: str,
) -> None:
"""Initialize the underlying climate"""
super().__init__(
hass=hass,
thermostat=thermostat,
entity_type=UnderlyingEntityType.CLIMATE,
entity_id=climate_entity_id,
)
self._underlying_climate = None
def find_underlying_climate(self) -> ClimateEntity:
"""Find the underlying climate entity"""
component: EntityComponent[ClimateEntity] = self._hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if self.entity_id == entity.entity_id:
return entity
return None
def startup(self):
"""Startup the Entity"""
# Get the underlying climate
self._underlying_climate = self.find_underlying_climate()
if self._underlying_climate:
_LOGGER.info(
"%s - The underlying climate entity: %s have been succesfully found",
self,
self._underlying_climate,
)
else:
_LOGGER.error(
"%s - Cannot find the underlying climate entity: %s. Thermostat will not be operational",
self,
self.entity_id,
)
# #56 keep the over_climate and try periodically to find the underlying climate
# self._is_over_climate = False
raise UnknownEntity(f"Underlying entity {self.entity_id} not found")
return
@property
def is_initialized(self) -> bool:
"""True if the underlying climate was found"""
return self._underlying_climate is not None
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
"""Set the HVACmode of the underlying climate. Returns true if something have change"""
if not self.is_initialized:
return False
data = {ATTR_ENTITY_ID: self._entity_id, "hvac_mode": hvac_mode}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
data,
)
return True
@property
def is_device_active(self):
"""If the toggleable device is currently active."""
if self.is_initialized:
return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
HVACAction.IDLE,
HVACAction.OFF,
]
else:
return None
async def set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"fan_mode": fan_mode,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
data,
)
async def set_humidity(self, humidity: int):
"""Set new target humidity."""
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"humidity": humidity,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HUMIDITY,
data,
)
async def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"swing_mode": swing_mode,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE,
data,
)
async def set_temperature(self, temperature, max_temp, min_temp):
"""Set the target temperature"""
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"temperature": temperature,
"target_temp_high": max_temp,
"target_temp_low": min_temp,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
data,
)
@property
def hvac_action(self) -> HVACAction | None:
"""Get the hvac action of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.hvac_action
@property
def hvac_mode(self) -> HVACMode | None:
"""Get the hvac mode of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.hvac_mode
@property
def fan_mode(self) -> str | None:
"""Get the fan_mode of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.fan_mode
@property
def swing_mode(self) -> str | None:
"""Get the swing_mode of the underlying"""
if not self.is_initialized:
return None
return self._underlying_climate.swing_mode
@property
def supported_features(self) -> ClimateEntityFeature:
"""Get the supported features of the climate"""
if not self.is_initialized:
return ClimateEntityFeature.TARGET_TEMPERATURE
return self._underlying_climate.supported_features
@property
def hvac_modes(self) -> list[HVACMode]:
"""Get the hvac_modes"""
if not self.is_initialized:
return []
return self._underlying_climate.hvac_modes
@property
def fan_modes(self) -> list[str]:
"""Get the fan_modes"""
if not self.is_initialized:
return []
return self._underlying_climate.fan_modes
@property
def swing_modes(self) -> list[str]:
"""Get the swing_modes"""
if not self.is_initialized:
return []
return self._underlying_climate.swing_modes
@property
def temperature_unit(self) -> str:
"""Get the temperature_unit"""
if not self.is_initialized:
return UnitOfTemperature.CELSIUS
return self._underlying_climate.temperature_unit
@property
def target_temperature_step(self) -> float:
"""Get the target_temperature_step"""
if not self.is_initialized:
return 1
return self._underlying_climate.target_temperature_step
@property
def target_temperature_high(self) -> float:
"""Get the target_temperature_high"""
if not self.is_initialized:
return 30
return self._underlying_climate.target_temperature_high
@property
def target_temperature_low(self) -> float:
"""Get the target_temperature_low"""
if not self.is_initialized:
return 15
return self._underlying_climate.target_temperature_low
@property
def is_aux_heat(self) -> bool:
"""Get the is_aux_heat"""
if not self.is_initialized:
return False
return self._underlying_climate.is_aux_heat
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_on()
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_off()

View File

@@ -3,5 +3,5 @@
"content_in_root": false,
"render_readme": true,
"hide_default_branch": false,
"homeassistant": "2022.2.0"
"homeassistant": "2023.7.3"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB