Compare commits

...

28 Commits

Author SHA1 Message Date
Jean-Marc Collin
00d0659eef +1 2023-10-14 17:36:17 +02:00
Jean-Marc Collin
0b22abefa0 Issue #121 - try fix 2023-10-14 17:34:40 +02:00
Jean-Marc Collin
a6ad8e7927 +1 2023-10-14 09:29:42 +02:00
Jean-Marc Collin
23ee8f3d7f NPE fix 2023-10-14 08:49:28 +02:00
Jean-Marc Collin
03723375e2 Issue #121 - loop when underlying is slow 2023-10-14 08:28:04 +02:00
Jean-Marc Collin
fcdd93b4ae Issue # 114 - add unit test for multi-climate VTherm 2023-10-08 13:08:34 +02:00
Jean-Marc Collin
56fdbf4fba FIX Issue #114 - Thermostat not reporting on / off properly (90 sec delay) 2023-10-08 11:05:04 +02:00
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
22 changed files with 1410 additions and 488 deletions

View File

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

View File

@@ -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. ;-).
- [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)
- [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-)
@@ -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*_
> * **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.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.

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.
>![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)
> * **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.
@@ -67,15 +70,15 @@ Many thanks to @salabur, @pvince83 and @bergoglio for the beers. It's very pleas
# When to use / not use
This thermostat can control 2 types of equipment:
1. a heater that only works in on/off mode (named ```thermostat_over_switch```). The minimum configuration required to use this type of thermostat is:
has. equipment such as a radiator (a ```switch``` or equivalent),
b. a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one)
- an equipment such as a radiator (a ```switch``` or equivalent),
- a temperature probe for the room (or an input_number),
- an external temperature sensor (think about weather integration if you don't have one)
2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). For this type of thermostat, the minimum configuration requires:
has. equipment such as air conditioning which is controlled by its own ```climate``` type entity,
b. a temperature probe for the room (or an input_number),
vs. an external temperature sensor (think about weather integration if you don't have one)
- an equipment such as air conditioning which is controlled by its own ```climate``` type entity,
- a temperature probe for the room (or an input_number),
- 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 ?
@@ -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,
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, ...)
## 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:
![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.helpers.restore_state import RestoreEntity
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
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service
@@ -100,6 +100,8 @@ from .const import (
CONF_DEVICE_POWER,
CONF_PRESETS,
CONF_PRESETS_AWAY,
CONF_PRESETS_WITH_AC,
CONF_PRESETS_AWAY_WITH_AC,
CONF_CYCLE_MIN,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_INT,
@@ -127,10 +129,15 @@ from .const import (
# CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_CLIMATE,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_AC_MODE,
UnknownEntity,
EventType,
ATTR_MEAN_POWER_CYCLE,
ATTR_TOTAL_ENERGY,
PRESET_AC_SUFFIX,
)
from .underlyings import UnderlyingSwitch, UnderlyingClimate, UnderlyingEntity
@@ -179,7 +186,7 @@ async def async_setup_entry(
platform.async_register_entity_service(
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_away"): vol.Coerce(float),
},
@@ -213,6 +220,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_presence_state: bool
_window_auto_state: bool
_underlyings: list[UnderlyingEntity]
_last_change_time: datetime
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat."""
@@ -277,6 +285,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
self._last_change_time = None
self._underlyings = []
self.post_init(entry_infos)
@@ -289,9 +299,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self,
entry_infos,
)
self._ac_mode = entry_infos.get(CONF_AC_MODE) == True
# convert entry_infos into usable attributes
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)
if value in entry_infos:
presets[key] = entry_infos.get(value)
@@ -299,7 +312,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_LOGGER.debug("value %s not found in Entry", value)
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)
if value in entry_infos:
presets_away[key] = entry_infos.get(value)
@@ -316,16 +334,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
# Initialize underlying entities
self._underlyings = []
self._thermostat_type = entry_infos.get(CONF_THERMOSTAT_TYPE)
if self._thermostat_type == CONF_THERMOSTAT_CLIMATE:
self._is_over_climate = True
self._underlyings.append(
UnderlyingClimate(
hass=self._hass,
thermostat=self,
climate_entity_id=entry_infos.get(CONF_CLIMATE),
)
)
for climate in [CONF_CLIMATE, CONF_CLIMATE_2, CONF_CLIMATE_3, CONF_CLIMATE_4]:
if entry_infos.get(climate):
self._underlyings.append(
UnderlyingClimate(
hass=self._hass,
thermostat=self,
climate_entity_id=entry_infos.get(climate),
)
)
else:
lst_switches = [entry_infos.get(CONF_HEATER)]
if entry_infos.get(CONF_HEATER_2):
@@ -336,7 +357,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
lst_switches.append(entry_infos.get(CONF_HEATER_4))
delta_cycle = self._cycle_min * 60 / len(lst_switches)
self._underlyings = []
for idx, switch in enumerate(lst_switches):
self._underlyings.append(
UnderlyingSwitch(
@@ -394,10 +414,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
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
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
# else:
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
if self._ac_mode:
self._hvac_list = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
else:
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
self._unit = self._hass.config.units.temperature_unit
# Will be restored if possible
@@ -437,7 +457,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._target_temp = None
self._saved_target_temp = PRESET_NONE
self._humidity = None
self._ac_mode = False
self._fan_mode = None
self._swing_mode = None
self._cur_temp = None
@@ -494,13 +513,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if len(presets):
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:
self._attr_preset_modes.append(key)
# self._attr_preset_modes = (
# [PRESET_NONE] + list(presets.keys()) + [PRESET_ACTIVITY]
# )
_LOGGER.debug(
"After adding presets, preset_modes to %s", self._attr_preset_modes
)
@@ -609,11 +625,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# Ingore this error which is possible if underlying climate is not found temporary
pass
async def remove_thermostat(self):
def remove_thermostat(self):
"""Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self)
for under in self._underlyings:
await under.remove_entity()
under.remove_entity()
async def async_startup(self):
"""Triggered on startup, used to get old state and set internal states accordingly"""
@@ -765,6 +781,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
else:
self.hass.create_task(self._async_control_heating())
self.reset_last_change_time()
await self.get_my_previous_state()
if self.hass.state == CoreState.running:
@@ -936,13 +954,16 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
@property
def hvac_mode(self) -> HVACMode | None:
"""Return current operation."""
if self._is_over_climate:
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different
# delta will be managed by climate_state_change event.
# TODO remove this when ok
# if self._is_over_climate:
# if one not OFF -> return it
# else OFF
for under in self._underlyings:
if (action := under.hvac_mode) not in [HVACMode.OFF]:
return action
return HVACMode.OFF
# for under in self._underlyings:
# if (mode := under.hvac_mode) not in [HVACMode.OFF]
# return mode
# return HVACMode.OFF
return self._hvac_mode
@@ -958,7 +979,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# else OFF
one_idle = False
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
if under.hvac_action == HVACAction.IDLE:
one_idle = True
@@ -1207,12 +1228,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
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:
await self._async_control_heating(force=True)
# Ensure we update the current operation after changing the mode
self.reset_last_temperature_time()
self.reset_last_change_time()
self.update_custom_attributes()
self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
@@ -1268,6 +1295,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self.recalculate()
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
def reset_last_change_time(self, old_preset_mode=None):
"""Reset to now the last change time"""
self._last_change_time = datetime.now(tz=self._current_tz)
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
def reset_last_temperature_time(self, old_preset_mode=None):
"""Reset to now the last temperature time if conditions are satisfied"""
if (
@@ -1286,13 +1318,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) # in security just keep the current target temperature, the thermostat should be off
if preset_mode == PRESET_POWER:
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:
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):
"""Get the preset name in away mode (when presence is off)"""
@@ -1338,6 +1375,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await self._async_internal_set_temperature(temperature)
self._attr_preset_mode = PRESET_NONE
self.recalculate()
self.reset_last_change_time()
await self._async_control_heating(force=True)
async def _async_internal_set_temperature(self, temperature):
@@ -1541,8 +1579,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def _check_switch_initial_state(self):
"""Prevent the device from keep running if HVAC_MODE_OFF."""
_LOGGER.debug("%s - Calling _check_switch_initial_state", self)
if self.is_over_climate:
return
# We need to do the same check for over_climate underlyings
#if self.is_over_climate:
# return
for under in self._underlyings:
await under.check_initial_state(self._hvac_mode)
@@ -1560,11 +1599,22 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
@callback
async def _async_climate_changed(self, event):
"""Handle unerdlying climate state changes."""
async def end_climate_changed(changes):
""" To end the event management"""
if changes:
self.async_write_ha_state()
self.update_custom_attributes()
await self._async_control_heating()
new_state = event.data.get("new_state")
_LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state)
if not new_state:
return
changes = False
new_hvac_mode = new_state.state
old_state = event.data.get("old_state")
old_hvac_action = (
old_state.attributes.get("hvac_action")
@@ -1577,27 +1627,29 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
else None
)
old_state_date_changed = old_state.last_changed if old_state and old_state.last_changed else None
old_state_date_updated = old_state.last_updated if old_state and old_state.last_updated else None
new_state_date_changed = new_state.last_changed if new_state and new_state.last_changed else None
new_state_date_updated = new_state.last_updated if new_state and new_state.last_updated else None
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
#if self._hvac_mode == HVACMode.OFF 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(
"%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,
new_state,
new_hvac_mode,
self._hvac_mode,
new_hvac_action,
old_hvac_action,
)
if new_state.state in [
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.HEAT_COOL,
HVACMode.DRY,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
]:
self._hvac_mode = new_state.state
_LOGGER.debug("%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", self, self._last_change_time, old_state_date_changed, old_state_date_updated, new_state_date_changed, new_state_date_updated)
# Interpretation of hvac
# Interpretation of hvac action
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
HVACAction.COOLING,
HVACAction.DRYING,
@@ -1613,6 +1665,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self,
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:
stop_power_date = self.get_last_updated_date_or_now(new_state)
@@ -1633,9 +1686,44 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
stop_power_date.isoformat(),
self._underlying_climate_delta_t,
)
changes = True
# Issue #120 - Some TRV are chaning target temperature a very long time (6 sec) after the change. In that case a loop is possible because
if new_state_date_updated and self._last_change_time:
delta = (new_state_date_updated - self._last_change_time).total_seconds()
if delta < 10:
_LOGGER.info("%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", self
)
await end_climate_changed(changes)
return
if new_hvac_mode in [
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.HEAT_COOL,
HVACMode.DRY,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
None
] and self._hvac_mode != new_hvac_mode:
changes = True
self._hvac_mode = new_hvac_mode
# Update all underlyings state
if self._is_over_climate:
for under in self._underlyings:
await under.set_hvac_mode(new_hvac_mode)
if not changes:
# 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 in underlying have change to %s", self, new_target_temp)
await self.async_set_temperature(temperature = new_target_temp)
changes = True
await end_climate_changed(changes)
self.update_custom_attributes()
await self._async_control_heating()
@callback
async def _async_update_temp(self, state: State):
@@ -2081,9 +2169,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
switch_cond,
)
ret = False
if mode_cond and temp_cond and climate_cond:
if not self._security_state:
# Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security !
shouldClimateBeInSecurity = False # temp_cond and climate_cond
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(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Set it into security mode",
self,
@@ -2092,10 +2189,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
delta_ext_temp,
self.hvac_action,
)
ret = True
if mode_cond and temp_cond and switch_cond:
if not self._security_state:
elif shouldSwitchBeInSecurity:
_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",
self,
@@ -2105,9 +2199,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._prop_algorithm.on_percent,
self._security_min_on_percent,
)
ret = True
if mode_cond and temp_cond and not self._security_state:
self.send_event(
EventType.TEMPERATURE_EVENT,
{
@@ -2123,8 +2215,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
},
)
if not self._security_state and ret:
self._security_state = ret
if shouldStartSecurity:
self._security_state = True
self.save_hvac_mode()
self.save_preset_mode()
await self._async_set_preset_mode_internal(PRESET_SECURITY)
@@ -2150,18 +2242,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
},
)
if (
self._security_state
and self._attr_preset_mode == PRESET_SECURITY
and not ret
):
if shouldStopSecurity:
_LOGGER.warning(
"%s - End of security mode. restoring hvac_mode to %s and preset_mode to %s",
self,
self._saved_hvac_mode,
self._saved_preset_mode,
)
self._security_state = ret
self._security_state = False
# Restore hvac_mode if previously saved
if self._is_over_climate or self._security_default_on_percent <= 0.0:
await self.restore_hvac_mode(False)
@@ -2184,7 +2272,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
},
)
return ret
return shouldBeInSecurity
async def _async_control_heating(self, force=False, _=None):
"""The main function used to run the calculation at each cycle"""
@@ -2334,9 +2422,19 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"window_auto_max_duration": self._window_auto_max_duration,
}
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
].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[
"start_hvac_action_date"
] = self._underlying_climate_start_hvac_action_date
@@ -2399,8 +2497,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Called by a service call:
service: versatile_thermostat.set_preset_temperature
data:
temperature: 17.8
preset: boost
temperature: 17.8
temperature_away: 15
target:
entity_id: climate.thermostat_2

