Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcdd93b4ae | ||
|
|
56fdbf4fba | ||
|
|
ef994e300b | ||
|
|
72c4105bbd | ||
|
|
79eb4a0a0d | ||
|
|
b032198c66 | ||
|
|
487c118b44 | ||
|
|
e29ff0568b | ||
|
|
814e4d3b83 | ||
|
|
abb6531f49 | ||
|
|
f970c18eaf | ||
|
|
af51ef62e0 | ||
|
|
b38fbd9d78 | ||
|
|
6e8e72e343 | ||
|
|
2bebe3e210 |
@@ -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"
|
||||
|
||||
@@ -54,7 +54,9 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
|
||||
|
||||
>  _*Nouveautés*_
|
||||
> > * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
|
||||
> * **Release 3.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.
|
||||
|
||||
17
README.md
17
README.md
@@ -53,6 +53,8 @@
|
||||
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.
|
||||
|
||||
> _*News*_
|
||||
> * **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)
|
||||
@@ -68,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 ?
|
||||
|
||||
@@ -218,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:
|
||||

|
||||
|
||||
@@ -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
|
||||
@@ -129,6 +129,9 @@ 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,
|
||||
@@ -183,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),
|
||||
},
|
||||
@@ -328,16 +331,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):
|
||||
@@ -348,7 +354,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(
|
||||
@@ -505,7 +510,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
if len(presets):
|
||||
self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE
|
||||
|
||||
for key, val in CONF_PRESETS.items(): # TODO before presets.items():
|
||||
for key, val in CONF_PRESETS.items():
|
||||
if val != 0.0:
|
||||
self._attr_preset_modes.append(key)
|
||||
|
||||
@@ -944,13 +949,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
|
||||
|
||||
@@ -966,7 +974,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
|
||||
@@ -1558,8 +1566,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)
|
||||
|
||||
@@ -1582,6 +1591,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
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")
|
||||
@@ -1594,16 +1606,22 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
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 [
|
||||
if new_hvac_mode in [
|
||||
HVACMode.OFF,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.COOL,
|
||||
@@ -1611,8 +1629,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
HVACMode.DRY,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.FAN_ONLY,
|
||||
]:
|
||||
self._hvac_mode = new_state.state
|
||||
None
|
||||
] and self._hvac_mode != new_hvac_mode:
|
||||
changes = True
|
||||
self._hvac_mode = new_hvac_mode
|
||||
# Do not try to update all underlying state, else we will have a loop
|
||||
if self._is_over_climate:
|
||||
for under in self._underlyings:
|
||||
await under.set_hvac_mode(new_hvac_mode)
|
||||
|
||||
# Interpretation of hvac
|
||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
@@ -1630,6 +1654,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)
|
||||
@@ -1650,9 +1675,20 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
stop_power_date.isoformat(),
|
||||
self._underlying_climate_delta_t,
|
||||
)
|
||||
changes = True
|
||||
|
||||
self.update_custom_attributes()
|
||||
await self._async_control_heating()
|
||||
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 have change to %s", self, new_target_temp)
|
||||
await self.async_set_temperature(temperature = new_target_temp)
|
||||
changes = True
|
||||
|
||||
if changes:
|
||||
self.async_write_ha_state()
|
||||
self.update_custom_attributes()
|
||||
await self._async_control_heating()
|
||||
|
||||
@callback
|
||||
async def _async_update_temp(self, state: State):
|
||||
@@ -2098,9 +2134,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,
|
||||
@@ -2109,10 +2154,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,
|
||||
@@ -2122,9 +2164,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,
|
||||
{
|
||||
@@ -2140,8 +2180,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)
|
||||
@@ -2167,18 +2207,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)
|
||||
@@ -2201,7 +2237,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"""
|
||||
@@ -2351,9 +2387,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
|
||||
@@ -2416,8 +2462,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -81,6 +81,9 @@ 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,
|
||||
@@ -231,6 +234,15 @@ 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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -65,6 +65,9 @@ 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"
|
||||
@@ -130,6 +133,9 @@ 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,
|
||||
@@ -159,6 +165,9 @@ 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,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
homeassistant
|
||||
homeassistant==2023.10.1
|
||||
ffmpeg
|
||||
@@ -1,4 +1,4 @@
|
||||
# -r requirements_dev.txt
|
||||
-r requirements_dev.txt
|
||||
# aiodiscover
|
||||
ulid_transform
|
||||
pytest-homeassistant-custom-component
|
||||
@@ -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
|
||||
|
||||
@@ -26,11 +26,14 @@
|
||||
"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": {
|
||||
@@ -40,6 +43,9 @@
|
||||
"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_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"
|
||||
}
|
||||
},
|
||||
@@ -170,6 +176,9 @@
|
||||
"heater_entity4_id": "4th Heater switch",
|
||||
"proportional_function": "Algorithm",
|
||||
"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": {
|
||||
@@ -179,6 +188,9 @@
|
||||
"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_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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
@@ -26,11 +26,14 @@
|
||||
"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": {
|
||||
@@ -40,6 +43,9 @@
|
||||
"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_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"
|
||||
}
|
||||
},
|
||||
@@ -170,6 +176,9 @@
|
||||
"heater_entity4_id": "4th Heater switch",
|
||||
"proportional_function": "Algorithm",
|
||||
"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": {
|
||||
@@ -179,6 +188,9 @@
|
||||
"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_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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"proportional_function": "Algorithme",
|
||||
"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": {
|
||||
@@ -39,6 +42,9 @@
|
||||
"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_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)"
|
||||
}
|
||||
},
|
||||
@@ -170,6 +176,9 @@
|
||||
"heater_entity4_id": "4ème radiateur",
|
||||
"proportional_function": "Algorithme",
|
||||
"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": {
|
||||
@@ -179,6 +188,9 @@
|
||||
"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_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)"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
"heater_entity4_id": "Quarto riscaldatore",
|
||||
"proportional_function": "Algoritmo",
|
||||
"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": {
|
||||
@@ -40,6 +43,9 @@
|
||||
"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_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) ?"
|
||||
}
|
||||
},
|
||||
@@ -170,6 +176,9 @@
|
||||
"heater_entity4_id": "Quarto interruttore riscaldatore",
|
||||
"proportional_function": "Algoritmo",
|
||||
"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": {
|
||||
@@ -179,6 +188,9 @@
|
||||
"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_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) ?"
|
||||
}
|
||||
},
|
||||
@@ -301,4 +313,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -131,6 +131,23 @@ class UnderlyingEntity:
|
||||
"""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
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user