Compare commits

...

15 Commits

Author SHA1 Message Date
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
16 changed files with 716 additions and 337 deletions
+175 -175
View File
@@ -1,196 +1,196 @@
default_config: default_config:
logger: logger:
default: info default: info
logs: logs:
custom_components.versatile_thermostat: info custom_components.versatile_thermostat: debug
custom_components.versatile_thermostat.underlyings: debug custom_components.versatile_thermostat.underlyings: debug
custom_components.versatile_thermostat.climate: debug custom_components.versatile_thermostat.climate: debug
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) # If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy: debugpy:
start: true start: true
wait: false wait: false
port: 5678 port: 5678
input_number: input_number:
fake_temperature_sensor1: fake_temperature_sensor1:
name: Temperature name: Temperature
min: 0 min: 0
max: 35 max: 35
step: .1 step: .1
icon: mdi:thermometer icon: mdi:thermometer
unit_of_measurement: °C unit_of_measurement: °C
mode: box mode: box
fake_external_temperature_sensor1: fake_external_temperature_sensor1:
name: Ext Temperature name: Ext Temperature
min: -10 min: -10
max: 35 max: 35
step: .1 step: .1
icon: mdi:home-thermometer icon: mdi:home-thermometer
unit_of_measurement: °C unit_of_measurement: °C
mode: box mode: box
fake_current_power: fake_current_power:
name: Current power name: Current power
min: 0 min: 0
max: 1000 max: 1000
step: 10 step: 10
icon: mdi:flash icon: mdi:flash
unit_of_measurement: kW unit_of_measurement: kW
fake_current_power_max: fake_current_power_max:
name: Current power max threshold name: Current power max threshold
min: 0 min: 0
max: 1000 max: 1000
step: 10 step: 10
icon: mdi:flash icon: mdi:flash
unit_of_measurement: kW unit_of_measurement: kW
input_boolean: input_boolean:
# input_boolean to simulate the windows entity. Only for development environment. # input_boolean to simulate the windows entity. Only for development environment.
fake_window_sensor1: fake_window_sensor1:
name: Window 1 name: Window 1
icon: mdi:window-closed-variant icon: mdi:window-closed-variant
# input_boolean to simulate the heater entity switch. Only for development environment. # input_boolean to simulate the heater entity switch. Only for development environment.
fake_heater_switch3: fake_heater_switch3:
name: Heater 3 name: Heater 3
icon: mdi:radiator icon: mdi:radiator
fake_heater_switch2: fake_heater_switch2:
name: Heater 2 name: Heater 2
icon: mdi:radiator icon: mdi:radiator
fake_heater_switch1: fake_heater_switch1:
name: Heater 1 name: Heater 1
icon: mdi:radiator icon: mdi:radiator
fake_heater_4switch1: fake_heater_4switch1:
name: Heater (multiswitch1) name: Heater (multiswitch1)
icon: mdi:radiator icon: mdi:radiator
fake_heater_4switch2: fake_heater_4switch2:
name: Heater (multiswitch2) name: Heater (multiswitch2)
icon: mdi:radiator icon: mdi:radiator
fake_heater_4switch3: fake_heater_4switch3:
name: Heater (multiswitch3) name: Heater (multiswitch3)
icon: mdi:radiator icon: mdi:radiator
fake_heater_4switch4: fake_heater_4switch4:
name: Heater (multiswitch4) name: Heater (multiswitch4)
icon: mdi:radiator icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment. # input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1: fake_motion_sensor1:
name: Motion Sensor 1 name: Motion Sensor 1
icon: mdi:run icon: mdi:run
# input_boolean to simulate the presence sensor entity. Only for development environment. # input_boolean to simulate the presence sensor entity. Only for development environment.
fake_presence_sensor1: fake_presence_sensor1:
name: Presence Sensor 1 name: Presence Sensor 1
icon: mdi:home icon: mdi:home
climate: climate:
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat1 name: Underlying thermostat1
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat2 name: Underlying thermostat2
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat3 name: Underlying thermostat3
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat4 name: Underlying thermostat4
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat5 name: Underlying thermostat5
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat6 name: Underlying thermostat6
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat7 name: Underlying thermostat7
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat8 name: Underlying thermostat8
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat9 name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
recorder: recorder:
include: include:
domains: domains:
- input_boolean - input_boolean
- input_number - input_number
- switch - switch
- climate - climate
- sensor - sensor
template: template:
- binary_sensor: - binary_sensor:
- name: maison_occupee - name: maison_occupee
unique_id: maison_occupee unique_id: maison_occupee
state: "{{is_state('person.jmc', 'home') }}" state: "{{is_state('person.jmc', 'home') }}"
device_class: occupancy device_class: occupancy
- sensor: - sensor:
- name: "Total énergie switch1" - name: "Total énergie switch1"
unique_id: total_energie_switch1 unique_id: total_energie_switch1
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
device_class: energy device_class: energy
state_class: total_increasing state_class: total_increasing
state: > state: >
{% set energy = state_attr('climate.thermostat_switch_1', 'total_energy') %} {% set energy = state_attr('climate.thermostat_switch_1', 'total_energy') | float(default=-1) %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %} {% if energy < 0 %}{{none}}{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }} {{ energy | round(2, default=0) }}
{% endif %} {% endif %}
- name: "Total énergie climate 2" - name: "Total énergie climate 2"
unique_id: total_energie_climate2 unique_id: total_energie_climate2
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
device_class: energy device_class: energy
state_class: total_increasing state_class: total_increasing
state: > state: >
{% set energy = state_attr('climate.thermostat_climate_2', 'total_energy') %} {% set energy = state_attr('climate.thermostat_climate_2', 'total_energy') | float(default=-1) %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %} {% if energy < 0 %}{{none}}{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }} {{ energy | round(2, default=0) }}
{% endif %} {% endif %}
- name: "Total énergie chambre" - name: "Total énergie chambre"
unique_id: total_energie_chambre unique_id: total_energie_chambre
unit_of_measurement: "kWh" unit_of_measurement: "kWh"
device_class: energy device_class: energy
state_class: total_increasing state_class: total_increasing
state: > state: >
{% set energy = state_attr('climate.thermostat_chambre', 'total_energy') %} {% set energy = state_attr('climate.thermostat_chambre', 'total_energy') | float(default=-1) %}
{% if energy == 'unavailable' or energy is none%}unavailable{% else %} {% if energy < 0 %}{{none}}{% else %}
{{ ((energy | float) / 1.0) | round(2, default=0) }} {{ energy | round(2, default=0) }}
{% endif %} {% endif %}
switch: switch:
- platform: template - platform: template
switches: switches:
pilote_sdb_rdc: pilote_sdb_rdc:
friendly_name: "Pilote chauffage SDB RDC" friendly_name: "Pilote chauffage SDB RDC"
value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}" value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}"
turn_on: turn_on:
service: select.select_option service: select.select_option
data: data:
option: comfort option: comfort
target: target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
turn_off: turn_off:
service: select.select_option service: select.select_option
data: data:
option: comfort-2 option: comfort-2
target: target:
entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode
frontend: frontend:
themes: themes:
versatile_thermostat_theme: versatile_thermostat_theme:
state-binary_sensor-safety-on-color: "#FF0B0B" state-binary_sensor-safety-on-color: "#FF0B0B"
state-binary_sensor-power-on-color: "#FF0B0B" state-binary_sensor-power-on-color: "#FF0B0B"
state-binary_sensor-window-on-color: "rgb(156, 39, 176)" state-binary_sensor-window-on-color: "rgb(156, 39, 176)"
state-binary_sensor-motion-on-color: "rgb(156, 39, 176)" state-binary_sensor-motion-on-color: "rgb(156, 39, 176)"
state-binary_sensor-presence-on-color: "lightgreen" state-binary_sensor-presence-on-color: "lightgreen"
+2 -1
View File
@@ -8,7 +8,7 @@
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-). > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) Cette intégration de thermostat vise à simplifier considérablement vos automatisations autour de la gestion du chauffage. Parce que tous les événements autour du chauffage classiques sont gérés nativement par le thermostat (personne à la maison ?, activité détectée dans une pièce ?, fenêtre ouverte ?, délestage de courant ?), vous n'avez pas à vous encombrer de scripts et d'automatismes compliqués pour gérer vos climats. ;-).
- [Merci pour la bière buymecoffee: https://www.buymeacoffee.com/jmcollin78](#merci-pour-la-bière-buymecoffee-httpswwwbuymeacoffeecomjmcollin78) - [Merci pour la bière buymecoffee](#merci-pour-la-bière-buymecoffee)
- [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser) - [Quand l'utiliser et ne pas l'utiliser](#quand-lutiliser-et-ne-pas-lutiliser)
- [Pourquoi une nouvelle implémentation du thermostat ?](#pourquoi-une-nouvelle-implémentation-du-thermostat-) - [Pourquoi une nouvelle implémentation du thermostat ?](#pourquoi-une-nouvelle-implémentation-du-thermostat-)
- [Comment installer cet incroyable Thermostat Versatile ?](#comment-installer-cet-incroyable-thermostat-versatile-) - [Comment installer cet incroyable Thermostat Versatile ?](#comment-installer-cet-incroyable-thermostat-versatile-)
@@ -54,6 +54,7 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_ > ![Nouveau](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*Nouveautés*_
> > * **Release 3.3**: ajout du mode Air Conditionné (AC). Cette fonction vous permet d'utiliser le mode AC de votre thermostat sous-jacent. Pour l'utiliser, vous devez cocher l'option "Uitliser le mode AC" et définir les valeurs de température pour les presets et pour les presets en cas d'absence
> * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées) > * **Release 3.2** : ajout de la possibilité de commander plusieurs switch à partir du même thermostat. Dans ce mode, les switchs sont déclenchés avec un délai pour minimiser la puissance nécessaire à un instant (on minimise les périodes de recouvrement). Voir [Configuration](#sélectionnez-des-entités-pilotées)
> * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto) > * **Release 3.1** : ajout d'une détection de fenêtres/portes ouvertes par chute de température. Cette nouvelle fonction permet de stopper automatiquement un radiateur lorsque la température chute brutalement. Voir [Le mode auto](#le-mode-auto)
> * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard. > * **Release majeure 3.0** : ajout d'un équipement thermostat et de capteurs (binaires et non binaires) associés. Beaucoup plus proche de la philosphie Home Assistant, vous avez maintenant un accès direct à l'énergie consommée par le radiateur piloté par le thermostat et à plein d'autres capteurs qui seront utiles dans vos automatisations et dashboard.
+2 -1
View File
@@ -53,7 +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. This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
>![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_ >![New](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/new-icon.png?raw=true) _*News*_
> * **Release 3.2**: added the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity) > * **Release 3.3**: add the Air Conditionned mode (AC). This feature allow to use the eventual AC mode of your underlying climate entity. You have to check the "Use AC mode" checkbox in configuration and give preset temperature value for AC mode and AC mode when absent if absence is configured
> * **Release 3.2**: add the ability to control multiple switches from the same thermostat. In this mode, the switches are triggered with a delay to minimize the power required at one time (we minimize the recovery periods). See [Configuration](#select-the-driven-entity)
> * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode) > * **Release 3.1**: added detection of open windows/doors by temperature drop. This new function makes it possible to automatically stop a radiator when the temperature drops suddenly. See [Auto mode](#auto-mode)
> * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard. > * **Major release 3.0**: addition of thermostat equipment and associated sensors (binary and non-binary). Much closer to the Home Assistant philosophy, you now have direct access to the energy consumed by the radiator controlled by the thermostat and many other sensors that will be useful in your automations and dashboard.
> * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat. > * **release 2.3**: addition of the power and energy measurement of the radiator controlled by the thermostat.
@@ -18,7 +18,7 @@ from homeassistant.core import (
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity import DeviceInfo, DeviceEntryType from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.reload import async_setup_reload_service
@@ -183,7 +183,7 @@ async def async_setup_entry(
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_PRESET_TEMPERATURE, SERVICE_SET_PRESET_TEMPERATURE,
{ {
vol.Required("preset"): vol.In(CONF_PRESETS), vol.Required("preset"): vol.In(CONF_PRESETS_WITH_AC),
vol.Optional("temperature"): vol.Coerce(float), vol.Optional("temperature"): vol.Coerce(float),
vol.Optional("temperature_away"): vol.Coerce(float), vol.Optional("temperature_away"): vol.Coerce(float),
}, },
@@ -505,7 +505,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if len(presets): if len(presets):
self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE self._support_flags = SUPPORT_FLAGS | ClimateEntityFeature.PRESET_MODE
for key, val in CONF_PRESETS.items(): # TODO before presets.items(): for key, val in CONF_PRESETS.items():
if val != 0.0: if val != 0.0:
self._attr_preset_modes.append(key) self._attr_preset_modes.append(key)
@@ -966,7 +966,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# else OFF # else OFF
one_idle = False one_idle = False
for under in self._underlyings: for under in self._underlyings:
if action := under.hvac_action not in [HVACAction.IDLE, HVACAction.OFF]: if (action := under.hvac_action) not in [HVACAction.IDLE, HVACAction.OFF]:
return action return action
if under.hvac_action == HVACAction.IDLE: if under.hvac_action == HVACAction.IDLE:
one_idle = True one_idle = True
@@ -1215,6 +1215,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await under.set_hvac_mode(hvac_mode) or need_control_heating await under.set_hvac_mode(hvac_mode) or need_control_heating
) )
# If AC is on maybe we have to change the temperature in force mode
if self._ac_mode:
await self._async_set_preset_mode_internal(self._attr_preset_mode, True)
if need_control_heating and sub_need_control_heating: if need_control_heating and sub_need_control_heating:
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
@@ -1578,6 +1582,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if not new_state: if not new_state:
return return
new_hvac_mode = new_state.state
old_state = event.data.get("old_state") old_state = event.data.get("old_state")
old_hvac_action = ( old_hvac_action = (
old_state.attributes.get("hvac_action") old_state.attributes.get("hvac_action")
@@ -1590,16 +1596,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
else None else None
) )
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
_LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
new_hvac_mode = HVACMode.OFF
_LOGGER.info( _LOGGER.info(
"%s - Underlying climate changed. Event.new_state is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s", "%s - Underlying climate changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
self, self,
new_state, new_hvac_mode,
self._hvac_mode, self._hvac_mode,
new_hvac_action, new_hvac_action,
old_hvac_action, old_hvac_action,
) )
if new_state.state in [ if new_hvac_mode in [
HVACMode.OFF, HVACMode.OFF,
HVACMode.HEAT, HVACMode.HEAT,
HVACMode.COOL, HVACMode.COOL,
@@ -1607,8 +1618,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
HVACMode.DRY, HVACMode.DRY,
HVACMode.AUTO, HVACMode.AUTO,
HVACMode.FAN_ONLY, HVACMode.FAN_ONLY,
None
]: ]:
self._hvac_mode = new_state.state self._hvac_mode = new_hvac_mode
# Interpretation of hvac # Interpretation of hvac
HVAC_ACTION_ON = [ # pylint: disable=invalid-name HVAC_ACTION_ON = [ # pylint: disable=invalid-name
@@ -1647,6 +1659,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._underlying_climate_delta_t, self._underlying_climate_delta_t,
) )
# Manage new target temperature set
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)
self.update_custom_attributes() self.update_custom_attributes()
await self._async_control_heating() await self._async_control_heating()
@@ -2094,9 +2111,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
switch_cond, switch_cond,
) )
ret = False # Issue 99 - a climate is regulated by the device itself and not by VTherm. So a VTherm should never be in security !
if mode_cond and temp_cond and climate_cond: shouldClimateBeInSecurity = False # temp_cond and climate_cond
if not self._security_state: shouldSwitchBeInSecurity = temp_cond and switch_cond
shouldBeInSecurity = shouldClimateBeInSecurity or shouldSwitchBeInSecurity
shouldStartSecurity = mode_cond and not self._security_state and shouldBeInSecurity
# attr_preset_mode is not necessary normaly. It is just here to be sure
shouldStopSecurity = self._security_state and not shouldBeInSecurity and self._attr_preset_mode == PRESET_SECURITY
# Logging and event
if shouldStartSecurity:
if shouldClimateBeInSecurity:
_LOGGER.warning( _LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Set it into security mode", "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and underlying climate is %s. Set it into security mode",
self, self,
@@ -2105,10 +2131,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
delta_ext_temp, delta_ext_temp,
self.hvac_action, self.hvac_action,
) )
ret = True elif shouldSwitchBeInSecurity:
if mode_cond and temp_cond and switch_cond:
if not self._security_state:
_LOGGER.warning( _LOGGER.warning(
"%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f) is over defined value (%.2f). Set it into security mode", "%s - No temperature received for more than %.1f minutes (dt=%.1f, dext=%.1f) and on_percent (%.2f) is over defined value (%.2f). Set it into security mode",
self, self,
@@ -2118,9 +2141,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._prop_algorithm.on_percent, self._prop_algorithm.on_percent,
self._security_min_on_percent, self._security_min_on_percent,
) )
ret = True
if mode_cond and temp_cond and not self._security_state:
self.send_event( self.send_event(
EventType.TEMPERATURE_EVENT, EventType.TEMPERATURE_EVENT,
{ {
@@ -2136,8 +2157,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
}, },
) )
if not self._security_state and ret: if shouldStartSecurity:
self._security_state = ret self._security_state = True
self.save_hvac_mode() self.save_hvac_mode()
self.save_preset_mode() self.save_preset_mode()
await self._async_set_preset_mode_internal(PRESET_SECURITY) await self._async_set_preset_mode_internal(PRESET_SECURITY)
@@ -2163,18 +2184,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
}, },
) )
if ( if shouldStopSecurity:
self._security_state
and self._attr_preset_mode == PRESET_SECURITY
and not ret
):
_LOGGER.warning( _LOGGER.warning(
"%s - End of security mode. restoring hvac_mode to %s and preset_mode to %s", "%s - End of security mode. restoring hvac_mode to %s and preset_mode to %s",
self, self,
self._saved_hvac_mode, self._saved_hvac_mode,
self._saved_preset_mode, self._saved_preset_mode,
) )
self._security_state = ret self._security_state = False
# Restore hvac_mode if previously saved # Restore hvac_mode if previously saved
if self._is_over_climate or self._security_default_on_percent <= 0.0: if self._is_over_climate or self._security_default_on_percent <= 0.0:
await self.restore_hvac_mode(False) await self.restore_hvac_mode(False)
@@ -2197,7 +2214,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
}, },
) )
return ret return shouldBeInSecurity
async def _async_control_heating(self, force=False, _=None): async def _async_control_heating(self, force=False, _=None):
"""The main function used to run the calculation at each cycle""" """The main function used to run the calculation at each cycle"""
@@ -2412,8 +2429,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_preset_temperature service: versatile_thermostat.set_preset_temperature
data: data:
temperature: 17.8
preset: boost preset: boost
temperature: 17.8
temperature_away: 15 temperature_away: 15
target: target:
entity_id: climate.thermostat_2 entity_id: climate.thermostat_2
@@ -4,7 +4,8 @@ from datetime import timedelta
from homeassistant.core import HomeAssistant, callback, Event from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity, DeviceInfo, DeviceEntryType from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import async_track_state_change_event, async_call_later from homeassistant.helpers.event import async_track_state_change_event, async_call_later
from .climate import VersatileThermostat from .climate import VersatileThermostat
@@ -550,7 +550,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
"""Handle the presence management flow steps""" """Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
if self._infos[CONF_AC_MODE]: if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else: else:
schema = self.STEP_PRESENCE_DATA_SCHEMA schema = self.STEP_PRESENCE_DATA_SCHEMA
@@ -689,7 +689,7 @@ class VersatileThermostatOptionsFlowHandler(
elif self._infos[CONF_USE_PRESENCE_FEATURE]: elif self._infos[CONF_USE_PRESENCE_FEATURE]:
next_step = self.async_step_presence next_step = self.async_step_presence
if self._infos[CONF_AC_MODE]: if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA schema = self.STEP_PRESETS_WITH_AC_DATA_SCHEMA
else: else:
schema = self.STEP_PRESETS_DATA_SCHEMA schema = self.STEP_PRESETS_DATA_SCHEMA
@@ -752,7 +752,7 @@ class VersatileThermostatOptionsFlowHandler(
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input "Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
) )
if self._infos[CONF_AC_MODE]: if self._infos.get(CONF_AC_MODE) == True:
schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA schema = self.STEP_PRESENCE_WITH_AC_DATA_SCHEMA
else: else:
schema = self.STEP_PRESENCE_DATA_SCHEMA schema = self.STEP_PRESENCE_DATA_SCHEMA
@@ -1,2 +1,2 @@
homeassistant homeassistant==2023.10.1
ffmpeg ffmpeg
@@ -1,4 +1,4 @@
# -r requirements_dev.txt -r requirements_dev.txt
# aiodiscover # aiodiscover
ulid_transform ulid_transform
pytest-homeassistant-custom-component pytest-homeassistant-custom-component
@@ -1,120 +1,124 @@
reload: reload:
description: Reload all Versatile Thermostat entities. name: Reload
description: Reload all Versatile Thermostat entities.
set_presence: set_presence:
name: Set presence name: Set presence
description: Force the presence mode in thermostat description: Force the presence mode in thermostat
target: target:
entity: entity:
integration: versatile_thermostat integration: versatile_thermostat
fields: fields:
presence: presence:
name: Presence name: Presence
description: Presence setting description: Presence setting
required: true required: true
advanced: false advanced: false
example: "on" example: "on"
default: "on" default: "on"
selector: selector:
select: select:
options: options:
- "on" - "on"
- "off" - "off"
- "home" - "home"
- "not_home" - "not_home"
set_preset_temperature: set_preset_temperature:
name: Set temperature preset name: Set temperature preset
description: Change the target temperature of a preset description: Change the target temperature of a preset
target: target:
entity: entity:
integration: versatile_thermostat integration: versatile_thermostat
fields: fields:
preset: preset:
name: Preset name: Preset
description: Preset name description: Preset name
required: true required: true
advanced: false advanced: false
example: "comfort" example: "comfort"
selector: selector:
select: select:
options: options:
- "eco" - "eco"
- "comfort" - "comfort"
- "boost" - "boost"
temperature: - "eco_ac"
name: Temperature when present - "comfort_ac"
description: Target temperature for the preset when present - "boost_ac"
required: false temperature:
advanced: false name: Temperature when present
example: "19.5" description: Target temperature for the preset when present
default: "17" required: false
selector: advanced: false
number: example: "19.5"
min: 7 default: "17"
max: 35 selector:
step: 0.1 number:
unit_of_measurement: ° min: 7
mode: slider max: 35
temperature_away: step: 0.1
name: Temperature when not present unit_of_measurement: °
description: Target temperature for the preset when not present mode: slider
required: false temperature_away:
advanced: false name: Temperature when not present
example: "17" description: Target temperature for the preset when not present
default: "15" required: false
selector: advanced: false
number: example: "17"
min: 7 default: "15"
max: 35 selector:
step: 0.1 number:
unit_of_measurement: ° min: 7
mode: slider max: 35
step: 0.1
unit_of_measurement: °
mode: slider
set_security: set_security:
name: Set security name: Set security
description: Change the security parameters description: Change the security parameters
target: target:
entity: entity:
integration: versatile_thermostat integration: versatile_thermostat
fields: fields:
delay_min: delay_min:
name: Delay in minutes name: Delay in minutes
description: Maximum allowed delay in minutes between two temperature mesures description: Maximum allowed delay in minutes between two temperature mesures
required: false required: false
advanced: false advanced: false
example: "30" example: "30"
selector: selector:
number: number:
min: 0 min: 0
max: 9999 max: 9999
unit_of_measurement: "min" unit_of_measurement: "min"
mode: box mode: box
min_on_percent: min_on_percent:
name: Minimal on_percent name: Minimal on_percent
description: Minimal heating percent value for security preset activation description: Minimal heating percent value for security preset activation
required: false required: false
advanced: false advanced: false
example: "0.5" example: "0.5"
default: "0.5" default: "0.5"
selector: selector:
number: number:
min: 0 min: 0
max: 1 max: 1
step: 0.05 step: 0.05
unit_of_measurement: "%" unit_of_measurement: "%"
mode: slider mode: slider
default_on_percent: default_on_percent:
name: on_percent used in security mode name: on_percent used in security mode
description: The default heating percent value in security preset description: The default heating percent value in security preset
required: false required: false
advanced: false advanced: false
example: "0.1" example: "0.1"
default: "0.1" default: "0.1"
selector: selector:
number: number:
min: 0 min: 0
max: 1 max: 1
step: 0.05 step: 0.05
unit_of_measurement: "%" unit_of_measurement: "%"
mode: slider mode: slider
@@ -83,6 +83,32 @@ _LOGGER = logging.getLogger(__name__)
class MockClimate(ClimateEntity): class MockClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode""" """A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos, hvac_mode:HVACMode = HVACMode.OFF) -> None:
"""Initialize the thermostat."""
super().__init__()
self.hass = hass
self.platform = 'climate'
self.entity_id= self.platform+'.'+unique_id
self._attr_extra_state_attributes = {}
self._unique_id = unique_id
self._name = name
self._attr_hvac_action = HVACAction.OFF
self._attr_hvac_mode = hvac_mode
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_target_temperature = 20
self._attr_current_temperature = 15
def set_temperature(self, temperature):
""" Set the target temperature"""
self._attr_target_temperature = temperature
self.async_write_ha_state()
class MockUnavailableClimate(ClimateEntity):
"""A Mock Climate class used for Underlying climate mode"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None: def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the thermostat.""" """Initialize the thermostat."""
@@ -92,12 +118,11 @@ class MockClimate(ClimateEntity):
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
self._unique_id = unique_id self._unique_id = unique_id
self._name = name self._name = name
self._attr_hvac_action = HVACAction.OFF self._attr_hvac_action = None
self._attr_hvac_mode = HVACMode.OFF self._attr_hvac_mode = None
self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT] self._attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT]
self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_temperature_unit = UnitOfTemperature.CELSIUS
class MagicMockClimate(MagicMock): class MagicMockClimate(MagicMock):
"""A Magic Mock class for a underlying climate entity""" """A Magic Mock class for a underlying climate entity"""
@@ -455,6 +480,51 @@ async def send_climate_change_event(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
return ret return ret
async def send_climate_change_event_with_temperature(
entity: VersatileThermostat,
new_hvac_mode: HVACMode,
old_hvac_mode: HVACMode,
new_hvac_action: HVACAction,
old_hvac_action: HVACAction,
date,
temperature,
sleep=True,
):
"""Sending a new climate event simulating a change on the underlying climate state"""
_LOGGER.info(
"------- Testu: sending send_temperature_change_event, new_hvac_mode=%s old_hvac_mode=%s new_hvac_action=%s old_hvac_action=%s date=%s temperature=%s on %s",
new_hvac_mode,
old_hvac_mode,
new_hvac_action,
old_hvac_action,
date,
temperature,
entity,
)
climate_event = Event(
EVENT_STATE_CHANGED,
{
"new_state": State(
entity_id=entity.entity_id,
state=new_hvac_mode,
attributes={"hvac_action": new_hvac_action, "temperature": temperature},
last_changed=date,
last_updated=date,
),
"old_state": State(
entity_id=entity.entity_id,
state=old_hvac_mode,
attributes={"hvac_action": old_hvac_action},
last_changed=date,
last_updated=date,
),
},
)
ret = await entity._async_climate_changed(climate_event)
if sleep:
await asyncio.sleep(0.1)
return ret
def cancel_switchs_cycles(entity: VersatileThermostat): def cancel_switchs_cycles(entity: VersatileThermostat):
"""This method will cancel all running cycle on all underlying switch entity""" """This method will cancel all running cycle on all underlying switch entity"""
@@ -15,6 +15,7 @@ from custom_components.versatile_thermostat.const import (
CONF_THERMOSTAT_CLIMATE, CONF_THERMOSTAT_CLIMATE,
CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_TYPE, CONF_THERMOSTAT_TYPE,
CONF_AC_MODE,
CONF_TEMP_SENSOR, CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR, CONF_EXTERNAL_TEMP_SENSOR,
CONF_CYCLE_MIN, CONF_CYCLE_MIN,
@@ -112,6 +113,7 @@ MOCK_TH_OVER_SWITCH_TPI_CONFIG = {
MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = { MOCK_TH_OVER_CLIMATE_TYPE_CONFIG = {
CONF_CLIMATE: "climate.mock_climate", CONF_CLIMATE: "climate.mock_climate",
CONF_AC_MODE: False,
} }
MOCK_PRESETS_CONFIG = { MOCK_PRESETS_CONFIG = {
@@ -1,5 +1,5 @@
""" Test the Window management """ """ Test the Window management """
from unittest.mock import patch from unittest.mock import patch, call
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -343,3 +343,186 @@ async def test_bug_66(
assert entity.window_state == STATE_OFF assert entity.window_state == STATE_OFF
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_82(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockUnavailableClimate(hass, "mockUniqueId", "MockClimateName", {})
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
# assert entity.hvac_action is HVACAction.OFF
# assert entity.hvac_mode is HVACMode.OFF
assert entity.hvac_mode is None
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == None
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_101(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.target_temperature == entity.min_temp
assert entity.preset_mode is PRESET_NONE
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force preset mode
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT
# 2. Change the target temp of underlying thermostat
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75)
# Should have been switched to Manual preset
assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE
@@ -189,3 +189,103 @@ async def test_security_feature(hass: HomeAssistant, skip_hass_states_is_state):
# Heater is now on # Heater is now on
assert mock_heater_on.call_count == 1 assert mock_heater_on.call_count == 1
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_security_over_climate(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that when a underlying climate is not available the VTherm doesn't go into security mode"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT)
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
) as mock_find_climate:
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
def find_my_entity(entity_id) -> ClimateEntity:
"""Find my new entity"""
component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
for entity in component.entities:
if entity.entity_id == entity_id:
return entity
entity = find_my_entity("climate.theoverclimatemockname")
assert entity
assert entity.name == "TheOverClimateMockName"
assert entity._is_over_climate is True
assert entity.hvac_action is HVACAction.OFF
assert entity.hvac_mode is HVACMode.HEAT
assert entity.target_temperature == entity.min_temp
assert entity.preset_modes == [
PRESET_NONE,
PRESET_ECO,
PRESET_COMFORT,
PRESET_BOOST,
]
assert entity.preset_mode is PRESET_NONE
assert entity._security_state is False
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
assert mock_send_event.call_count == 2
mock_send_event.assert_has_calls(
[
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
call.send_event(
EventType.HVAC_MODE_EVENT,
{"hvac_mode": HVACMode.OFF},
),
]
)
assert mock_find_climate.call_count == 1
assert mock_find_climate.mock_calls[0] == call()
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
# Force security mode
assert entity._last_ext_temperature_mesure is not None
assert entity._last_temperature_mesure is not None
assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1
assert (
entity._last_ext_temperature_mesure.astimezone(tz) - now
).total_seconds() < 1
# Tries to turns on the Thermostat
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode == HVACMode.HEAT
# 2. activate security feature when date is expired
with patch(
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on:
event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 15, event_timestamp)
# Should stay False because a climate is never in security mode
assert entity.security_state is False
assert entity.preset_mode == 'none'
assert entity._saved_preset_mode == 'none'
@@ -94,7 +94,7 @@ async def test_window_management_time_not_enough(
await try_window_condition(None) await try_window_condition(None)
assert entity.window_state == STATE_OFF assert entity.window_state == STATE_OFF
await entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -234,7 +234,7 @@ async def test_window_management_time_enough(
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
# Clean the entity # Clean the entity
await entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -418,7 +418,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity # Clean the entity
await entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -561,7 +561,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
# Clean the entity # Clean the entity
await entity.remove_thermostat() entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@@ -670,4 +670,4 @@ async def test_window_auto_no_on_percent(
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
# Clean the entity # Clean the entity
await entity.remove_thermostat() entity.remove_thermostat()
@@ -180,7 +180,7 @@ class UnderlyingSwitch(UnderlyingEntity):
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
if self.is_device_active: if self.is_device_active:
await self.turn_off() await self.turn_off()
await self._cancel_cycle() self._cancel_cycle()
if self._hvac_mode != hvac_mode: if self._hvac_mode != hvac_mode:
self._hvac_mode = hvac_mode self._hvac_mode = hvac_mode
@@ -275,7 +275,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._on_time_sec, self._on_time_sec,
) )
await self._cancel_cycle() self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF: if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self) _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
@@ -327,7 +327,7 @@ class UnderlyingSwitch(UnderlyingEntity):
self._should_relaunch_control_heating, self._should_relaunch_control_heating,
self._off_time_sec, self._off_time_sec,
) )
await self._cancel_cycle() self._cancel_cycle()
if self._hvac_mode == HVACMode.OFF: if self._hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self) _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF - 2)", self)
@@ -446,7 +446,7 @@ class UnderlyingClimate(UnderlyingEntity):
def is_device_active(self): def is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
if self.is_initialized: if self.is_initialized:
return self._underlying_climate.hvac_action not in [ return self._underlying_climate.hvac_mode != HVACMode.OFF and self._underlying_climate.hvac_action not in [
HVACAction.IDLE, HVACAction.IDLE,
HVACAction.OFF, HVACAction.OFF,
] ]
+1 -1
View File
@@ -3,5 +3,5 @@
"content_in_root": false, "content_in_root": false,
"render_readme": true, "render_readme": true,
"hide_default_branch": false, "hide_default_branch": false,
"homeassistant": "2022.2.0" "homeassistant": "2023.7.3"
} }