Compare commits

..

22 Commits
3.2.5 ... 3.5.0

Author SHA1 Message Date
Jean-Marc Collin
ef994e300b Documentation 2023-10-08 01:29:25 +02:00
Jean-Marc Collin
72c4105bbd Issue #113 - Add multi thermostat over climate 2023-10-08 01:23:25 +02:00
Jean-Marc Collin
79eb4a0a0d Enhance documentation. 2023-10-07 18:44:39 +02:00
Jean-Marc Collin
b032198c66 Issue #103 - expose preset temp pour AC mode 2023-10-07 18:16:20 +02:00
Jean-Marc Collin
487c118b44 Issue #101 - Use target temperature of underlying if changed 2023-10-07 17:54:50 +02:00
Jean-Marc Collin
e29ff0568b Issue #82 - unavailable climate goes into security mode 2023-10-07 10:49:40 +02:00
Jean-Marc Collin
814e4d3b83 Last HA versions 2023-10-03 08:09:12 +02:00
felix schwenzel
abb6531f49 Update climate.py (#112)
without these parenthesis the `action` and therefore returned `hvac_action` seems to be `True` instead of i.e. `heating`
(did not see that when using an underlying homekit devices climate device, but when the underlying was a generic thermostat)
2023-10-03 08:06:52 +02:00
Jean-Marc Collin
f970c18eaf Issue #100: compatibility with HA 2023.9.0 2023-09-08 08:48:09 +02:00
Jean-Marc Collin
af51ef62e0 Issue #99 : over climate VTherm a regulated by the device itself and should not goes into security 2023-08-30 09:06:26 +02:00
Jean-Marc Collin
b38fbd9d78 Issue #99 - security mode toggling 100 times within 2 minutes 2023-08-27 18:06:43 +02:00
Jean-Marc Collin
6e8e72e343 FIX Service name Github error 2023-08-16 22:30:46 +02:00
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
21 changed files with 1120 additions and 457 deletions

View File

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

View File

@@ -8,7 +8,7 @@
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-). > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-).
- [Merci pour la bière buymecoffee: https://www.buymeacoffee.com/jmcollin78](#merci-pour-la-bière-buymecoffee-httpswwwbuymeacoffeecomjmcollin78) - [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser) - [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
- [Pourquoi une nouvelle implémentation du thermostat ?](#pourquoi-une-nouvelle-implémentation-du-thermostat-) - [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-) - [Comment installer cet incroyable Thermostat Versatile ?](#comment-installer-cet-incroyable-thermostat-versatile-)
@@ -54,6 +54,9 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_ > ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_
> * **Release 3.5**: Plusieurs thermostats sont possibles en "thermostat over climate" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fix et exposition des preset temperatures pour le mode AC [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
> * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées) > * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées)
> * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto) > * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto)
> * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard. > * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard.

View File

@@ -53,7 +53,10 @@
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features. This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
>![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_ >![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_
> * **Release 3.2**: added 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.5**: Multiple thermostats when using "thermostat over another thermostat" mode [#113](https://github.com/jmcollin78/versatile_thermostat/issues/113)
> * **Release 3.4**: bug fixes and expose preset temperatures for AC mode [#103](https://github.com/jmcollin78/versatile_thermostat/issues/103)
> * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
> * **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) > * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode)
> * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard. > * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard.
> * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat. > * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat.
@@ -67,15 +70,15 @@ Many thanks to @salabur, @pvince83 and @bergoglio for the beers. It's very pleas
# When to use / not use # When to use / not use
This thermostat can control 2 types of equipment: 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: 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:
has. equipment such as a radiator (a ```switch``` or equivalent), - an equipment such as a radiator (a ```switch``` or equivalent),
b. a temperature probe for the room (or an input_number), - a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one) - an external temperature sensor (think about weather integration if you don't have one)
2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). For this type of thermostat, the minimum configuration requires: 2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). For this type of thermostat, the minimum configuration requires:
has. equipment such as air conditioning which is controlled by its own ```climate``` type entity, - an equipment such as air conditioning which is controlled by its own ```climate``` type entity,
b. a temperature probe for the room (or an input_number), - a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one) - an external temperature sensor (think about weather integration if you don't have one)
The ```thermostat_over_climate``` type allows you to add all the functionality provided by VersatileThermostat to your existing equipment. The climate VersatileThermostat entity will control your climate entity, turning it off if the windows are open, switching it to Eco mode if no one is present, etc. See [here](#why-a-new-implementation-of-the-thermostat). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself. The ```thermostat_over_climate``` type allows you to add all the functionality provided by VersatileThermostat to your existing equipment. The climate VersatileThermostat entity will control your existing climate entity, turning it off if the windows are open, switching it to Eco mode if no one is present, etc. See [here](#why-a-new-implementation-of-the-thermostat). For this type of thermostat, any heating cycles are controlled by the underlying climate entity and not by the Versatile Thermostat itself.
# Why another thermostat implementation ? # Why another thermostat implementation ?
@@ -217,6 +220,7 @@ And that's all ! your thermostat will turn off when the windows are open and tur
2. If you don't have a window/door sensor in your room, just leave the sensor entity id blank, 2. If you don't have a window/door sensor in your room, just leave the sensor entity id blank,
3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time, 3. **Only one mode is allowed**. You cannot configure a thermostat with a sensor and automatic detection. The 2 modes may contradict each other, it is not possible to have the 2 modes at the same time,
4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...) 4. It is not recommended to use the automatic mode for equipment subject to frequent and normal temperature variations (corridors, open areas, ...)
## Configure the activity mode or motion detection ## Configure the activity mode or motion detection
If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there: If you choose the ```Motion management``` feature, lick on 'Validate' on the previous page and you will get there:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true) ![image](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/config-motion.png?raw=true)

View File