View File

@@ -4,7 +4,8 @@ from datetime import timedelta
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
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 .climate import VersatileThermostat

View File

@@ -61,7 +61,9 @@ from .const import (
CONF_CYCLE_MIN,
CONF_PRESET_POWER,
CONF_PRESETS,
CONF_PRESETS_WITH_AC,
CONF_PRESETS_AWAY,
CONF_PRESETS_AWAY_WITH_AC,
CONF_PRESETS_SELECTIONABLE,
CONF_PROP_FUNCTION,
CONF_TPI_COEF_EXT,
@@ -79,10 +81,14 @@ from .const import (
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_SWITCH,
CONF_CLIMATE,
CONF_CLIMATE_2,
CONF_CLIMATE_3,
CONF_CLIMATE_4,
CONF_USE_WINDOW_FEATURE,
CONF_USE_MOTION_FEATURE,
CONF_USE_PRESENCE_FEATURE,
CONF_USE_POWER_FEATURE,
CONF_AC_MODE,
CONF_THERMOSTAT_TYPES,
UnknownEntity,
WindowOpenDetectionMethod,
@@ -132,35 +138,6 @@ def add_suggested_values_to_schema(
return vol.Schema(schema)
# def is_temperature_sensor(sensor: RegistryEntry):
# """Check if a registryEntry is a temperature sensor or assimilable to a temperature sensor"""
# if not sensor.entity_id.startswith(
# INPUT_NUMBER_DOMAIN
# ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
# return False
# return (
# sensor.device_class == TEMPERATURE
# or sensor.original_device_class == TEMPERATURE
# or sensor.unit_of_measurement in TEMPERATURE_UNITS
# )
#
#
# def is_power_sensor(sensor: RegistryEntry):
# """Check if a registryEntry is a power sensor or assimilable to a temperature sensor"""
# if not sensor.entity_id.startswith(
# INPUT_NUMBER_DOMAIN
# ) and not sensor.entity_id.startswith(SENSOR_DOMAIN):
# return False
# return (
# sensor.unit_of_measurement
# in [
# UnitOfPower.KILO_WATT,
# UnitOfPower.WATT,
# UnitOfPower.BTU_PER_HOUR,
# ]
# )
class VersatileThermostatBaseConfigFlow(FlowHandler):
"""The base Config flow class. Used to put some code in commons."""
@@ -257,6 +234,16 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
vol.Required(CONF_CLIMATE): selector.EntitySelector(
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
{
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
{
vol.Required(
@@ -489,9 +506,12 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
)
if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps"""
@@ -542,9 +562,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESENCE_DATA_SCHEMA
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
schema,
user_input,
self.async_step_advanced,
)
@@ -676,9 +701,12 @@ class VersatileThermostatOptionsFlowHandler(
elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence
return await self.generic_step(
"presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, next_step
)
if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESETS_DATA_SCHEMA
return await self.generic_step("presets", schema, user_input, next_step)
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window sensor flow steps"""
@@ -736,9 +764,14 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
)
if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else:
schema = self.STEP_PRESENCE_DATA_SCHEMA
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
schema,
user_input,
self.async_step_advanced,
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,11 +26,15 @@
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch",
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat"
"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": {
"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_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying 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": {
@@ -55,7 +63,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -102,7 +113,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -161,7 +175,11 @@
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"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": {
"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_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying 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": {
@@ -186,7 +208,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -233,7 +258,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {

View File

@@ -83,6 +83,37 @@ _LOGGER = logging.getLogger(__name__)
class MockClimate(ClimateEntity):
"""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 if hvac_mode == HVACMode.OFF else HVACAction.HEATING
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()
def async_set_hvac_mode(self, hvac_mode):
""" The hvac mode"""
self._attr_hvac_mode = hvac_mode
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:
"""Initialize the thermostat."""
@@ -92,12 +123,11 @@ class MockClimate(ClimateEntity):
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
self._name = name
self._attr_hvac_action = HVACAction.OFF
self._attr_hvac_mode = HVACMode.OFF
self._attr_hvac_action = None
self._attr_hvac_mode = None
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
class MagicMockClimate(MagicMock):
"""A Magic Mock class for a underlying climate entity"""
@@ -455,6 +485,51 @@ async def send_climate_change_event(
await asyncio.sleep(0.1)
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):
"""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_SWITCH,
CONF_THERMOSTAT_TYPE,
CONF_AC_MODE,
CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN,
@@ -112,6 +113,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
}
MOCK_PRESETS_CONFIG = {

View File

@@ -1,5 +1,5 @@
""" 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 datetime import datetime, timedelta
@@ -343,3 +343,198 @@ async def test_bug_66(
assert entity.window_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT
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 == 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)
# 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
)
# Underlying is in HEAT mode but should be shutdown at startup
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, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
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_mode is HVACMode.OFF
# because the underlying is heating. In real life the underlying should be shut-off
assert entity.hvac_action is HVACAction.HEATING
# Underlying should have been shutdown
assert mock_underlying_set_hvac_mode.call_count == 1
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
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

@@ -257,11 +257,14 @@ async def test_multiple_switchs(
)
assert entity
assert entity.is_over_climate is False
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
):
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
@@ -273,14 +276,16 @@ async def test_multiple_switchs(
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Checks that all heaters are off
with patch(
"homeassistant.core.StateMachine.is_state", return_value=False
) as mock_is_state:
# Checks that all climates are off
assert entity._is_device_active is False
# Should be call for all Switch
assert mock_is_state.call_count == 4
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
# Set temperature to a low level
with patch(
@@ -339,3 +344,215 @@ async def test_multiple_switchs(
# The first heater should be turned on but is already on but because call_later is mocked, it is only turned on here
assert mock_heater_on.call_count == 1
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_climates(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
):
"""Test that when multiple climates are configured the activation and deactivation is propagated to all climates"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4ClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4ClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "switch.mock_climate1",
CONF_CLIMATE_2: "switch.mock_climate2",
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname"
)
assert entity
assert entity.is_over_climate is True
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity._is_device_active is False
# Stop heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.OFF)
assert entity.hvac_mode is HVACMode.OFF
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity._is_device_active is False
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_climates_underlying_changes(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_send_event,
):
"""Test that when multiple switch are configured the activation of one underlying climate activate the others"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOver4ClimateMockName",
unique_id="uniqueId",
data={
CONF_NAME: "TheOver4ClimateMockName",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "switch.mock_climate1",
CONF_CLIMATE_2: "switch.mock_climate2",
CONF_CLIMATE_3: "switch.mock_climate3",
CONF_CLIMATE_4: "switch.mock_climate4",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
},
)
entity: VersatileThermostat = await create_thermostat(
hass, entry, "climate.theover4climatemockname"
)
assert entity
assert entity.is_over_climate is True
assert entity.nb_underlying_entities == 4
# start heating, in boost mode. We block the control_heating to avoid running a cycle
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await entity.async_set_hvac_mode(HVACMode.HEAT)
await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
assert entity.window_state is None
event_timestamp = now - timedelta(minutes=4)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity._is_device_active is False
# Stop heating on one underlying climate
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode:
await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, now)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.OFF),
]
)
assert entity.hvac_mode == HVACMode.OFF
assert entity._is_device_active is False
# Start heating on one underlying climate
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode, patch(
# notice that there is no need of return_value=HVACAction.IDLE because this is not a function but a property
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", HVACAction.IDLE
) as mock_underlying_get_hvac_action:
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, now)
# Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4
mock_underlying_set_hvac_mode.assert_has_calls(
[
call.set_hvac_mode(HVACMode.HEAT),
]
)
assert entity.hvac_mode == HVACMode.HEAT
assert entity.hvac_action == HVACAction.IDLE
assert entity._is_device_active is False

View File

@@ -189,3 +189,117 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# Heater is now on
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
# Because the underlying is HEATING. In real life the underlying will be shut-off
assert entity.hvac_action is HVACAction.HEATING
assert entity.hvac_mode is HVACMode.OFF
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
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(
[
# At startup
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
# One call more
assert mock_send_event.call_count == 3
mock_send_event.assert_has_calls(
[
call.send_event(
EventType.HVAC_MODE_EVENT,
{"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)
assert entity.window_state == STATE_OFF
await entity.remove_thermostat()
entity.remove_thermostat()
@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
# Clean the entity
await entity.remove_thermostat()
entity.remove_thermostat()
@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
# Clean the entity
await entity.remove_thermostat()
entity.remove_thermostat()
@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
# Clean the entity
await entity.remove_thermostat()
entity.remove_thermostat()
@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
# Clean the entity
await entity.remove_thermostat()
entity.remove_thermostat()

View File

@@ -26,11 +26,15 @@
"description": "Linked entities attributes",
"data": {
"heater_entity_id": "Heater switch",
"heater_entity2_id": "2nd Heater switch",
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm",
"climate_entity_id": "Underlying thermostat"
"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": {
"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_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying 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": {
@@ -55,7 +63,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -102,7 +113,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {
@@ -161,7 +175,11 @@
"heater_entity3_id": "3rd Heater switch",
"heater_entity4_id": "4th Heater switch",
"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": {
"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_entity4_id": "Optional 4th Heater entity id. Leave empty if not used",
"proportional_function": "Algorithm to use (TPI is the only one for now)",
"climate_entity_id": "Underlying 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": {
@@ -186,7 +208,10 @@
"data": {
"eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset"
"boost_temp": "Temperature in Boost preset",
"eco_ac_temp": "Temperature in Eco preset for AC mode",
"comfort_ac_temp": "Temperature in Comfort preset for AC mode",
"boost_ac_temp": "Temperature in Boost preset for AC mode"
}
},
"window": {
@@ -233,7 +258,10 @@
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
"boost_away_temp": "Temperature in Boost preset when no presence",
"eco_ac_away_temp": "Temperature in Eco preset when no presence in AC mode",
"comfort_ac_away_temp": "Temperature in Comfort preset when no presence in AC mode",
"boost_ac_away_temp": "Temperature in Boost preset when no presence in AC mode"
}
},
"advanced": {

View File

@@ -29,7 +29,11 @@
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"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": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -37,7 +41,11 @@
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "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": {
@@ -54,7 +62,10 @@
"data": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost"
"boost_temp": "Température en preset Boost",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
}
},
"window": {
@@ -101,7 +112,10 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
"boost_away_temp": "Température en preset Boost en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
}
},
"advanced": {
@@ -161,7 +175,11 @@
"heater_entity3_id": "3ème radiateur",
"heater_entity4_id": "4ème radiateur",
"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": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",
@@ -169,7 +187,11 @@
"heater_entity3_id": "Optionnel entity id du 3ème radiateur",
"heater_entity4_id": "Optionnel entity id du 4ème radiateur",
"proportional_function": "Algorithme à utiliser (Seul TPI est disponible pour l'instant)",
"climate_entity_id": "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": {
@@ -186,7 +208,10 @@
"data": {
"eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost"
"boost_temp": "Température en preset Boost",
"eco_ac_temp": "Température en preset Eco en mode AC",
"comfort_ac_temp": "Température en preset Comfort en mode AC",
"boost_ac_temp": "Température en preset Boost en mode AC"
}
},
"window": {
@@ -233,7 +258,10 @@
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
"boost_away_temp": "Température en preset Boost en cas d'absence",
"eco_ac_away_temp": "Température en preset Eco en cas d'absence en mode AC",
"comfort_ac_away_temp": "Température en preset Comfort en cas d'absence en mode AC",
"boost_ac_away_temp": "Température en preset Boost en cas d'absence en mode AC"
}
},
"advanced": {

View File

@@ -30,7 +30,11 @@
"heater_entity3_id": "Terzo riscaldatore",
"heater_entity4_id": "Quarto riscaldatore",
"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": {
"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_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del termostato sottostante"
"climate_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": {
@@ -55,7 +63,10 @@
"data": {
"eco_temp": "Temperatura nel preset Eco",
"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": {
@@ -102,7 +113,10 @@
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza"
"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": {
@@ -161,7 +175,11 @@
"heater_entity3_id": "Terzo interruttore riscaldatore",
"heater_entity4_id": "Quarto interruttore riscaldatore",
"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": {
"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_entity4_id": "Entity id del quarto riscaldatore facoltativo. Lasciare vuoto se non utilizzato",
"proportional_function": "Algoritmo da utilizzare (il TPI per adesso è l'unico)",
"climate_entity_id": "Entity id del termostato sottostante"
"climate_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": {
@@ -186,7 +208,10 @@
"data": {
"eco_temp": "Temperatura nel preset Eco",
"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": {
@@ -233,7 +258,10 @@
"presence_sensor_entity_id": "Entity id sensore presenza (true se è presente qualcuno)",
"eco_away_temp": "Temperatura al preset Eco in caso d'assenza",
"comfort_away_temp": "Temperatura al preset Comfort in caso d'assenza",
"boost_away_temp": "Temperatura al preset Boost in caso d'assenza"
"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": {
@@ -285,4 +313,4 @@
}
}
}
}
}

View File

@@ -6,7 +6,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.exceptions import ServiceNotFound
from homeassistant.backports.enum import StrEnum
from enum import StrEnum
from homeassistant.core import HomeAssistant, DOMAIN as HA_DOMAIN, CALLBACK_TYPE
from homeassistant.components.climate import (
ClimateEntity,
@@ -127,10 +127,27 @@ class UnderlyingEntity:
"""Set the target temperature"""
return
async def remove_entity(self):
def remove_entity(self):
"""Remove the underlying entity"""
return
async def check_initial_state(self, hvac_mode: HVACMode):
"""Prevent the underlying to be on but thermostat is off"""
if hvac_mode == HVACMode.OFF and self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is OFF, but the underlying device is ON. Turning off device %s",
self,
self._entity_id,
)
await self.set_hvac_mode(hvac_mode)
elif hvac_mode != HVACMode.OFF and self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is ON, but the underlying device is not ON. Turning on device %s",
self,
self._entity_id,
)
await self.set_hvac_mode(hvac_mode)
# override to be able to mock the call
def call_later(
self, hass: HomeAssistant, delay_sec: int, called_method
@@ -180,7 +197,7 @@ class UnderlyingSwitch(UnderlyingEntity):
if hvac_mode == HVACMode.OFF:
if self.is_device_active:
await self.turn_off()
await self._cancel_cycle()
self._cancel_cycle()
if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode
@@ -193,16 +210,6 @@ class UnderlyingSwitch(UnderlyingEntity):
"""If the toggleable device is currently active."""
return self._hass.states.is_state(self._entity_id, STATE_ON)
async def check_initial_state(self, hvac_mode: HVACMode):
"""Prevent the heater to be on but thermostat is off"""
if hvac_mode == HVACMode.OFF and self.is_device_active:
_LOGGER.warning(
"%s - The hvac mode is OFF, but the switch device is ON. Turning off device %s",
self,
self._entity_id,
)
await self.turn_off()
async def start_cycle(
self,
hvac_mode: HVACMode,
@@ -228,7 +235,7 @@ class UnderlyingSwitch(UnderlyingEntity):
if self._async_cancel_cycle is not None:
if force:
_LOGGER.debug("%s - we force a new cycle", self)
await self._cancel_cycle()
self._cancel_cycle()
else:
_LOGGER.debug(
"%s - A previous cycle is alredy running and no force -> waits for its end",
@@ -258,7 +265,7 @@ class UnderlyingSwitch(UnderlyingEntity):
else:
_LOGGER.debug("%s - nothing to do", self)
async def _cancel_cycle(self):
def _cancel_cycle(self):
"""Cancel the cycle"""
if self._async_cancel_cycle:
self._async_cancel_cycle()
@@ -275,7 +282,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._on_time_sec,
)
await self._cancel_cycle()
self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
@@ -327,7 +334,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._should_relaunch_control_heating,
self._off_time_sec,
)
await self._cancel_cycle()
self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
@@ -368,9 +375,9 @@ class UnderlyingSwitch(UnderlyingEntity):
# increment energy at the end of the cycle
self._thermostat.incremente_energy()
async def remove_entity(self):
"""Remove the entity"""
await self._cancel_cycle()
def remove_entity(self):
"""Remove the entity after stopping its cycle"""
self._cancel_cycle()
class UnderlyingClimate(UnderlyingEntity):
@@ -446,7 +453,7 @@ class UnderlyingClimate(UnderlyingEntity):
def is_device_active(self):
"""If the toggleable device is currently active."""
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.OFF,
]
@@ -618,4 +625,4 @@ class UnderlyingClimate(UnderlyingEntity):
"""Turn auxiliary heater on."""
if not self.is_initialized:
return None
return self._underlying_climate.turn_aux_heat_off()
return self._underlying_climate.turn_aux_heat_off()

View File

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