Compare commits

..

6 Commits

Author SHA1 Message Date
Jean-Marc Collin
81900ceeea Issue #120 - presence sensor not updated 2023-10-15 19:04:46 +02:00
Jean-Marc Collin
66297c6044 FIX non catched error (see issue #120) 2023-10-15 18:39:13 +02:00
Jean-Marc Collin
f384225b0f Maj documentation + issue #115 2023-10-15 18:16:24 +02:00
Jean-Marc Collin
e6ecd100f6 Issue #119 - Set preset target temperature not updating in ac mode 2023-10-15 12:22:15 +02:00
Jean-Marc Collin
7ac49d7864 undo remove other packages. Test don't run into gitlab-ci environment 2023-10-15 10:43:59 +02:00
Jean-Marc Collin
40da04838d Bug #121 loop in over climate
* Issue #121 - loop when underlying is slow
* Issue #121 - try fix
* Release 3.5.3
* Fix tests step
2023-10-15 10:29:54 +02:00
15 changed files with 276 additions and 91 deletions

View File

@@ -114,18 +114,22 @@ climate:
name: Underlying thermostat 4-1 name: Underlying thermostat 4-1
heater: input_boolean.fake_heater_4climate1 heater: input_boolean.fake_heater_4climate1
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: true
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat 4-2 name: Underlying thermostat 4-2
heater: input_boolean.fake_heater_4climate2 heater: input_boolean.fake_heater_4climate2
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: true
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat 4-3 name: Underlying thermostat 4-3
heater: input_boolean.fake_heater_4climate3 heater: input_boolean.fake_heater_4climate3
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: true
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat 4-4 name: Underlying thermostat 4-4
heater: input_boolean.fake_heater_4climate4 heater: input_boolean.fake_heater_4climate4
target_sensor: input_number.fake_temperature_sensor1 target_sensor: input_number.fake_temperature_sensor1
ac_mode: true
- platform: generic_thermostat - platform: generic_thermostat
name: Underlying thermostat9 name: Underlying thermostat9
heater: input_boolean.fake_heater_switch3 heater: input_boolean.fake_heater_switch3

View File

@@ -31,6 +31,8 @@ jobs:
- run: black . - run: black .
tests: tests:
# Tests don't run in Gitlab ci environment
if: 0
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
name: Run tests name: Run tests
steps: steps:
@@ -41,10 +43,10 @@ jobs:
with: with:
python-version: "3.8" python-version: "3.8"
- name: Install requirements - name: Install requirements
run: python3 -m pip install -r requirements_test.txt run: cd custom_components/versatile_thermostat && python3 -m pip install -r requirements_test.txt
- name: Run tests - name: Run tests
run: | run: |
pytest \ cd custom_components/versatile_thermostat && pytest \
-qq \ -qq \
--timeout=9 \ --timeout=9 \
--durations=10 \ --durations=10 \

View File

@@ -26,6 +26,7 @@
- [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance) - [Configurer la gestion de la puissance](#configurer-la-gestion-de-la-puissance)
- [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation) - [Configurer la présence ou l'occupation](#configurer-la-présence-ou-loccupation)
- [Configuration avancée](#configuration-avancée) - [Configuration avancée](#configuration-avancée)
- [Synthèse des paramètres](#synthèse-des-paramètres)
- [Exemples de réglage](#exemples-de-réglage) - [Exemples de réglage](#exemples-de-réglage)
- [Chauffage électrique](#chauffage-électrique) - [Chauffage électrique](#chauffage-électrique)
- [Chauffage central (chauffage gaz ou fuel)](#chauffage-central-chauffage-gaz-ou-fuel) - [Chauffage central (chauffage gaz ou fuel)](#chauffage-central-chauffage-gaz-ou-fuel)
@@ -65,7 +66,7 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
> * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier. > * **release majeure 2.0** : ajout du thermostat "over climate" permettant de transformer n'importe quel thermostat en Versatile Thermostat et lui ajouter toutes les fonctions de ce dernier.
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Un grand merci à @salabur, @pvince83 and @bergoglio pour les bières. Ca fait très plaisir. Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @Kriss1670, @maia pour les bières. Ca fait très plaisir.
# Quand l'utiliser et ne pas l'utiliser # Quand l'utiliser et ne pas l'utiliser
@@ -315,6 +316,64 @@ Voir [exemple de réglages](#examples-tuning) pour avoir des exemples de réglag
5. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``, 5. Pour un usage naturel, le ``security_default_on_percent`` doit être inférieur à ``security_min_on_percent``,
6. Lorsqu'un thermostat de type ``thermostat_over_climate`` passe en mode ``security`` il est éteint. Les paramètres ``security_min_on_percent`` et ``security_default_on_percent`` ne sont alors pas utilisés. 6. Lorsqu'un thermostat de type ``thermostat_over_climate`` passe en mode ``security`` il est éteint. Les paramètres ``security_min_on_percent`` et ``security_default_on_percent`` ne sont alors pas utilisés.
## Synthèse des paramètres
| Paramètre | Libellé | "over switch" | "over climate" |
| ----------| --------| --- | ---|
| ``name`` | Nom | X | X |
| ``thermostat_type`` | Type de thermostat | X | X |
| ``temperature_sensor_entity_id`` | Temperature sensor entity id | X | - |
| ``external_temperature_sensor_entity_id`` | Température exterieure sensor entity id | X | - |
| ``cycle_min`` | Durée du cycle (minutes) | X | X |
| ``temp_min`` | Température minimale permise | X | X |
| ``temp_max`` | Température maximale permise | X | X |
| ``device_power`` | Puissance de l'équipement | X | X |
| ``use_window_feature`` | Avec détection des ouvertures | X | X |
| ``use_motion_feature`` | Avec détection de mouvement | X | X |
| ``use_power_feature`` | Avec gestion de la puissance | X | X |
| ``use_presence_feature`` | Avec détection de présence | X | X |
| ``heater_entity1_id`` | 1er radiateur | X | - |
| ``heater_entity2_id`` | 2ème radiateur | X | - |
| ``heater_entity3_id`` | 3ème radiateur | X | - |
| ``heater_entity4_id`` | 4ème radiateur | X | - |
| ``proportional_function`` | Algorithme | X | - |
| ``climate_entity1_id`` | Thermostat sous-jacent | - | X |
| ``climate_entity2_id`` | 2ème thermostat sous-jacent | - | X |
| ``climate_entity3_id`` | 3ème thermostat sous-jacent | - | X |
| ``climate_entity4_id`` | 4ème thermostat sous-jacent | - | X |
| ``ac_mode`` | utilisation de l'air conditionné (AC) ? | - | X |
| ``tpi_coef_int`` | Coefficient à utiliser pour le delta de température interne | X | - |
| ``tpi_coef_ext`` | Coefficient à utiliser pour le delta de température externe | X | - |
| ``eco_temp`` | Température en preset Eco | X | X |
| ``comfort_temp`` | Température en preset Confort | X | X |
| ``boost_temp`` | Température en preset Boost | X | X |
| ``eco_ac_temp`` | Température en preset Eco en mode AC | X | X |
| ``comfort_ac_temp`` | Température en preset Confort en mode AC | X | X |
| ``boost_ac_temp`` | Température en preset Boost en mode AC | X | X |
| ``window_sensor_entity_id`` | Détecteur d'ouverture (entity id) | X | X |
| ``window_delay`` | Délai avant extinction (secondes) | X | X |
| ``window_auto_open_threshold`` | Seuil haut de chute de température pour la détection automatique (en °/min) | X | X |
| ``window_auto_close_threshold`` | Seuil bas de chute de température pour la fin de détection automatique (en °/min) | X | X |
| ``window_auto_max_duration`` | Durée maximum d'une extinction automatique (en min) | X | X |
| ``motion_sensor_entity_id`` | Détecteur de mouvement entity id | X | X |
| ``motion_delay`` | Délai avant changement (seconds) | X | X |
| ``motion_preset`` | Preset à utiliser si mouvement détecté | X | X |
| ``no_motion_preset`` | Preset à utiliser si pas de mouvement détecté | X | X |
| ``power_sensor_entity_id`` | Capteur de puissance totale (entity id) | X | X |
| ``max_power_sensor_entity_id`` | Capteur de puissance Max (entity id) | X | X |
| ``power_temp`` | Température si délestaqe | X | X |
| ``presence_sensor_entity_id`` | Capteur de présence entity id (true si quelqu'un est présent) | X | X |
| ``eco_away_temp`` | Température en preset Eco en cas d'absence | X | X |
| ``comfort_away_temp`` | Température en preset Comfort en cas d'absence | X | X |
| ``boost_away_temp`` | Température en preset Boost en cas d'absence | X | X |
| ``eco_ac_away_temp`` | Température en preset Eco en cas d'absence en mode AC | X | X |
| ``comfort_ac_away_temp`` | Température en preset Comfort en cas d'absence en mode AC | X | X |
| ``boost_ac_away_temp`` | Température en preset Boost en cas d'absence en mode AC | X | X |
| ``minimal_activation_delay`` | Délai minimal d'activation | X | - |
| ``security_delay_min`` | Délai maximal entre 2 mesures de températures | X | - |
| ``security_min_on_percent`` | Pourcentage minimal de puissance pour passer en mode sécurité | X | - |
| ``security_default_on_percent`` | Pourcentage de puissance a utiliser en mode securité | X | - |
# Exemples de réglage # Exemples de réglage
## Chauffage électrique ## Chauffage électrique
@@ -451,6 +510,17 @@ target:
entity_id : climate.my_thermostat entity_id : climate.my_thermostat
``` ```
Ou pour changer le pré-réglage du mode Air Conditionné (AC) ajoutez un préfixe `_ac`` au nom du preset comme ceci :
```
service: versatile_thermostat.set_preset_temperature
data:
preset: boost_ac
temperature: 25
temperature_away: 30
target:
entity_id: climate.my_thermostat
```
> ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Astuce](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
- après un redémarrage, les préréglages sont réinitialisés à la température configurée. Si vous souhaitez que votre changement soit permanent, vous devez modifier le préréglage de la température dans la configuration de l'intégration. - après un redémarrage, les préréglages sont réinitialisés à la température configurée. Si vous souhaitez que votre changement soit permanent, vous devez modifier le préréglage de la température dans la configuration de l'intégration.

View File

@@ -26,6 +26,7 @@
- [Configure the power management](#configure-the-power-management) - [Configure the power management](#configure-the-power-management)
- [Configure the presence or occupancy](#configure-the-presence-or-occupancy) - [Configure the presence or occupancy](#configure-the-presence-or-occupancy)
- [Advanced configuration](#advanced-configuration) - [Advanced configuration](#advanced-configuration)
- [Parameters synthesis](#parameters-synthesis)
- [Examples tuning](#examples-tuning) - [Examples tuning](#examples-tuning)
- [Electrical heater](#electrical-heater) - [Electrical heater](#electrical-heater)
- [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system) - [Central heating (gaz or fuel heating system)](#central-heating-gaz-or-fuel-heating-system)
@@ -64,16 +65,16 @@ This custom component for Home Assistant is an upgrade and is a complete rewrite
> * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter. > * **major release 2.0**: addition of the "over climate" thermostat allowing you to transform any thermostat into a Versatile Thermostat and add all the functions of the latter.
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78) # Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
Many thanks to @salabur, @pvince83 and @bergoglio for the beers. It's very pleasing. Many thanks to @@salabur, @pvince83, @bergoglio, @EPicLURcher, @Kriss1670, @maia for the beers. It's very pleasing.
# When to use / not use # When to use / not use
This thermostat can control 2 types of equipment: This thermostat can control 2 types of equipment:
1. a heater that only works in on/off mode (named ```thermostat_over_switch```). The minimum configuration required to use this type of thermostat is: 1. a heater that only works in on/off mode (named ```thermostat_over_switch```). Versatile Thermostat will regulate the length of a heating cycle and the pauses in-between by controlling a binary on/off switch. This mode is e.g. suitable for an electrical radiator controlled by a switch. The minimum configuration required to use this type of thermostat is:
- an equipment such as a radiator (a ```switch``` or equivalent), - an equipment such as a radiator (a ```switch``` or equivalent),
- a temperature probe for the room (or an input_number), - a temperature probe for the room (or an input_number),
- an external temperature sensor (think about weather integration if you don't have one) - an external temperature sensor (think about weather integration if you don't have one)
2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). For this type of thermostat, the minimum configuration requires: 2. another thermostat that has its own operating modes (named ```thermostat_over_climate```). Versatile Thermostat will regulate the target temperature of a climate entity. Common examples for this mode are the control of thermostatic radiator valves (TRV), air-conditions (AC), floor heating systems and pellet heating. For this type of thermostat, the minimum configuration requires:
- an equipment such as air conditioning which is controlled by its own ```climate``` type entity, - an equipment such as air conditioning which is controlled by its own ```climate``` type entity,
- a temperature probe for the room (or an input_number), - a temperature probe for the room (or an input_number),
- an external temperature sensor (think about weather integration if you don't have one) - an external temperature sensor (think about weather integration if you don't have one)
@@ -300,6 +301,64 @@ See [example tuning](#examples-tuning) for common tuning examples
5. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``, 5. For natural usage, the ``security_default_on_percent`` should be less than ``security_min_on_percent``,
6. When a ``thermostat_over_climate`` type thermostat goes into ``security`` mode it is turned off. The ``security_min_on_percent`` and ``security_default_on_percent`` parameters are then not used. 6. When a ``thermostat_over_climate`` type thermostat goes into ``security`` mode it is turned off. The ``security_min_on_percent`` and ``security_default_on_percent`` parameters are then not used.
## Parameters synthesis
| Paramètre | Libellé | "over switch" | "over climate" |
| ----------| --------| --- | --- |
| ``name`` | Name | X | X |
| ``thermostat_type`` | Thermostat type | X | X |
| ``temperature_sensor_entity_id`` | Temperature sensor entity id | X | - |
| ``external_temperature_sensor_entity_id`` | External temperature sensor entity id | X | - |
| ``cycle_min`` | Cycle duration (minutes) | X | X |
| ``temp_min`` | Minimal temperature allowed | X | X |
| ``temp_max`` | Maximal temperature allowed | X | X |
| ``device_power`` | Device power | X | X |
| ``use_window_feature`` | Use window detection | X | X |
| ``use_motion_feature`` | Use motion detection | X | X |
| ``use_power_feature`` | Use power management | X | X |
| ``use_presence_feature`` | Use presence detection | X | X |
| ``heater_entity1_id`` | 1rst heater switch | X | - |
| ``heater_entity2_id`` | 2nd heater switch | X | - |
| ``heater_entity3_id`` | 3rd heater switch | X | - |
| ``heater_entity4_id`` | 4th heater switch | X | - |
| ``proportional_function`` | Algorithm | X | - |
| ``climate_entity1_id`` | 1rst underlying climate | - | X |
| ``climate_entity2_id`` | 2nd underlying climate | - | X |
| ``climate_entity3_id`` | 3rd underlying climate | - | X |
| ``climate_entity4_id`` | 4th underlying climate | - | X |
| ``ac_mode`` | Use the Air Conditioning (AC) mode | - | X |
| ``tpi_coef_int`` | Coefficient to use for internal temperature delta | X | - |
| ``tpi_coef_ext`` | Coefficient to use for external temperature delta | X | - |
| ``eco_temp`` | Temperature in Eco preset | X | X |
| ``comfort_temp`` | Temperature in Comfort preset | X | X |
| ``boost_temp`` | Temperature in Boost preset | X | X |
| ``eco_ac_temp`` | Temperature in Eco preset for AC mode | X | X |
| ``comfort_ac_temp`` | Temperature in Comfort preset for AC mode | X | X |
| ``boost_ac_temp`` | Temperature in Boost preset for AC mode | X | X |
| ``window_sensor_entity_id`` | Window sensor entity id | X | X |
| ``window_delay`` | Window sensor delay (seconds) | X | X |
| ``window_auto_open_threshold`` | Temperature decrease threshold for automatic window open detection (in °/min) | X | X |
| ``window_auto_close_threshold`` | Temperature increase threshold for end of automatic detection (in °/min) | X | X |
| ``window_auto_max_duration`` | Maximum duration of automatic window open detection (in min) | X | X |
| ``motion_sensor_entity_id`` | Motion sensor entity id | X | X |
| ``motion_delay`` | Motion delay (seconds) | X | X |
| ``motion_preset`` | Preset to use when motion is detected | X | X |
| ``no_motion_preset`` | Preset to use when no motion is detected | X | X |
| ``power_sensor_entity_id`` | Power sensor entity id | X | X |
| ``max_power_sensor_entity_id`` | Max power sensor entity id | X | X |
| ``power_temp`` | Temperature for Power shedding | X | X |
| ``presence_sensor_entity_id`` | Presence sensor entity id | X | X |
| ``eco_away_temp`` | Temperature in Eco preset when no presence | X | X |
| ``comfort_away_temp`` | Temperature in Comfort preset when no presence | X | X |
| ``boost_away_temp`` | Temperature in Boost preset when no presence | X | X |
| ``eco_ac_away_temp`` | Temperature in Eco preset when no presence in AC mode | X | X |
| ``comfort_ac_away_temp`` | Temperature in Comfort preset when no presence in AC mode | X | X |
| ``boost_ac_away_temp`` | Temperature in Boost preset when no presence in AC mode | X | X |
| ``minimal_activation_delay`` | Minimal activation delay | X | - |
| ``security_delay_min`` | Security delay (in minutes) | X | X |
| ``security_min_on_percent`` | Minimal power percent to enable security mode | X | X |
| ``security_default_on_percent`` | Power percent to use in security mode | X | X |
# Examples tuning # Examples tuning
## Electrical heater ## Electrical heater
@@ -436,6 +495,17 @@ target:
entity_id: climate.my_thermostat entity_id: climate.my_thermostat
``` ```
Or to change the preset of the AC mode, add _ac to the preset name like this:
```
service: versatile_thermostat.set_preset_temperature
data:
preset: boost_ac
temperature: 25
temperature_away: 30
target:
entity_id: climate.my_thermostat
```
> ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_ > ![Tip](https://github.com/jmcollin78/versatile_thermostat/blob/main/images/tips.png?raw=true) _*Notes*_
- after a restart the preset are resetted to the configured temperature. If you want your change to be permanent you should modify the temperature preset into the confguration of the integration. - after a restart the preset are resetted to the configured temperature. If you want your change to be permanent you should modify the temperature preset into the confguration of the integration.

View File

@@ -3,7 +3,7 @@ import logging
from homeassistant.core import HomeAssistant, callback, Event from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.const import STATE_ON from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
@@ -133,12 +133,14 @@ class WindowBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
_LOGGER.debug("%s - climate state change", self._attr_unique_id) _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on old_state = self._attr_is_on
self._attr_is_on = ( # Issue 120 - only take defined presence value
self.my_climate.window_state == STATE_ON if self.my_climate.window_state in [STATE_ON, STATE_OFF] or self.my_climate.window_auto_state in [STATE_ON, STATE_OFF]:
or self.my_climate.window_auto_state == STATE_ON self._attr_is_on = (
) self.my_climate.window_state == STATE_ON
if old_state != self._attr_is_on: or self.my_climate.window_auto_state == STATE_ON
self.async_write_ha_state() )
if old_state != self._attr_is_on:
self.async_write_ha_state()
return return
@property @property
@@ -171,9 +173,11 @@ class MotionBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
"""Called when my climate have change""" """Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id) _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on old_state = self._attr_is_on
self._attr_is_on = self.my_climate.motion_state == STATE_ON # Issue 120 - only take defined presence value
if old_state != self._attr_is_on: if self.my_climate.motion_state in [STATE_ON, STATE_OFF]:
self.async_write_ha_state() self._attr_is_on = self.my_climate.motion_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
return return
@property @property
@@ -204,9 +208,11 @@ class PresenceBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity):
_LOGGER.debug("%s - climate state change", self._attr_unique_id) _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on old_state = self._attr_is_on
self._attr_is_on = self.my_climate.presence_state == STATE_ON # Issue 120 - only take defined presence value
if old_state != self._attr_is_on: if self.my_climate.presence_state in [STATE_ON, STATE_OFF]:
self.async_write_ha_state() self._attr_is_on = self.my_climate.presence_state == STATE_ON
if old_state != self._attr_is_on:
self.async_write_ha_state()
return return
@property @property

View File

@@ -956,7 +956,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Return current operation.""" """Return current operation."""
# Issue #114 - returns my current hvac_mode and not the underlying hvac_mode which could be different # 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. # delta will be managed by climate_state_change event.
# TODO remove this when ok
# if self._is_over_climate: # if self._is_over_climate:
# if one not OFF -> return it # if one not OFF -> return it
# else OFF # else OFF
@@ -1298,7 +1297,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
def reset_last_change_time(self, old_preset_mode=None): def reset_last_change_time(self, old_preset_mode=None):
"""Reset to now the last change time""" """Reset to now the last change time"""
self._last_change_time = datetime.now(tz=self._current_tz) self._last_change_time = datetime.now(tz=self._current_tz)
_LOGGER.warning("%s - last_change_time is now %s", self, self._last_change_time) _LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
def reset_last_temperature_time(self, old_preset_mode=None): def reset_last_temperature_time(self, old_preset_mode=None):
"""Reset to now the last temperature time if conditions are satisfied""" """Reset to now the last temperature time if conditions are satisfied"""
@@ -1598,7 +1597,22 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
@callback @callback
async def _async_climate_changed(self, event): async def _async_climate_changed(self, event):
"""Handle unerdlying climate state changes.""" """Handle unerdlying climate state changes.
This method takes the underlying values and update the VTherm with them.
To avoid loops (issues #121 #101 #95 #99), we discard the event if it is received
less than 10 sec after the last command. What we want here is to take the values
from underlyings ONLY if someone have change directly on the underlying and not
as a return of the command. The only thing we take all the time is the HVACAction
which is important for feedaback and which cannot generates loops.
"""
async def end_climate_changed(changes):
""" To end the event management"""
if changes:
self.async_write_ha_state()
self.update_custom_attributes()
await self._async_control_heating()
new_state = event.data.get("new_state") new_state = event.data.get("new_state")
_LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state) _LOGGER.debug("%s - _async_climate_changed new_state is %s", self, new_state)
if not new_state: if not new_state:
@@ -1639,26 +1653,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
old_hvac_action, old_hvac_action,
) )
_LOGGER.warning("%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", self, self._last_change_time, old_state_date_changed, old_state_date_updated, new_state_date_changed, new_state_date_updated) _LOGGER.debug("%s - last_change_time=%s old_state_date_changed=%s old_state_date_updated=%s new_state_date_changed=%s new_state_date_updated=%s", self, self._last_change_time, old_state_date_changed, old_state_date_updated, new_state_date_changed, new_state_date_updated)
if new_hvac_mode in [ # Interpretation of hvac action
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.HEAT_COOL,
HVACMode.DRY,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
None
] and self._hvac_mode != new_hvac_mode:
changes = True
self._hvac_mode = new_hvac_mode
# Update all underlyings state
if self._is_over_climate:
for under in self._underlyings:
await under.set_hvac_mode(new_hvac_mode)
# Interpretation of hvac
HVAC_ACTION_ON = [ # pylint: disable=invalid-name HVAC_ACTION_ON = [ # pylint: disable=invalid-name
HVACAction.COOLING, HVACAction.COOLING,
HVACAction.DRYING, HVACAction.DRYING,
@@ -1697,19 +1694,43 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
changes = True changes = True
# Issue #120 - Some TRV are chaning target temperature a very long time (6 sec) after the change.
# In that case a loop is possible if a user change multiple times during this 6 sec.
if new_state_date_updated and self._last_change_time:
delta = (new_state_date_updated - self._last_change_time).total_seconds()
if delta < 10:
_LOGGER.info("%s - underlying event is received less than 10 sec after command. Forget it to avoid loop", self
)
await end_climate_changed(changes)
return
if new_hvac_mode in [
HVACMode.OFF,
HVACMode.HEAT,
HVACMode.COOL,
HVACMode.HEAT_COOL,
HVACMode.DRY,
HVACMode.AUTO,
HVACMode.FAN_ONLY,
None
] and self._hvac_mode != new_hvac_mode:
changes = True
self._hvac_mode = new_hvac_mode
# Update all underlyings state
if self._is_over_climate:
for under in self._underlyings:
await under.set_hvac_mode(new_hvac_mode)
if not changes: if not changes:
# try to manage new target temperature set if state # 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) _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: 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.warning("%s - Target temp have change to %s", self, new_target_temp) _LOGGER.info("%s - Target temp in underlying have change to %s", self, new_target_temp)
# TODO temporary removes the temperature changes for test await self.async_set_temperature(temperature = new_target_temp)
# await self.async_set_temperature(temperature = new_target_temp)
changes = True changes = True
if changes: await end_climate_changed(changes)
self.async_write_ha_state()
self.update_custom_attributes()
await self._async_control_heating()
@callback @callback
async def _async_update_temp(self, state: State): async def _async_update_temp(self, state: State):
@@ -2124,9 +2145,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
now - self._last_ext_temperature_mesure.replace(tzinfo=self._current_tz) now - self._last_ext_temperature_mesure.replace(tzinfo=self._current_tz)
).total_seconds() / 60.0 ).total_seconds() / 60.0
# TODO before change:
# mode_cond = self._is_over_climate or self._hvac_mode != HVACMode.OFF
# fixed into this. Why if _is_over_climate we could into security even if HVACMode is OFF ?
mode_cond = self._hvac_mode != HVACMode.OFF mode_cond = self._hvac_mode != HVACMode.OFF
temp_cond: bool = ( temp_cond: bool = (
@@ -2281,21 +2299,21 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
try: try:
under.startup() under.startup()
except UnknownEntity as err: except UnknownEntity:
# still not found, we an stop here # still not found, we an stop here
raise err return False
# Check overpowering condition # Check overpowering condition
# Not necessary for switch because each switch is checking at startup # Not necessary for switch because each switch is checking at startup
overpowering: bool = await self.check_overpowering() overpowering: bool = await self.check_overpowering()
if overpowering: if overpowering:
_LOGGER.debug("%s - End of cycle (overpowering)", self) _LOGGER.debug("%s - End of cycle (overpowering)", self)
return return True
security: bool = await self.check_security() security: bool = await self.check_security()
if security and self._is_over_climate: if security and self._is_over_climate:
_LOGGER.debug("%s - End of cycle (security and over climate)", self) _LOGGER.debug("%s - End of cycle (security and over climate)", self)
return return True
# Stop here if we are off # Stop here if we are off
if self._hvac_mode == HVACMode.OFF: if self._hvac_mode == HVACMode.OFF:
@@ -2303,7 +2321,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
# A security to force stop heater if still active # A security to force stop heater if still active
if self._is_device_active: if self._is_device_active:
await self._async_underlying_entity_turn_off() await self._async_underlying_entity_turn_off()
return return True
if not self._is_over_climate: if not self._is_over_climate:
for under in self._underlyings: for under in self._underlyings:
@@ -2315,19 +2333,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
self.update_custom_attributes() self.update_custom_attributes()
return True
def recalculate(self): def recalculate(self):
"""A utility function to force the calculation of a the algo and """A utility function to force the calculation of a the algo and
update the custom attributes and write the state update the custom attributes and write the state
""" """
if self._is_over_climate:
self.update_custom_attributes()
return
_LOGGER.debug("%s - recalculate all", self) _LOGGER.debug("%s - recalculate all", self)
self._prop_algorithm.calculate( if not self._is_over_climate:
self._target_temp, self._cur_temp, self._cur_ext_temp self._prop_algorithm.calculate(
) self._target_temp, self._cur_temp, self._cur_ext_temp
)
self.update_custom_attributes() self.update_custom_attributes()
self.async_write_ha_state() self.async_write_ha_state()
@@ -2402,13 +2418,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
.astimezone(self._current_tz) .astimezone(self._current_tz)
.isoformat(), .isoformat(),
"timezone": str(self._current_tz), "timezone": str(self._current_tz),
"window_sensor_entity_id": self._window_sensor_entity_id,
"window_delay_sec": self._window_delay_sec, "window_delay_sec": self._window_delay_sec,
"window_auto_open_threshold": self._window_auto_open_threshold, "window_auto_open_threshold": self._window_auto_open_threshold,
"window_auto_close_threshold": self._window_auto_close_threshold, "window_auto_close_threshold": self._window_auto_close_threshold,
"window_auto_max_duration": self._window_auto_max_duration, "window_auto_max_duration": self._window_auto_max_duration,
"motion_sensor_entity_id": self._motion_sensor_entity_id,
"presence_sensor_entity_id": self._presence_sensor_entity_id,
"power_sensor_entity_id": self._power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
} }
if self._is_over_climate: if self._is_over_climate:
self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[ self._attr_extra_state_attributes["underlying_climate_0"] = self._underlyings[
0 0
].entity_id ].entity_id
self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[ self._attr_extra_state_attributes["underlying_climate_1"] = self._underlyings[
@@ -2509,8 +2530,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
) )
# If the changed preset is active, change the current temperature # If the changed preset is active, change the current temperature
if self._attr_preset_mode == preset: # Issue #119 - reload new preset temperature also in ac mode
await self._async_set_preset_mode_internal(preset, force=True) if preset.startswith(self._attr_preset_mode):
await self._async_set_preset_mode_internal(preset.rstrip(PRESET_AC_SUFFIX), force=True)
await self._async_control_heating(force=True) await self._async_control_heating(force=True)
async def service_set_security(self, delay_min, min_on_percent, default_on_percent): async def service_set_security(self, delay_min, min_on_percent, default_on_percent):

View File

@@ -14,6 +14,6 @@
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [], "requirements": [],
"ssdp": [], "ssdp": [],
"version": "3.0.0", "version": "3.5.3",
"zeroconf": [] "zeroconf": []
} }

View File

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

View File

@@ -1,4 +1,5 @@
# Warning: For automatic run of test in Gitlab CI, we must not include other things that pytest-homeassistant-custom-component
-r requirements_dev.txt -r requirements_dev.txt
# aiodiscover aiodiscover
ulid_transform ulid_transform
pytest-homeassistant-custom-component pytest-homeassistant-custom-component

View File

@@ -14,7 +14,7 @@
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximal temperature allowed",
"device_power": "Device power (kW)", "device_power": "Device power",
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
@@ -25,12 +25,12 @@
"title": "Linked entities", "title": "Linked entities",
"description": "Linked entities attributes", "description": "Linked entities attributes",
"data": { "data": {
"heater_entity_id": "Heater switch", "heater_entity_id": "1rst heater switch",
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "Underlying climate", "climate_entity_id": "1rst underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate", "climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate", "climate_entity4_id": "4th underlying climate",
@@ -110,7 +110,7 @@
"title": "Presence management", "title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)", "presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence", "eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence", "comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence", "boost_away_temp": "Temperature in Boost preset when no presence",
@@ -125,7 +125,7 @@
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)", "security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent for security mode", "security_min_on_percent": "Minimal power percent to enable security mode",
"security_default_on_percent": "Power percent to use in security mode" "security_default_on_percent": "Power percent to use in security mode"
}, },
"data_description": { "data_description": {

View File

@@ -62,11 +62,9 @@ async def test_bug_56(
# try to call _async_control_heating # try to call _async_control_heating
try: try:
await entity._async_control_heating() ret = await entity._async_control_heating()
# an exception should be send # an exception should be send
assert False assert ret is False
except UnknownEntity:
pass
except Exception: # pylint: disable=broad-exception-caught except Exception: # pylint: disable=broad-exception-caught
assert False assert False
@@ -530,9 +528,16 @@ async def test_bug_101(
await entity.async_set_preset_mode(PRESET_COMFORT) await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.preset_mode == PRESET_COMFORT assert entity.preset_mode == PRESET_COMFORT
# 2. Change the target temp of underlying thermostat # 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75) 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 # Should NOT have been switched to Manual preset
assert entity.target_temperature == 17
assert entity.preset_mode is PRESET_COMFORT
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
# Wait 11 sec
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, event_timestamp, 12.75)
assert entity.target_temperature == 12.75 assert entity.target_temperature == 12.75
assert entity.preset_mode is PRESET_NONE assert entity.preset_mode is PRESET_NONE

View File

@@ -163,7 +163,7 @@ async def test_one_switch_cycle(
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert mock_heater_on.call_count == 1 assert mock_heater_on.call_count == 1
# TODO normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False # normal ? assert entity.underlying_entity(0)._should_relaunch_control_heating is False
# Simulate the end of heater on cycle # Simulate the end of heater on cycle
event_timestamp = now - timedelta(minutes=3) event_timestamp = now - timedelta(minutes=3)
@@ -522,7 +522,9 @@ async def test_multiple_climates_underlying_changes(
), patch( ), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
) as mock_underlying_set_hvac_mode: ) as mock_underlying_set_hvac_mode:
await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, now) # Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(entity, HVACMode.OFF, HVACMode.HEAT, HVACAction.OFF, HVACAction.HEATING, event_timestamp)
# Should be call for all Switch # Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4 assert mock_underlying_set_hvac_mode.call_count == 4
@@ -543,7 +545,9 @@ async def test_multiple_climates_underlying_changes(
# notice that there is no need of return_value=HVACAction.IDLE because this is not a function but a property # 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 "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.hvac_action", HVACAction.IDLE
) as mock_underlying_get_hvac_action: ) as mock_underlying_get_hvac_action:
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, now) # Wait 11 sec so that the event will not be discarded
event_timestamp = now + timedelta(seconds=11)
await send_climate_change_event(entity, HVACMode.HEAT, HVACMode.OFF, HVACAction.IDLE, HVACAction.OFF, event_timestamp)
# Should be call for all Switch # Should be call for all Switch
assert mock_underlying_set_hvac_mode.call_count == 4 assert mock_underlying_set_hvac_mode.call_count == 4

View File

@@ -14,7 +14,7 @@
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"temp_min": "Minimal temperature allowed", "temp_min": "Minimal temperature allowed",
"temp_max": "Maximal temperature allowed", "temp_max": "Maximal temperature allowed",
"device_power": "Device power (kW)", "device_power": "Device power",
"use_window_feature": "Use window detection", "use_window_feature": "Use window detection",
"use_motion_feature": "Use motion detection", "use_motion_feature": "Use motion detection",
"use_power_feature": "Use power management", "use_power_feature": "Use power management",
@@ -25,12 +25,12 @@
"title": "Linked entities", "title": "Linked entities",
"description": "Linked entities attributes", "description": "Linked entities attributes",
"data": { "data": {
"heater_entity_id": "Heater switch", "heater_entity_id": "1rst heater switch",
"heater_entity2_id": "2nd heater switch", "heater_entity2_id": "2nd heater switch",
"heater_entity3_id": "3rd heater switch", "heater_entity3_id": "3rd heater switch",
"heater_entity4_id": "4th heater switch", "heater_entity4_id": "4th heater switch",
"proportional_function": "Algorithm", "proportional_function": "Algorithm",
"climate_entity_id": "Underlying climate", "climate_entity_id": "1rst underlying climate",
"climate_entity2_id": "2nd underlying climate", "climate_entity2_id": "2nd underlying climate",
"climate_entity3_id": "3rd underlying climate", "climate_entity3_id": "3rd underlying climate",
"climate_entity4_id": "4th underlying climate", "climate_entity4_id": "4th underlying climate",
@@ -110,7 +110,7 @@
"title": "Presence management", "title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.", "description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": { "data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)", "presence_sensor_entity_id": "Presence sensor entity id",
"eco_away_temp": "Temperature in Eco preset when no presence", "eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence", "comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence", "boost_away_temp": "Temperature in Boost preset when no presence",
@@ -125,7 +125,7 @@
"data": { "data": {
"minimal_activation_delay": "Minimal activation delay", "minimal_activation_delay": "Minimal activation delay",
"security_delay_min": "Security delay (in minutes)", "security_delay_min": "Security delay (in minutes)",
"security_min_on_percent": "Minimal power percent for security mode", "security_min_on_percent": "Minimal power percent to enable security mode",
"security_default_on_percent": "Power percent to use in security mode" "security_default_on_percent": "Power percent to use in security mode"
}, },
"data_description": { "data_description": {

View File

@@ -8,8 +8,9 @@
"description": "Principaux attributs obligatoires", "description": "Principaux attributs obligatoires",
"data": { "data": {
"name": "Nom", "name": "Nom",
"thermostat_type": "Type de thermostat",
"temperature_sensor_entity_id": "Température sensor entity id", "temperature_sensor_entity_id": "Température sensor entity id",
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id", "external_temperature_sensor_entity_id": "Température exterieure sensor entity id",
"cycle_min": "Durée du cycle (minutes)", "cycle_min": "Durée du cycle (minutes)",
"temp_min": "Température minimale permise", "temp_min": "Température minimale permise",
"temp_max": "Température maximale permise", "temp_max": "Température maximale permise",
@@ -74,7 +75,7 @@
"data": { "data": {
"window_sensor_entity_id": "Détecteur d'ouverture (entity id)", "window_sensor_entity_id": "Détecteur d'ouverture (entity id)",
"window_delay": "Délai avant extinction (secondes)", "window_delay": "Délai avant extinction (secondes)",
"window_auto_open_threshold": "seuil haut de chute de température pour la détection automatique (en °/min)", "window_auto_open_threshold": "Seuil haut de chute de température pour la détection automatique (en °/min)",
"window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)", "window_auto_close_threshold": "Seuil bas de chute de température pour la fin de détection automatique (en °/min)",
"window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)" "window_auto_max_duration": "Durée maximum d'une extinction automatique (en min)"
}, },

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": "2023.7.3" "homeassistant": "2023.10.3"
} }