@@ -18,7 +18,7 @@ from homeassistant.core import (
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.reload import async_setup_reload_service
@@ -100,6 +100,8 @@ from .const import (
CONF_DEVICE_POWER, CONF_DEVICE_POWER,
CONF_PRESETS, CONF_PRESETS,
CONF_PRESETS_AWAY, CONF_PRESETS_AWAY,
CONF_PRESETS_WITH_AC,
CONF_PRESETS_AWAY_WITH_AC,
CONF_CYCLE_MIN, CONF_CYCLE_MIN,
CONF_PROP_FUNCTION, CONF_PROP_FUNCTION,
CONF_TPI_COEF_INT, CONF_TPI_COEF_INT,
@@ -127,10 +129,15 @@ from .const import (
# CONF_THERMOSTAT_SWITCH, # CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_CLIMATE, CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_AC_MODE,
UnknownEntity, UnknownEntity,
EventType, EventType,
ATTR_MEAN_POWER_CYCLE, ATTR_MEAN_POWER_CYCLE,
ATTR_TOTAL_ENERGY, ATTR_TOTAL_ENERGY,
PRESET_AC_SUFFIX,
) )
from .underlyings import UnderlyingSwitch, UnderlyingClimate, UnderlyingEntity from .underlyings import UnderlyingSwitch, UnderlyingClimate, UnderlyingEntity
@@ -179,7 +186,7 @@ async def async_setup_entry(
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_PRESET_TEMPERATURE, SERVICE_SET_PRESET_TEMPERATURE,
{ {
vol.Required("preset"): vol.In(CONF_PRESETS), vol.Required("preset"): vol.In(CONF_PRESETS_WITH_AC),
vol.Optional("temperature"): vol.Coerce(float), vol.Optional("temperature"): vol.Coerce(float),
vol.Optional("temperature_away"): vol.Coerce(float), vol.Optional("temperature_away"): vol.Coerce(float),
}, },
@@ -289,9 +296,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self, self,
entry_infos, entry_infos,
) )
self._ac_mode = entry_infos.get(CONF_AC_MODE) == True
# convert entry_infos into usable attributes # convert entry_infos into usable attributes
presets = {} presets = {}
for key, value in CONF_PRESETS.items(): items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
for key, value in items:
_LOGGER.debug("looking for key=%s, value=%s", key, value) _LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos: if value in entry_infos:
presets[key] = entry_infos.get(value) presets[key] = entry_infos.get(value)
@@ -299,7 +309,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug("value %s not found in Entry", value) _LOGGER.debug("value %s not found in Entry", value)
presets_away = {} presets_away = {}
for key, value in CONF_PRESETS_AWAY.items(): items = (
CONF_PRESETS_AWAY_WITH_AC.items()
if self._ac_mode
else CONF_PRESETS_AWAY.items()
)
for key, value in items:
_LOGGER.debug("looking for key=%s, value=%s", key, value) _LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry_infos: if value in entry_infos:
presets_away[key] = entry_infos.get(value) presets_away[key] = entry_infos.get(value)
@@ -316,16 +331,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN) self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
# Initialize underlying entities # Initialize underlying entities
self._underlyings = []
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE) self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE: if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
self._is_over_climate = True self._is_over_climate = True
self._underlyings.append( for climate in [CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4]:
UnderlyingClimate( if entry_infos.get(climate):
hass=self._hass, self._underlyings.append(
thermostat=self, UnderlyingClimate(
climate_entity_id=entry_infos.get(CONF_CLIMATE), hass=self._hass,
) thermostat=self,
) climate_entity_id=entry_infos.get(climate),
)
)
else: else:
lst_switches = [entry_infos.get(CONF_HEATER)] lst_switches = [entry_infos.get(CONF_HEATER)]
if entry_infos.get(CONF_HEATER_2): if entry_infos.get(CONF_HEATER_2):
@@ -336,7 +354,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
lst_switches.append(entry_infos.get(CONF_HEATER_4)) lst_switches.append(entry_infos.get(CONF_HEATER_4))
delta_cycle = self._cycle_min * 60 / len(lst_switches) delta_cycle = self._cycle_min * 60 / len(lst_switches)
self._underlyings = []
for idx, switch in enumerate(lst_switches): for idx, switch in enumerate(lst_switches):
self._underlyings.append( self._underlyings.append(
UnderlyingSwitch( UnderlyingSwitch(
@@ -394,10 +411,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._presence_on = self._presence_sensor_entity_id is not None self._presence_on = self._presence_sensor_entity_id is not None
# if self.ac_mode: -> MODE_COOL should be better to use thermostat_over_climate type if self._ac_mode:
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF] self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
# else: else:
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF] self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
self._unit = self._hass.config.units.temperature_unit self._unit = self._hass.config.units.temperature_unit
# Will be restored if possible # Will be restored if possible
@@ -437,7 +454,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._target_temp = None self._target_temp = None
self._saved_target_temp = PRESET_NONE self._saved_target_temp = PRESET_NONE
self._humidity = None self._humidity = None
self._ac_mode = False
self._fan_mode = None self._fan_mode = None
self._swing_mode = None self._swing_mode = None
self._cur_temp = None self._cur_temp = None
@@ -494,13 +510,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if len(presets): if len(presets):
self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE
for key, val in presets.items(): for key, val in CONF_PRESETS.items():
if val != 0.0: if val != 0.0:
self._attr_preset_modes.append(key) self._attr_preset_modes.append(key)
# self._attr_preset_modes = (
# [PRESET_NONE] + list(presets.keys()) + [PRESET_ACTIVITY]
# )
_LOGGER.debug( _LOGGER.debug(
"After adding presets, preset_modes to %s", self._attr_preset_modes "After adding presets, preset_modes to %s", self._attr_preset_modes
) )
@@ -609,11 +622,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Ingore this error which is possible if underlying climate is not found temporary # Ingore this error which is possible if underlying climate is not found temporary
pass pass
async def remove_thermostat(self): def remove_thermostat(self):
"""Called when the thermostat will be removed""" """Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self) _LOGGER.info("%s - Removing thermostat", self)
for under in self._underlyings: for under in self._underlyings:
await under.remove_entity() under.remove_entity()
async def async_startup(self): async def async_startup(self):
"""Triggered on startup, used to get old state and set internal states accordingly""" """Triggered on startup, used to get old state and set internal states accordingly"""
@@ -958,7 +971,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# else OFF # else OFF
one_idle = False one_idle = False
for under in self._underlyings: for under in self._underlyings:
if action := under.hvac_action not in [HVACAction.IDLE, HVACAction.OFF]: if (action := under.hvac_action) not in [HVACAction.IDLE, HVACAction.OFF]:
return action return action
if under.hvac_action == HVACAction.IDLE: if under.hvac_action == HVACAction.IDLE:
one_idle = True one_idle = True
@@ -1207,6 +1220,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await under.set_hvac_mode(hvac_mode) or need_control_heating await under.set_hvac_mode(hvac_mode) or need_control_heating
) )
# If AC is on maybe we have to change the temperature in force mode
if self._ac_mode:
await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
if need_control_heating and sub_need_control_heating: if need_control_heating and sub_need_control_heating:
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
@@ -1286,13 +1303,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) # in security just keep the current target temperature, the thermostat should be off ) # in security just keep the current target temperature, the thermostat should be off
if preset_mode == PRESET_POWER: if preset_mode == PRESET_POWER:
return self._power_temp return self._power_temp
elif self._presence_on is False or self._presence_state in [
STATE_ON,
STATE_HOME,
]:
return self._presets[preset_mode]
else: else:
return self._presets_away[self.get_preset_away_name(preset_mode)] # Select _ac presets if in COOL Mode
if self._ac_mode and self._hvac_mode == HVACMode.COOL:
preset_mode = preset_mode + PRESET_AC_SUFFIX
if self._presence_on is False or self._presence_state in [
STATE_ON,
STATE_HOME,
]:
return self._presets[preset_mode]
else:
return self._presets_away[self.get_preset_away_name(preset_mode)]
def get_preset_away_name(self, preset_mode): def get_preset_away_name(self, preset_mode):
"""Get the preset name in away mode (when presence is off)""" """Get the preset name in away mode (when presence is off)"""
@@ -1565,6 +1587,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if not new_state: if not new_state:
return return
changes = False
new_hvac_mode = new_state.state
old_state = event.data.get("old_state") old_state = event.data.get("old_state")
old_hvac_action = ( old_hvac_action = (
old_state.attributes.get("hvac_action") old_state.attributes.get("hvac_action")
@@ -1577,16 +1602,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
else None else None
) )
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
#if self._hvac_mode == HVACMode.OFF and new_hvac_mode == HVACMode.COOL and new_hvac_action == HVACAction.IDLE:
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
# new_hvac_mode = HVACMode.OFF
_LOGGER.info( _LOGGER.info(
"%s - Underlying climate changed. Event.new_state is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s", "%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
self, self,
new_state, new_hvac_mode,
self._hvac_mode, self._hvac_mode,
new_hvac_action, new_hvac_action,
old_hvac_action, old_hvac_action,
) )
if new_state.state in [ if new_hvac_mode in [
HVACMode.OFF, HVACMode.OFF,
HVACMode.HEAT, HVACMode.HEAT,
HVACMode.COOL, HVACMode.COOL,
@@ -1594,8 +1624,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
HVACMode.DRY, HVACMode.DRY,
HVACMode.AUTO, HVACMode.AUTO,
HVACMode.FAN_ONLY, HVACMode.FAN_ONLY,
]: None
self._hvac_mode = new_state.state ] and self._hvac_mode != new_hvac_mode:
changes = True
self._hvac_mode = new_hvac_mode
# Do not try to update all underlying state, else we will have a loop
if self._is_over_climate:
for under in self._underlyings:
await under.set_hvac_mode(new_hvac_mode)
# Interpretation of hvac # Interpretation of hvac
HVAC_ACTION_ON = [ # pylint: disable=invalid-name HVAC_ACTION_ON = [ # pylint: disable=invalid-name
@@ -1613,6 +1649,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self, self,
self._underlying_climate_start_hvac_action_date.isoformat(), self._underlying_climate_start_hvac_action_date.isoformat(),
) )
changes = True
if old_hvac_action in HVAC_ACTION_ON and new_hvac_action not in HVAC_ACTION_ON: if old_hvac_action in HVAC_ACTION_ON and new_hvac_action not in HVAC_ACTION_ON:
stop_power_date = self.get_last_updated_date_or_now(new_state) stop_power_date = self.get_last_updated_date_or_now(new_state)
@@ -1633,9 +1670,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
stop_power_date.isoformat(), stop_power_date.isoformat(),
self._underlying_climate_delta_t, self._underlying_climate_delta_t,
) )
changes = True
self.update_custom_attributes() if not changes:
await self._async_control_heating() # try to manage new target temperature set if state
_LOGGER.debug("Do temperature check. temperature is %s, new_state.attributes is %s", self.target_temperature, new_state.attributes)
if self._is_over_climate and new_state.attributes and (new_target_temp := new_state.attributes.get("temperature")) and new_target_temp != self.target_temperature:
_LOGGER.info("%s - Target temp have change to %s", self, new_target_temp)
await self.async_set_temperature(temperature = new_target_temp)
changes = True
if changes:
self.async_write_ha_state()
self.update_custom_attributes()
await self._async_control_heating()
@callback @callback
async def _async_update_temp(self, state: State): async def _async_update_temp(self, state: State):
@@ -2050,7 +2098,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
now - self._last_ext_temperature_mesure.replace(tzinfo=self._current_tz) now - self._last_ext_temperature_mesure.replace(tzinfo=self._current_tz)
).total_seconds() / 60.0 ).total_seconds() / 60.0
mode_cond = self._is_over_climate or self._hvac_mode != HVACMode.OFF # TODO before change:
# mode_cond = self._is_over_climate or self._hvac_mode != HVACMode.OFF
# fixed into this. Why if _is_over_climate we could into security even if HVACMode is OFF ?
mode_cond = self._hvac_mode != HVACMode.OFF
temp_cond: bool = ( temp_cond: bool = (
delta_temp > self._security_delay_min delta_temp > self._security_delay_min
@@ -2078,9 +2129,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
switch_cond, switch_cond,
) )
ret = False # Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security !
if mode_cond and temp_cond and climate_cond: shouldClimateBeInSecurity = False # temp_cond and climate_cond
if not self._security_state: shouldSwitchBeInSecurity = temp_cond and switch_cond
shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity
shouldStartSecurity = mode_cond and not self._security_state and shouldBeInSecurity
# attr_preset_mode is not necessary normaly. It is just here to be sure
shouldStopSecurity = self._security_state and not shouldBeInSecurity and self._attr_preset_mode == PRESET_SECURITY
# Logging and event
if shouldStartSecurity:
if shouldClimateBeInSecurity:
_LOGGER.warning( _LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Set it into security mode", "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Set it into security mode",
self, self,
@@ -2089,10 +2149,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
delta_ext_temp, delta_ext_temp,
self.hvac_action, self.hvac_action,
) )
ret = True elif shouldSwitchBeInSecurity:
if mode_cond and temp_cond and switch_cond:
if not self._security_state:
_LOGGER.warning( _LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f) is over defined value (%.2f). Set it into security mode", "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f) is over defined value (%.2f). Set it into security mode",
self, self,
@@ -2102,9 +2159,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._prop_algorithm.on_percent, self._prop_algorithm.on_percent,
self._security_min_on_percent, self._security_min_on_percent,
) )
ret = True
if mode_cond and temp_cond and not self._security_state:
self.send_event( self.send_event(
EventType.TEMPERATURE_EVENT, EventType.TEMPERATURE_EVENT,
{ {
@@ -2120,8 +2175,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
}, },
) )
if not self._security_state and ret: if shouldStartSecurity:
self._security_state = ret self._security_state = True
self.save_hvac_mode() self.save_hvac_mode()
self.save_preset_mode() self.save_preset_mode()
await self._async_set_preset_mode_internal(PRESET_SECURITY) await self._async_set_preset_mode_internal(PRESET_SECURITY)
@@ -2147,18 +2202,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
}, },
) )
if ( if shouldStopSecurity:
self._security_state
and self._attr_preset_mode == PRESET_SECURITY
and not ret
):
_LOGGER.warning( _LOGGER.warning(
"%s - End of security mode. restoring hvac_mode to %s and preset_mode to %s", "%s - End of security mode. restoring hvac_mode to %s and preset_mode to %s",
self, self,
self._saved_hvac_mode, self._saved_hvac_mode,
self._saved_preset_mode, self._saved_preset_mode,
) )
self._security_state = ret self._security_state = False
# Restore hvac_mode if previously saved # Restore hvac_mode if previously saved
if self._is_over_climate or self._security_default_on_percent <= 0.0: if self._is_over_climate or self._security_default_on_percent <= 0.0:
await self.restore_hvac_mode(False) await self.restore_hvac_mode(False)
@@ -2181,7 +2232,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
}, },
) )
return ret return shouldBeInSecurity
async def _async_control_heating(self, force=False, _=None): async def _async_control_heating(self, force=False, _=None):
"""The main function used to run the calculation at each cycle""" """The main function used to run the calculation at each cycle"""
@@ -2331,9 +2382,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"window_auto_max_duration": self._window_auto_max_duration, "window_auto_max_duration": self._window_auto_max_duration,
} }
if self._is_over_climate: if self._is_over_climate:
self._attr_extra_state_attributes["underlying_climate"] = self._underlyings[ self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[
0 0
].entity_id ].entity_id
self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[
1
].entity_id if len(self._underlyings) > 1 else None
self._attr_extra_state_attributes["underlying_climate_2"] = self._underlyings[
2
].entity_id if len(self._underlyings) > 2 else None
self._attr_extra_state_attributes["underlying_climate_3"] = self._underlyings[
3
].entity_id if len(self._underlyings) > 3 else None
self._attr_extra_state_attributes[ self._attr_extra_state_attributes[
"start_hvac_action_date" "start_hvac_action_date"
] = self._underlying_climate_start_hvac_action_date ] = self._underlying_climate_start_hvac_action_date
@@ -2396,8 +2457,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_preset_temperature service: versatile_thermostat.set_preset_temperature
data: data:
temperature: 17.8
preset: boost preset: boost
temperature: 17.8
temperature_away: 15 temperature_away: 15
target: target:
entity_id: climate.thermostat_2 entity_id: climate.thermostat_2

View File

@@ -4,7 +4,8 @@ from datetime import timedelta
from homeassistant.core import HomeAssistant, callback, Event from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity, DeviceInfo, DeviceEntryType from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event, async_call_later from homeassistant.helpers.event import async_track_state_change_event, async_call_later
from .climate import VersatileThermostat from .climate import VersatileThermostat

View File

@@ -61,7 +61,9 @@ from .const import (
CONF_CYCLE_MIN, CONF_CYCLE_MIN,
CONF_PRESET_POWER, CONF_PRESET_POWER,
CONF_PRESETS, CONF_PRESETS,
CONF_PRESETS_WITH_AC,
CONF_PRESETS_AWAY, CONF_PRESETS_AWAY,
CONF_PRESETS_AWAY_WITH_AC,
CONF_PRESETS_SELECTIONABLE, CONF_PRESETS_SELECTIONABLE,
CONF_PROP_FUNCTION, CONF_PROP_FUNCTION,
CONF_TPI_COEF_EXT, CONF_TPI_COEF_EXT,
@@ -79,10 +81,14 @@ from .const import (
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_SWITCH,
CONF_CLIMATE, CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_USE_WINDOW_FEATURE, CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE, CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE, CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE, CONF_USE_POWER_FEATURE,
CONF_AC_MODE,
CONF_THERMOSTAT_TYPES, CONF_THERMOSTAT_TYPES,
UnknownEntity, UnknownEntity,
WindowOpenDetectionMethod, WindowOpenDetectionMethod,
@@ -132,35 +138,6 @@ def add_suggested_values_to_schema(
return vol.Schema(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): class VersatileThermostatBaseConfigFlow(FlowHandler):
"""The base Config flow class. Used to put some code in commons.""" """The base Config flow class. Used to put some code in commons."""
@@ -257,6 +234,16 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
vol.Required(CONF_CLIMATE): selector.EntitySelector( vol.Required(CONF_CLIMATE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN), selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
), ),
vol.Optional(CONF_CLIMATE_2): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_3): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_CLIMATE_4): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN),
),
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
} }
) )
@@ -274,6 +261,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 self.STEP_WINDOW_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector( vol.Optional(CONF_WINDOW_SENSOR): selector.EntitySelector(
@@ -340,6 +336,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 self.STEP_ADVANCED_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Required( vol.Required(
@@ -489,9 +506,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
elif self._infos[CONF_USE_PRESENCE_FEATURE]: elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence next_step = self.async_step_presence
return await self.generic_step( if self._infos.get(CONF_AC_MODE) == True:
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step 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: async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps""" """Handle the window sensor flow steps"""
@@ -542,9 +562,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presence management flow steps""" """Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input) _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( return await self.generic_step(
"presence", "presence",
self.STEP_PRESENCE_DATA_SCHEMA, schema,
user_input, user_input,
self.async_step_advanced, self.async_step_advanced,
) )
@@ -676,9 +701,12 @@ class VersatileThermostatOptionsFlowHandler(
elif self._infos[CONF_USE_PRESENCE_FEATURE]: elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence next_step = self.async_step_presence
return await self.generic_step( if self._infos.get(CONF_AC_MODE) == True:
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step 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: async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps""" """Handle the window sensor flow steps"""
@@ -736,9 +764,14 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input "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( return await self.generic_step(
"presence", "presence",
self.STEP_PRESENCE_DATA_SCHEMA, schema,
user_input, user_input,
self.async_step_advanced, self.async_step_advanced,
) )

View File

@@ -11,6 +11,11 @@ from homeassistant.components.climate import (
ClimateEntityFeature, 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 homeassistant.exceptions import HomeAssistantError
from .prop_algorithm import ( from .prop_algorithm import (
@@ -60,10 +65,14 @@ CONF_THERMOSTAT_TYPE = "thermostat_type"
CONF_THERMOSTAT_SWITCH = "thermostat_over_switch" CONF_THERMOSTAT_SWITCH = "thermostat_over_switch"
CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate" CONF_THERMOSTAT_CLIMATE = "thermostat_over_climate"
CONF_CLIMATE = "climate_entity_id" CONF_CLIMATE = "climate_entity_id"
CONF_CLIMATE_2 = "climate_entity2_id"
CONF_CLIMATE_3 = "climate_entity3_id"
CONF_CLIMATE_4 = "climate_entity4_id"
CONF_USE_WINDOW_FEATURE = "use_window_feature" CONF_USE_WINDOW_FEATURE = "use_window_feature"
CONF_USE_MOTION_FEATURE = "use_motion_feature" CONF_USE_MOTION_FEATURE = "use_motion_feature"
CONF_USE_PRESENCE_FEATURE = "use_presence_feature" CONF_USE_PRESENCE_FEATURE = "use_presence_feature"
CONF_USE_POWER_FEATURE = "use_power_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_OPEN_THRESHOLD = "window_auto_open_threshold"
CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold" CONF_WINDOW_AUTO_CLOSE_THRESHOLD = "window_auto_close_threshold"
CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration" CONF_WINDOW_AUTO_MAX_DURATION = "window_auto_max_duration"
@@ -77,14 +86,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" PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = { CONF_PRESETS_AWAY = {
p: f"{p}_temp" p: f"{p}_temp"
for p in ( for p in (
PRESET_ECO + PRESET_AWAY_SUFFIX, PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + 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,
) )
} }
@@ -92,11 +126,16 @@ CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
CONF_PRESETS_VALUES = list(CONF_PRESETS.values()) CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.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 = ( ALL_CONF = (
[ [
CONF_NAME, CONF_NAME,
CONF_HEATER, CONF_HEATER,
CONF_HEATER_2,
CONF_HEATER_3,
CONF_HEATER_4,
CONF_TEMP_SENSOR, CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR, CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR, CONF_POWER_SENSOR,
@@ -126,13 +165,19 @@ ALL_CONF = (
CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_CLIMATE, CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_USE_WINDOW_FEATURE, CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE, CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE, CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE, CONF_USE_POWER_FEATURE,
CONF_AC_MODE,
] ]
+ CONF_PRESETS_VALUES + CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES, + CONF_PRESETS_AWAY_VALUES
+ CONF_PRESETS_WITH_AC_VALUES
+ CONF_PRESETS_AWAY_WITH_AC_VALUES,
) )
CONF_FUNCTIONS = [ CONF_FUNCTIONS = [

View File

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

View File

@@ -1,4 +1,4 @@
# -r requirements_dev.txt -r requirements_dev.txt
# aiodiscover # aiodiscover
ulid_transform ulid_transform
pytest-homeassistant-custom-component pytest-homeassistant-custom-component

View File

@@ -1,120 +1,124 @@
reload: reload:
description: Reload all Versatile Thermostat entities. name: Reload
description: Reload all Versatile Thermostat entities.
set_presence: set_presence:
name: Set presence name: Set presence
description: Force the presence mode in thermostat description: Force the presence mode in thermostat
target: target:
entity: entity:
integration: versatile_thermostat integration: versatile_thermostat
fields: fields:
presence: presence:
name: Presence name: Presence
description: Presence setting description: Presence setting
required: true required: true
advanced: false advanced: false
example: "on" example: "on"
default: "on" default: "on"
selector: selector:
select: select:
options: options:
- "on" - "on"
- "off" - "off"
- "home" - "home"
- "not_home" - "not_home"
set_preset_temperature: set_preset_temperature:
name: Set temperature preset name: Set temperature preset
description: Change the target temperature of a preset description: Change the target temperature of a preset
target: target:
entity: entity:
integration: versatile_thermostat integration: versatile_thermostat
fields: fields:
preset: preset:
name: Preset name: Preset
description: Preset name description: Preset name
required: true required: true
advanced: false advanced: false
example: "comfort" example: "comfort"
selector: selector:
select: select:
options: options:
- "eco" - "eco"
- "comfort" - "comfort"
- "boost" - "boost"
temperature: - "eco_ac"
name: Temperature when present - "comfort_ac"
description: Target temperature for the preset when present - "boost_ac"
required: false temperature:
advanced: false name: Temperature when present
example: "19.5" description: Target temperature for the preset when present
default: "17" required: false
selector: advanced: false
number: example: "19.5"
min: 7 default: "17"
max: 35 selector:
step: 0.1 number:
unit_of_measurement: ° min: 7
mode: slider max: 35
temperature_away: step: 0.1
name: Temperature when not present unit_of_measurement: °
description: Target temperature for the preset when not present mode: slider
required: false temperature_away:
advanced: false name: Temperature when not present
example: "17" description: Target temperature for the preset when not present
default: "15" required: false
selector: advanced: false
number: example: "17"
min: 7 default: "15"
max: 35 selector:
step: 0.1 number:
unit_of_measurement: ° min: 7
mode: slider max: 35
step: 0.1
unit_of_measurement: °
mode: slider
set_security: set_security:
name: Set security name: Set security
description: Change the security parameters description: Change the security parameters
target: target:
entity: entity:
integration: versatile_thermostat integration: versatile_thermostat
fields: fields:
delay_min: delay_min:
name: Delay in minutes name: Delay in minutes
description: Maximum allowed delay in minutes between two temperature mesures description: Maximum allowed delay in minutes between two temperature mesures
required: false required: false
advanced: false advanced: false
example: "30" example: "30"
selector: selector:
number: number:
min: 0 min: 0
max: 9999 max: 9999
unit_of_measurement: "min" unit_of_measurement: "min"
mode: box mode: box
min_on_percent: min_on_percent:
name: Minimal on_percent name: Minimal on_percent
description: Minimal heating percent value for security preset activation description: Minimal heating percent value for security preset activation
required: false required: false
advanced: false advanced: false
example: "0.5" example: "0.5"
default: "0.5" default: "0.5"
selector: selector:
number: number:
min: 0 min: 0
max: 1 max: 1
step: 0.05 step: 0.05
unit_of_measurement: "%" unit_of_measurement: "%"
mode: slider mode: slider
default_on_percent: default_on_percent:
name: on_percent used in security mode name: on_percent used in security mode
description: The default heating percent value in security preset description: The default heating percent value in security preset
required: false required: false
advanced: false advanced: false
example: "0.1" example: "0.1"
default: "0.1" default: "0.1"
selector: selector:
number: number:
min: 0 min: 0
max: 1 max: 1
step: 0.05 step: 0.05
unit_of_measurement: "%" unit_of_measurement: "%"
mode: slider mode: slider

View File

@@ -26,11 +26,15 @@
"description": "Linked entities attributes", "description": "Linked entities attributes",
"data": { "data": {
"heater_entity_id": "Heater switch", "heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd Heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th Heater switch", "heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat" "climate_entity_id": "Underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -38,7 +42,11 @@
"heater_entity3_id": "Optional 3rd 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", "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)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id" "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode"
} }
}, },
"tpi": { "tpi": {
@@ -55,7 +63,10 @@
"data": { "data": {
"eco_temp": "Temperature in Eco preset", "eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort 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": { "window": {
@@ -102,7 +113,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)", "presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence", "eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort 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": { "advanced": {
@@ -161,7 +175,11 @@
"heater_entity3_id": "3rd Heater switch", "heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch", "heater_entity4_id": "4th Heater switch",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat" "climate_entity_id": "Underlying thermostat",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -169,7 +187,11 @@
"heater_entity3_id": "Optional 3rd 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", "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)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id" "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode"
} }
}, },
"tpi": { "tpi": {
@@ -186,7 +208,10 @@
"data": { "data": {
"eco_temp": "Temperature in Eco preset", "eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort 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": { "window": {
@@ -233,7 +258,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)", "presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence", "eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort 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": { "advanced": {

View File

@@ -83,6 +83,32 @@ _LOGGER = logging.getLogger(__name__)
class MockClimate(ClimateEntity): class MockClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode""" """A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF) -> None:
"""Initialize the thermostat."""
super().__init__()
self.hass = hass
self.platform = 'climate'
self.entity_id= self.platform+'.'+unique_id
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
self._name = name
self._attr_hvac_action = HVACAction.OFF
self._attr_hvac_mode = hvac_mode
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_target_temperature = 20
self._attr_current_temperature = 15
def set_temperature(self, temperature):
""" Set the target temperature"""
self._attr_target_temperature = temperature
self.async_write_ha_state()
class MockUnavailableClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat.""" """Initialize the thermostat."""
@@ -92,12 +118,11 @@ class MockClimate(ClimateEntity):
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
self._unique_id = unique_id self._unique_id = unique_id
self._name = name self._name = name
self._attr_hvac_action = HVACAction.OFF self._attr_hvac_action = None
self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_mode = None
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_temperature_unit = UnitOfTemperature.CELSIUS
class MagicMockClimate(MagicMock): class MagicMockClimate(MagicMock):
"""A Magic Mock class for a underlying climate entity""" """A Magic Mock class for a underlying climate entity"""
@@ -455,6 +480,51 @@ async def send_climate_change_event(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
return ret return ret
async def send_climate_change_event_with_temperature(
entity: VersatileThermostat,
new_hvac_mode: HVACMode,
old_hvac_mode: HVACMode,
new_hvac_action: HVACAction,
old_hvac_action: HVACAction,
date,
temperature,
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 temperature=%s on %s",
new_hvac_mode,
old_hvac_mode,
new_hvac_action,
old_hvac_action,
date,
temperature,
entity,
)
climate_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_hvac_mode,
attributes={"hvac_action": new_hvac_action, "temperature": temperature},
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
state=old_hvac_mode,
attributes={"hvac_action": old_hvac_action},
last_changed=date,
last_updated=date,
),
},
)
ret = await entity._async_climate_changed(climate_event)
if sleep:
await asyncio.sleep(0.1)
return ret
def cancel_switchs_cycles(entity: VersatileThermostat): def cancel_switchs_cycles(entity: VersatileThermostat):
"""This method will cancel all running cycle on all underlying switch entity""" """This method will cancel all running cycle on all underlying switch entity"""

View File

@@ -15,6 +15,7 @@ from custom_components.versatile_thermostat.const import (
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_AC_MODE,
CONF_TEMP_SENSOR, CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR, CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN, CONF_CYCLE_MIN,
@@ -112,6 +113,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
} }
MOCK_PRESETS_CONFIG = { MOCK_PRESETS_CONFIG = {

View File

@@ -1,5 +1,5 @@
""" Test the Window management """ """ Test the Window management """
from unittest.mock import patch from unittest.mock import patch, call
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -343,3 +343,186 @@ async def test_bug_66(
assert entity.window_state == STATE_OFF assert entity.window_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_82(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockUnavailableClimate(hass, "mockUniqueId", "MockClimateName", {})
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
# assert entity.hvac_action is HVACAction.OFF
# assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_mode is None
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# 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},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == None
# 2. activate security feature when date is expired
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:
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_101(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.target_temperature == entity.min_temp
assert entity.preset_mode is PRESET_NONE
# 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},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force preset mode
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
# 2. Change the target temp of underlying thermostat
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75)
# Should have been switched to Manual preset
assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE

View File

@@ -189,3 +189,103 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# Heater is now on # Heater is now on
assert mock_heater_on.call_count == 1 assert mock_heater_on.call_count == 1
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_over_climate(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# 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},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
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:
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False because a climate is never in security mode
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'

View File

@@ -94,7 +94,7 @@ async def test_window_management_time_not_enough(
await try_window_condition(None) await try_window_condition(None)
assert entity.window_state == STATE_OFF assert entity.window_state == STATE_OFF
await entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -234,7 +234,7 @@ async def test_window_management_time_enough(
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
# Clean the entity # Clean the entity
await entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -418,7 +418,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity # Clean the entity
await entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -561,7 +561,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
# Clean the entity # Clean the entity
await entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -670,4 +670,4 @@ async def test_window_auto_no_on_percent(
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity # Clean the entity
await entity.remove_thermostat() entity.remove_thermostat()

View File

@@ -26,11 +26,15 @@
"description": "Linked entities attributes", "description": "Linked entities attributes",
"data": { "data": {
"heater_entity_id": "Heater switch", "heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd Heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th Heater switch", "heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat" "climate_entity_id": "Underlying climate",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -38,7 +42,11 @@
"heater_entity3_id": "Optional 3rd 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", "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)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id" "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode"
} }
}, },
"tpi": { "tpi": {
@@ -55,7 +63,10 @@
"data": { "data": {
"eco_temp": "Temperature in Eco preset", "eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort 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": { "window": {
@@ -102,7 +113,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)", "presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence", "eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort 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": { "advanced": {
@@ -161,7 +175,11 @@
"heater_entity3_id": "3rd Heater switch", "heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch", "heater_entity4_id": "4th Heater switch",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat" "climate_entity_id": "Underlying thermostat",
"climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate",
"ac_mode": "AC mode"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Mandatory heater entity id", "heater_entity_id": "Mandatory heater entity id",
@@ -169,7 +187,11 @@
"heater_entity3_id": "Optional 3rd 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", "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)", "proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying climate entity id" "climate_entity_id": "Underlying climate entity id",
"climate_entity2_id": "2nd underlying climate entity id",
"climate_entity3_id": "3rd underlying climate entity id",
"climate_entity4_id": "4th underlying climate entity id",
"ac_mode": "Use the Air Conditioning (AC) mode"
} }
}, },
"tpi": { "tpi": {
@@ -186,7 +208,10 @@
"data": { "data": {
"eco_temp": "Temperature in Eco preset", "eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort 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": { "window": {
@@ -233,7 +258,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)", "presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence", "eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort 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": { "advanced": {

View File

@@ -29,7 +29,11 @@
"heater_entity3_id": "3ème radiateur", "heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur", "heater_entity4_id": "4ème radiateur",
"proportional_function": "Algorithme", "proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent" "climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent",
"climate_entity3_id": "3ème thermostat sous-jacent",
"climate_entity4_id": "4ème thermostat sous-jacent",
"ac_mode": "AC mode ?"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -37,7 +41,11 @@
"heater_entity3_id": "Optionnel entity id du 3ème radiateur", "heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur", "heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent" "climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)"
} }
}, },
"tpi": { "tpi": {
@@ -54,7 +62,10 @@
"data": { "data": {
"eco_temp": "Température en preset Eco", "eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort", "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": { "window": {
@@ -101,7 +112,10 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)", "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", "eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort 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": { "advanced": {
@@ -161,7 +175,11 @@
"heater_entity3_id": "3ème radiateur", "heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur", "heater_entity4_id": "4ème radiateur",
"proportional_function": "Algorithme", "proportional_function": "Algorithme",
"climate_entity_id": "Thermostat sous-jacent" "climate_entity_id": "Thermostat sous-jacent",
"climate_entity2_id": "2ème thermostat sous-jacent",
"climate_entity3_id": "3ème thermostat sous-jacent",
"climate_entity4_id": "4ème thermostat sous-jacent",
"ac_mode": "AC mode ?"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire", "heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -169,7 +187,11 @@
"heater_entity3_id": "Optionnel entity id du 3ème radiateur", "heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur", "heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)", "proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "Entity id du thermostat sous-jacent" "climate_entity_id": "Entity id du thermostat sous-jacent",
"climate_entity2_id": "Entity id du 2ème thermostat sous-jacent",
"climate_entity3_id": "Entity id du 3ème thermostat sous-jacent",
"climate_entity4_id": "Entity id du 4ème thermostat sous-jacent",
"ac_mode": "Utilisation du mode Air Conditionné (AC)"
} }
}, },
"tpi": { "tpi": {
@@ -186,7 +208,10 @@
"data": { "data": {
"eco_temp": "Température en preset Eco", "eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort", "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": { "window": {
@@ -233,7 +258,10 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)", "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", "eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort 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": { "advanced": {

View File

@@ -30,7 +30,11 @@
"heater_entity3_id": "Terzo riscaldatore", "heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore", "heater_entity4_id": "Quarto riscaldatore",
"proportional_function": "Algoritmo", "proportional_function": "Algoritmo",
"climate_entity_id": "Termostato sottostante" "climate_entity_id": "Termostato sottostante",
"climate_entity2_id": "Secundo termostato sottostante",
"climate_entity3_id": "Terzo termostato sottostante",
"climate_entity4_id": "Quarto termostato sottostante",
"ac_mode": "AC mode ?"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore", "heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -38,7 +42,11 @@
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del termostato sottostante" "climate_entity_id": "Entity id del termostato sottostante",
"climate_entity2_id": "Entity id del secundo termostato sottostante",
"climate_entity3_id": "Entity id del terzo termostato sottostante",
"climate_entity4_id": "Entity id del quarto termostato sottostante",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
} }
}, },
"tpi": { "tpi": {
@@ -55,7 +63,10 @@
"data": { "data": {
"eco_temp": "Temperatura nel preset Eco", "eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort", "comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost" "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": { "window": {
@@ -102,7 +113,10 @@
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)", "presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza", "eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort 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" "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": { "advanced": {
@@ -161,7 +175,11 @@
"heater_entity3_id": "Terzo interruttore riscaldatore", "heater_entity3_id": "Terzo interruttore riscaldatore",
"heater_entity4_id": "Quarto interruttore riscaldatore", "heater_entity4_id": "Quarto interruttore riscaldatore",
"proportional_function": "Algoritmo", "proportional_function": "Algoritmo",
"climate_entity_id": "Termostato sottostante" "climate_entity_id": "Termostato sottostante",
"climate_entity2_id": "Secundo termostato sottostante",
"climate_entity3_id": "Terzo termostato sottostante",
"climate_entity4_id": "Quarto termostato sottostante",
"ac_mode": "AC mode ?"
}, },
"data_description": { "data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore", "heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -169,7 +187,11 @@
"heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity3_id": "Entity id del terzo riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato", "heater_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)", "proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del termostato sottostante" "climate_entity_id": "Entity id del termostato sottostante",
"climate_entity2_id": "Entity id del secundo termostato sottostante",
"climate_entity3_id": "Entity id del terzo termostato sottostante",
"climate_entity4_id": "Entity id del quarto termostato sottostante",
"ac_mode": "Utilizzare la modalità AC (Air Conditioned) ?"
} }
}, },
"tpi": { "tpi": {
@@ -186,7 +208,10 @@
"data": { "data": {
"eco_temp": "Temperatura nel preset Eco", "eco_temp": "Temperatura nel preset Eco",
"comfort_temp": "Temperatura nel preset Comfort", "comfort_temp": "Temperatura nel preset Comfort",
"boost_temp": "Temperatura nel preset Boost" "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": { "window": {
@@ -233,7 +258,10 @@
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)", "presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza", "eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort 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" "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": { "advanced": {
@@ -285,4 +313,4 @@
} }
} }
} }
} }

View File

@@ -127,7 +127,7 @@ class UnderlyingEntity:
"""Set the target temperature""" """Set the target temperature"""
return return
async def remove_entity(self): def remove_entity(self):
"""Remove the underlying entity""" """Remove the underlying entity"""
return return
@@ -180,7 +180,7 @@ class UnderlyingSwitch(UnderlyingEntity):
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
if self.is_device_active: if self.is_device_active:
await self.turn_off() await self.turn_off()
await self._cancel_cycle() self._cancel_cycle()
if self._hvac_mode != hvac_mode: if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode self._hvac_mode = hvac_mode
@@ -228,7 +228,7 @@ class UnderlyingSwitch(UnderlyingEntity):
if self._async_cancel_cycle is not None: if self._async_cancel_cycle is not None:
if force: if force:
_LOGGER.debug("%s - we force a new cycle", self) _LOGGER.debug("%s - we force a new cycle", self)
await self._cancel_cycle() self._cancel_cycle()
else: else:
_LOGGER.debug( _LOGGER.debug(
"%s - A previous cycle is alredy running and no force -> waits for its end", "%s - A previous cycle is alredy running and no force -> waits for its end",
@@ -258,7 +258,7 @@ class UnderlyingSwitch(UnderlyingEntity):
else: else:
_LOGGER.debug("%s - nothing to do", self) _LOGGER.debug("%s - nothing to do", self)
async def _cancel_cycle(self): def _cancel_cycle(self):
"""Cancel the cycle""" """Cancel the cycle"""
if self._async_cancel_cycle: if self._async_cancel_cycle:
self._async_cancel_cycle() self._async_cancel_cycle()
@@ -275,7 +275,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._on_time_sec, self._on_time_sec,
) )
await self._cancel_cycle() self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF: if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self) _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
@@ -327,7 +327,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._should_relaunch_control_heating, self._should_relaunch_control_heating,
self._off_time_sec, self._off_time_sec,
) )
await self._cancel_cycle() self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF: if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self) _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
@@ -368,9 +368,9 @@ class UnderlyingSwitch(UnderlyingEntity):
# increment energy at the end of the cycle # increment energy at the end of the cycle
self._thermostat.incremente_energy() self._thermostat.incremente_energy()
async def remove_entity(self): def remove_entity(self):
"""Remove the entity""" """Remove the entity after stopping its cycle"""
await self._cancel_cycle() self._cancel_cycle()
class UnderlyingClimate(UnderlyingEntity): class UnderlyingClimate(UnderlyingEntity):
@@ -446,7 +446,7 @@ class UnderlyingClimate(UnderlyingEntity):
def is_device_active(self): def is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
if self.is_initialized: if self.is_initialized:
return self._underlying_climate.hvac_action not in [ return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
HVACAction.IDLE, HVACAction.IDLE,
HVACAction.OFF, HVACAction.OFF,
] ]
@@ -581,8 +581,41 @@ class UnderlyingClimate(UnderlyingEntity):
return self._underlying_climate.temperature_unit return self._underlying_climate.temperature_unit
@property @property
def target_temperature_step(self) -> str: def target_temperature_step(self) -> float:
"""Get the target_temperature_step""" """Get the target_temperature_step"""
if not self.is_initialized: if not self.is_initialized:
return 1 return 1
return self._underlying_climate.target_temperature_step 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, "content_in_root": false,
"render_readme": true, "render_readme": true,
"hide_default_branch": false, "hide_default_branch": false,
"homeassistant": "2022.2.0" "homeassistant": "2023.7.3"
} }