Compare commits
9 Commits
5.3.3
...
5.4.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76382ebb35 | ||
|
|
90f9a0e1e3 | ||
|
|
ed977b53cd | ||
|
|
5d453393f8 | ||
|
|
d2f2ab7804 | ||
|
|
b0b6d0478d | ||
|
|
f8a2c9baa9 | ||
|
|
8cbd81012c | ||
|
|
26844593b1 |
11
.github/workflows/testus.yaml
vendored
11
.github/workflows/testus.yaml
vendored
@@ -42,9 +42,8 @@ jobs:
|
||||
|
||||
- name: Generate HTML Coverage Report
|
||||
run: coverage html
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./htmlcov
|
||||
# - name: Deploy to GitHub Pages
|
||||
# uses: peaceiris/actions-gh-pages@v3
|
||||
# with:
|
||||
# github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# publish_dir: ./htmlcov
|
||||
|
||||
@@ -84,14 +84,15 @@ Ce composant personnalisé pour Home Assistant est une mise à niveau et est une
|
||||
|
||||
|
||||
>  _*Nouveautés*_
|
||||
> * **Release 5.4** : Ajout du pas de température [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311). Ajout de seuils de régulation pour les `over_valve` pour éviter de trop vider la batterie des TRV [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338)
|
||||
> * **Release 5.3** : Ajout d'une fonction de pilotage d'une chaudière centrale [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - plus d'infos ici: [Le contrôle d'une chaudière centrale](#le-contrôle-dune-chaudière-centrale). Ajout de la possibilité de désactiver le mode sécurité pour le thermomètre extérieur [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
|
||||
> * **Release 5.2** : Ajout d'un `central_mode` permettant de piloter tous les VTherms de façon centralisée [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1** : Limitation des valeurs envoyées aux valves et au température envoyées au climate sous-jacent.
|
||||
> * **Release 5.0** : Ajout d'une configuration centrale permettant de mettre en commun les attributs qui peuvent l'être [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
<details>
|
||||
<summary>Autres versions</summary>
|
||||
|
||||
> * **Release 4.3** : Ajout d'un mode auto-fan pour le type `over_climate` permettant d'activer la ventilation si l'écart de température est important [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
> * **Release 4.2** : Le calcul de la pente de la courbe de température se fait maintenant en °/heure et non plus en °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction de la détection automatique des ouvertures par l'ajout d'un lissage de la courbe de température .
|
||||
> * **Release 4.1** : Ajout d'un mode de régulation **Expert** dans lequel l'utilisateur peut spécifier ses propres paramètres d'auto-régulation au lieu d'utiliser les pre-programmés [#194](https://github.com/jmcollin78/versatile_thermostat/issues/194).
|
||||
> * **Release 4.0** : Ajout de la prise en charge de la **Versatile Thermostat UI Card**. Voir [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Ajout d'un mode de régulation **Slow** pour les appareils de chauffage à latence lente [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Changement de la façon dont **la puissance est calculée** dans le cas de VTherm avec des équipements multi-sous-jacents [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Ajout de la prise en charge de AC et Heat pour VTherm via un interrupteur également [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
|
||||
@@ -127,7 +128,7 @@ En conséquence toute la phase de paramètrage d'un VTherm a été profondemment
|
||||
**Note :** les copies d'écran de la configuration d'un VTherm n'ont pas été mises à jour.
|
||||
|
||||
# Merci pour la bière [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
Un grand merci à @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG @Mexx62, @Someone, @Lajull, @giopeco pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
|
||||
|
||||
|
||||
# Quand l'utiliser et ne pas l'utiliser
|
||||
@@ -150,6 +151,7 @@ Certains thermostat de type TRV sont réputés incompatibles avec le Versatile T
|
||||
2. Les thermostats « Homematic » (et éventuellement Homematic IP) sont connus pour rencontrer des problèmes avec le Versatile Thermostat en raison des limitations du protocole RF sous-jacent. Ce problème se produit particulièrement lorsque vous essayez de contrôler plusieurs thermostats Homematic à la fois dans une seule instance de VTherm. Afin de réduire la charge du cycle de service, vous pouvez par ex. regroupez les thermostats avec des procédures spécifiques à Homematic (par exemple en utilisant un thermostat mural) et laissez Versatile Thermostat contrôler uniquement le thermostat mural directement. Une autre option consiste à contrôler un seul thermostat et à propager les changements de mode CVC et de température par un automatisme,
|
||||
3. les thermostats de type Heatzy qui ne supportent pas les commandes de type set_temperature
|
||||
4. les thermostats de type Rointe ont tendance a se réveiller tout seul. Le reste fonctionne normalement.
|
||||
5. les TRV de type Aqara SRTS-A01 qui n'ont pas le retour d'état `hvac_action` permettant de savoir si elle chauffe ou pas. Donc les retours d'état sont faussés, le reste à l'air fonctionnel.
|
||||
|
||||
# Pourquoi une nouvelle implémentation du thermostat ?
|
||||
|
||||
|
||||
@@ -84,14 +84,15 @@
|
||||
This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
|
||||
|
||||
> _*News*_
|
||||
> * **Release 5.4**: Added a temperature step [#311](https://github.com/jmcollin78/versatile_thermostat/issues/311). Added some regulation thresholdfor `over_valve` VTherm in order to avoid drowing the battery of TRV devices [#338](https://github.com/jmcollin78/versatile_thermostat/issues/338).
|
||||
> * **Release 5.3**: Added a central boiler control function [#234](https://github.com/jmcollin78/versatile_thermostat/issues/234) - more information here: [Controlling a central boiler](#controlling-a-central-boiler). Added the ability to disable security mode for outdoor thermometer [#343](https://github.com/jmcollin78/versatile_thermostat/issues/343)
|
||||
> * **Release 5.2**: Added a `central_mode` allowing all VTherms to be controlled centrally [#158](https://github.com/jmcollin78/versatile_thermostat/issues/158).
|
||||
> * **Release 5.1**: Limitation of the values sent to the valves and the temperature sent to the underlying climate.
|
||||
> * **Release 5.0**: Added a central configuration allowing the sharing of attributes that can be shared [#239](https://github.com/jmcollin78/versatile_thermostat/issues/239).
|
||||
> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
<details>
|
||||
<summary>Others releases</summary>
|
||||
|
||||
> * **Release 4.3**: Added an auto-fan mode for the `over_climate` type allowing ventilation to be activated if the temperature difference is significant [#223](https://github.com/jmcollin78/versatile_thermostat/issues/223).
|
||||
> * **Release 4.2**: The calculation of the slope of the temperature curve is now done in °/hour and no longer in °/min [#242](https://github.com/jmcollin78/versatile_thermostat/issues/242). Correction of automatic detection of openings by adding smoothing of the temperature curve.
|
||||
> * **Release 4.1**: Added an **Expert** regulation mode in which the user can specify their own auto-regulation parameters instead of using the pre-programmed ones [#194]( https://github.com/jmcollin78/versatile_thermostat/issues/194).
|
||||
> * **Release 4.0**: Added the support of **Versatile Thermostat UI Card**. See [Versatile Thermostat UI Card](https://github.com/jmcollin78/versatile-thermostat-ui-card). Added a **Slow** regulation mode for slow latency heating devices [#168](https://github.com/jmcollin78/versatile_thermostat/issues/168). Change the way **the power is calculated** in case of VTherm with multi-underlying equipements [#146](https://github.com/jmcollin78/versatile_thermostat/issues/146). Added the support of AC and Heat for VTherm over switch alse [#144](https://github.com/jmcollin78/versatile_thermostat/pull/144)
|
||||
@@ -127,7 +128,7 @@ Consequently, the entire configuration phase of a VTherm has been profoundly mod
|
||||
**Note:** the VTherm configuration screenshots have not been updated.
|
||||
|
||||
# Thanks for the beer [buymecoffee](https://www.buymeacoffee.com/jmcollin78)
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull for the beers. It's very nice and encourages me to continue!
|
||||
Many thanks to @salabur, @pvince83, @bergoglio, @EPicLURcher, @ecolorado66, @Kriss1670, @maia, @f.maymil, @moutte69, @Jerome, @Gunnar M, @Greg.o, @John Burgess, @abyssmal, @capinfo26, @Helge, @MattG, @MattG, @Mexx62, @Someone, @Lajull, @giopeco for the beers. It's very nice and encourages me to continue!
|
||||
|
||||
# When to use / not use
|
||||
This thermostat can control 3 types of equipment:
|
||||
@@ -150,6 +151,7 @@ Some TRV type thermostats are known to be incompatible with the Versatile Thermo
|
||||
2. "Homematic" (and possible Homematic IP) thermostats are known to have problems with Versatile Thermostats because of limitations of the underlying RF protocol. This problem especially occurs when trying to control several Homematic thermostats at once in one Versatile Thermostat instance. In order to reduce duty cycle load, you may e.g. group thermostats with Homematic-specific procedures (e.g. using a wall thermostat) and let Versatile Thermostat only control the wall thermostat directly. Another option is to control only one thermostat and propagate the changes in HVAC mode and temperature by an automation.
|
||||
3. Thermostat of type Heatzy which doesn't supports the set_temperature command.
|
||||
4. Thermostats of type Rointe tends to awake alone even if VTherm turns it off. Others functions works fine.
|
||||
5. TRV of type Aqara SRTS-A01 which doesn't have the return state `hvac_action` allowing to know if it is heating or not. So return states are not available. Others features, seems to work normally.
|
||||
|
||||
# Why another thermostat implementation ?
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import math
|
||||
import logging
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.core import (
|
||||
@@ -20,10 +22,12 @@ from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_call_later,
|
||||
EventStateChangedData,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import ConditionError
|
||||
@@ -134,6 +138,7 @@ from .open_window_algorithm import WindowOpenDetectionAlgorithm
|
||||
from .ema import ExponentialMovingAverage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ConfigData = MappingProxyType[str, Any]
|
||||
|
||||
|
||||
def get_tz(hass: HomeAssistant):
|
||||
@@ -145,20 +150,6 @@ def get_tz(hass: HomeAssistant):
|
||||
class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Representation of a base class for all Versatile Thermostat device."""
|
||||
|
||||
# The list of VersatileThermostat entities
|
||||
_hass: HomeAssistant
|
||||
_last_temperature_measure: datetime
|
||||
_last_ext_temperature_measure: datetime
|
||||
_total_energy: float
|
||||
_overpowering_state: bool
|
||||
_window_state: bool
|
||||
_motion_state: bool
|
||||
_presence_state: bool
|
||||
_window_auto_state: bool
|
||||
_window_bypass_state: bool
|
||||
_underlyings: list[UnderlyingEntity]
|
||||
_last_change_time: datetime
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
ClimateEntity._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
@@ -211,7 +202,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id: str,
|
||||
name: str,
|
||||
entry_infos: ConfigData,
|
||||
):
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
super().__init__()
|
||||
@@ -276,7 +273,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._last_change_time = None
|
||||
|
||||
self._underlyings = []
|
||||
self._underlyings: list[UnderlyingEntity] = []
|
||||
|
||||
self._ema_temp = None
|
||||
self._ema_algo = None
|
||||
@@ -290,7 +287,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self.post_init(entry_infos)
|
||||
|
||||
def clean_central_config_doublon(self, config_entry, central_config) -> dict:
|
||||
def clean_central_config_doublon(
|
||||
self, config_entry: ConfigData, central_config: ConfigEntry | None
|
||||
) -> dict[str, Any]:
|
||||
"""Removes all values from config with are concerned by central_config"""
|
||||
|
||||
def clean_one(cfg, schema: vol.Schema):
|
||||
@@ -336,7 +335,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
return entry_infos
|
||||
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Finish the initialization of the thermostast"""
|
||||
|
||||
_LOGGER.info(
|
||||
@@ -355,9 +354,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._ac_mode = entry_infos.get(CONF_AC_MODE) is True
|
||||
self._attr_max_temp = entry_infos.get(CONF_TEMP_MAX)
|
||||
self._attr_min_temp = entry_infos.get(CONF_TEMP_MIN)
|
||||
if (step := entry_infos.get(CONF_STEP_TEMPERATURE)) is not None:
|
||||
self._attr_target_temperature_step = step
|
||||
|
||||
# convert entry_infos into usable attributes
|
||||
presets = {}
|
||||
presets: dict[str, Any] = {}
|
||||
items = CONF_PRESETS_WITH_AC.items() if self._ac_mode else CONF_PRESETS.items()
|
||||
for key, value in items:
|
||||
_LOGGER.debug("looking for key=%s, value=%s", key, value)
|
||||
@@ -369,7 +370,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._attr_max_temp if self._ac_mode else self._attr_min_temp
|
||||
)
|
||||
|
||||
presets_away = {}
|
||||
presets_away: dict[str, Any] = {}
|
||||
items = (
|
||||
CONF_PRESETS_AWAY_WITH_AC.items()
|
||||
if self._ac_mode
|
||||
@@ -400,8 +401,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._proportional_function = entry_infos.get(CONF_PROP_FUNCTION)
|
||||
self._temp_sensor_entity_id = entry_infos.get(CONF_TEMP_SENSOR)
|
||||
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
|
||||
# Default value not configurable
|
||||
self._attr_target_temperature_step = 0.1
|
||||
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
|
||||
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
||||
self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
|
||||
@@ -819,7 +818,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
def init_underlyings(self):
|
||||
"""Initialize all underlyings. Should be overriden if necessary"""
|
||||
|
||||
def restore_specific_previous_state(self, old_state):
|
||||
def restore_specific_previous_state(self, old_state: State):
|
||||
"""Should be overriden in each specific thermostat
|
||||
if a specific previous state or attribute should be
|
||||
restored
|
||||
@@ -900,7 +899,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._hvac_mode,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@property
|
||||
@@ -930,19 +929,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
def should_poll(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""List of available operation modes."""
|
||||
return self._hvac_list
|
||||
|
||||
@@ -1030,17 +1029,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
return self._is_used_by_central_boiler
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def is_device_active(self):
|
||||
def is_device_active(self) -> bool:
|
||||
"""Returns true if one underlying is active"""
|
||||
for under in self._underlyings:
|
||||
if under.is_device_active:
|
||||
@@ -1048,7 +1047,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
|
||||
@@ -1217,7 +1216,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""Turn auxiliary heater off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode, need_control_heating=True):
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode, need_control_heating=True):
|
||||
"""Set new target hvac mode."""
|
||||
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
|
||||
|
||||
@@ -1253,7 +1252,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
|
||||
|
||||
@overrides
|
||||
async def async_set_preset_mode(self, preset_mode, overwrite_saved_preset=True):
|
||||
async def async_set_preset_mode(
|
||||
self, preset_mode: str, overwrite_saved_preset=True
|
||||
):
|
||||
"""Set new preset mode."""
|
||||
await self._async_set_preset_mode_internal(
|
||||
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
|
||||
@@ -1261,7 +1262,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_set_preset_mode_internal(
|
||||
self, preset_mode, force=False, overwrite_saved_preset=True
|
||||
self, preset_mode: str, force=False, overwrite_saved_preset=True
|
||||
):
|
||||
"""Set new preset mode."""
|
||||
_LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
|
||||
@@ -1310,13 +1311,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self.send_event(EventType.PRESET_EVENT, {"preset": self._attr_preset_mode})
|
||||
|
||||
def reset_last_change_time(
|
||||
self, old_preset_mode=None
|
||||
self, old_preset_mode: str | None = None
|
||||
): # pylint: disable=unused-argument
|
||||
"""Reset to now the last change time"""
|
||||
self._last_change_time = datetime.now(tz=self._current_tz)
|
||||
_LOGGER.debug("%s - last_change_time is now %s", self, self._last_change_time)
|
||||
|
||||
def reset_last_temperature_time(self, old_preset_mode=None):
|
||||
def reset_last_temperature_time(self, old_preset_mode: str | None = None):
|
||||
"""Reset to now the last temperature time if conditions are satisfied"""
|
||||
if (
|
||||
self._attr_preset_mode not in HIDDEN_PRESETS
|
||||
@@ -1326,7 +1327,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self._last_ext_temperature_measure
|
||||
) = datetime.now(tz=self._current_tz)
|
||||
|
||||
def find_preset_temp(self, preset_mode):
|
||||
def find_preset_temp(self, preset_mode: str):
|
||||
"""Find the right temperature of a preset considering the presence if configured"""
|
||||
if preset_mode is None or preset_mode == "none":
|
||||
return (
|
||||
@@ -1362,11 +1363,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
else:
|
||||
return self._presets_away[self.get_preset_away_name(preset_mode)]
|
||||
|
||||
def get_preset_away_name(self, preset_mode):
|
||||
def get_preset_away_name(self, preset_mode: str) -> str:
|
||||
"""Get the preset name in away mode (when presence is off)"""
|
||||
return preset_mode + PRESET_AWAY_SUFFIX
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
async def async_set_fan_mode(self, fan_mode: str):
|
||||
"""Set new target fan mode."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||
return
|
||||
@@ -1376,7 +1377,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
|
||||
return
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
async def async_set_swing_mode(self, swing_mode: str):
|
||||
"""Set new target swing operation."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
|
||||
return
|
||||
@@ -1393,14 +1394,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
self.reset_last_change_time()
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
async def _async_internal_set_temperature(self, temperature: float):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any
|
||||
For testing purpose you can pass an event_timestamp.
|
||||
"""
|
||||
self._target_temp = temperature
|
||||
return
|
||||
|
||||
def get_state_date_or_now(self, state: State):
|
||||
def get_state_date_or_now(self, state: State) -> datetime:
|
||||
"""Extract the last_changed state from State or return now if not available"""
|
||||
return (
|
||||
state.last_changed.astimezone(self._current_tz)
|
||||
@@ -1408,7 +1409,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
else datetime.now(tz=self._current_tz)
|
||||
)
|
||||
|
||||
def get_last_updated_date_or_now(self, state: State):
|
||||
def get_last_updated_date_or_now(self, state: State) -> datetime:
|
||||
"""Extract the last_changed state from State or return now if not available"""
|
||||
return (
|
||||
state.last_updated.astimezone(self._current_tz)
|
||||
@@ -1693,7 +1694,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.error("Unable to update external temperature from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_power_changed(self, event):
|
||||
async def _async_power_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
"""Handle power changes."""
|
||||
_LOGGER.debug("Thermostat %s - Receive new Power event", self.name)
|
||||
_LOGGER.debug(event)
|
||||
@@ -1719,7 +1720,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_max_power_changed(self, event):
|
||||
async def _async_max_power_changed(
|
||||
self, event: HASSEventType[EventStateChangedData]
|
||||
):
|
||||
"""Handle power max changes."""
|
||||
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
|
||||
_LOGGER.debug(event)
|
||||
@@ -1744,7 +1747,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
async def _async_presence_changed(self, event):
|
||||
async def _async_presence_changed(
|
||||
self, event: HASSEventType[EventStateChangedData]
|
||||
):
|
||||
"""Handle presence changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
@@ -1760,7 +1765,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self._async_update_presence(new_state.state)
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def _async_update_presence(self, new_state):
|
||||
async def _async_update_presence(self, new_state: str):
|
||||
_LOGGER.info("%s - Updating presence. New state is %s", self, new_state)
|
||||
self._presence_state = (
|
||||
STATE_ON if new_state in (STATE_ON, STATE_HOME) else STATE_OFF
|
||||
@@ -1873,7 +1878,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
|
||||
if self.window_bypass_state or not self.is_window_auto_enabled:
|
||||
_LOGGER.info(
|
||||
_LOGGER.debug(
|
||||
"%s - Window auto event is ignored because bypass is ON or window auto detection is disabled",
|
||||
self,
|
||||
)
|
||||
@@ -2058,7 +2063,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
return self._overpowering_state
|
||||
|
||||
async def check_central_mode(self, new_central_mode, old_central_mode) -> None:
|
||||
async def check_central_mode(
|
||||
self, new_central_mode: str | None, old_central_mode: str | None
|
||||
):
|
||||
"""Take into account a central mode change"""
|
||||
if not self.is_controlled_by_central_mode:
|
||||
self._last_central_mode = None
|
||||
@@ -2348,7 +2355,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
else: # default is to turn_off
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
async def async_control_heating(self, force=False, _=None):
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -2416,7 +2423,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
def update_custom_attributes(self):
|
||||
"""Update the custom extra attributes for the entity"""
|
||||
|
||||
self._attr_extra_state_attributes: dict(str, str) = {
|
||||
self._attr_extra_state_attributes: dict[str, Any] = {
|
||||
"is_on": self.is_on,
|
||||
"hvac_action": self.hvac_action,
|
||||
"hvac_mode": self.hvac_mode,
|
||||
@@ -2497,7 +2504,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
"""
|
||||
_LOGGER.info("%s - The config entry have been updated")
|
||||
|
||||
async def service_set_presence(self, presence):
|
||||
async def service_set_presence(self, presence: str):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_presence
|
||||
data:
|
||||
@@ -2510,7 +2517,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating(force=True)
|
||||
|
||||
async def service_set_preset_temperature(
|
||||
self, preset, temperature=None, temperature_away=None
|
||||
self,
|
||||
preset: str,
|
||||
temperature: float | None = None,
|
||||
temperature_away: float | None = None,
|
||||
):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_preset_temperature
|
||||
@@ -2548,7 +2558,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
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: int | None,
|
||||
min_on_percent: float | None,
|
||||
default_on_percent: float | None,
|
||||
):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_security
|
||||
data:
|
||||
@@ -2578,7 +2593,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
|
||||
await self.async_control_heating()
|
||||
self.update_custom_attributes()
|
||||
|
||||
async def service_set_window_bypass_state(self, window_bypass):
|
||||
async def service_set_window_bypass_state(self, window_bypass: bool):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_window_bypass
|
||||
data:
|
||||
|
||||
@@ -61,6 +61,7 @@ STEP_CENTRAL_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
),
|
||||
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
|
||||
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
|
||||
vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float),
|
||||
vol.Required(CONF_ADD_CENTRAL_BOILER_CONTROL, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
@@ -72,6 +73,7 @@ STEP_CENTRAL_SPEC_MAIN_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||
),
|
||||
vol.Required(CONF_TEMP_MIN, default=7): vol.Coerce(float),
|
||||
vol.Required(CONF_TEMP_MAX, default=35): vol.Coerce(float),
|
||||
vol.Required(CONF_STEP_TEMPERATURE, default=0.1): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -164,6 +166,8 @@ STEP_THERMOSTAT_VALVE = vol.Schema( # pylint: disable=invalid-name
|
||||
]
|
||||
),
|
||||
vol.Optional(CONF_AC_MODE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_AUTO_REGULATION_DTEMP, default=10): vol.Coerce(float),
|
||||
vol.Optional(CONF_AUTO_REGULATION_PERIOD_MIN, default=5): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ CONF_AUTO_FAN_LOW = "auto_fan_low"
|
||||
CONF_AUTO_FAN_MEDIUM = "auto_fan_medium"
|
||||
CONF_AUTO_FAN_HIGH = "auto_fan_high"
|
||||
CONF_AUTO_FAN_TURBO = "auto_fan_turbo"
|
||||
CONF_STEP_TEMPERATURE = "step_temperature"
|
||||
|
||||
# Global params into configuration.yaml
|
||||
CONF_SHORT_EMA_PARAMS = "short_ema_params"
|
||||
@@ -270,6 +271,7 @@ ALL_CONF = (
|
||||
CONF_CENTRAL_BOILER_ACTIVATION_SRV,
|
||||
CONF_CENTRAL_BOILER_DEACTIVATION_SRV,
|
||||
CONF_WINDOW_ACTION,
|
||||
CONF_STEP_TEMPERATURE,
|
||||
]
|
||||
+ CONF_PRESETS_VALUES
|
||||
+ CONF_PRESETS_AWAY_VALUES
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "5.3.0",
|
||||
"version": "5.4.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -14,8 +14,10 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.base_thermostat import (
|
||||
BaseThermostat,
|
||||
ConfigData,
|
||||
)
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DEVICE_MANUFACTURER,
|
||||
@@ -57,7 +59,9 @@ async def async_setup_entry(
|
||||
class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
"""Representation of the central mode choice"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the energy sensor"""
|
||||
self._config_id = unique_id
|
||||
self._device_name = entry_infos.get(CONF_NAME)
|
||||
@@ -67,7 +71,7 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
self._attr_current_option = CENTRAL_MODE_AUTO
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
def icon(self) -> str:
|
||||
return "mdi:form-select"
|
||||
|
||||
@property
|
||||
@@ -116,7 +120,7 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
self._attr_current_option = option
|
||||
await self.notify_central_mode_change(old_central_mode=old_option)
|
||||
|
||||
async def notify_central_mode_change(self, old_central_mode=None):
|
||||
async def notify_central_mode_change(self, old_central_mode: str | None = None):
|
||||
"""Notify all VTherm that the central_mode have change"""
|
||||
# Update all VTherm states
|
||||
component: EntityComponent[ClimateEntity] = self.hass.data[CLIMATE_DOMAIN]
|
||||
@@ -130,5 +134,5 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
self._attr_current_option, old_central_mode
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -284,7 +284,7 @@ class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
"""Initialize the energy sensor"""
|
||||
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
|
||||
self._attr_name = "Vave open percent"
|
||||
self._attr_name = "Valve open percent"
|
||||
self._attr_unique_id = f"{self._device_name}_valve_open_percent"
|
||||
|
||||
@callback
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"device_power": "Device power",
|
||||
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
|
||||
"use_window_feature": "Use window detection",
|
||||
@@ -77,7 +78,7 @@
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
@@ -257,6 +258,7 @@
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"device_power": "Device power",
|
||||
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
|
||||
"use_window_feature": "Use window detection",
|
||||
@@ -311,7 +313,7 @@
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
EventStateChangedData,
|
||||
)
|
||||
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
@@ -16,7 +17,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
|
||||
from .commons import NowClass, round_to_nearest
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .pi_algorithm import PITemperatureRegulator
|
||||
|
||||
from .const import (
|
||||
@@ -59,19 +60,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class ThermostatOverClimate(BaseThermostat):
|
||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||
|
||||
_auto_regulation_mode: str = None
|
||||
_auto_regulation_mode: str | None = None
|
||||
_regulation_algo = None
|
||||
_regulated_target_temp: float = None
|
||||
_auto_regulation_dtemp: float = None
|
||||
_auto_regulation_period_min: int = None
|
||||
_last_regulation_change: datetime = None
|
||||
_regulated_target_temp: float | None = None
|
||||
_auto_regulation_dtemp: float | None = None
|
||||
_auto_regulation_period_min: int | None = None
|
||||
_last_regulation_change: datetime | None = None
|
||||
# The fan mode configured in configEntry
|
||||
_auto_fan_mode: str = None
|
||||
_auto_fan_mode: str | None = None
|
||||
# The current fan mode (could be change by service call)
|
||||
_current_auto_fan_mode: str = None
|
||||
_current_auto_fan_mode: str | None = None
|
||||
# The fan_mode name depending of the current_mode
|
||||
_auto_activated_fan_mode: str = None
|
||||
_auto_deactivated_fan_mode: str = None
|
||||
_auto_activated_fan_mode: str | None = None
|
||||
_auto_deactivated_fan_mode: str | None = None
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
@@ -94,7 +95,9 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData
|
||||
):
|
||||
"""Initialize the thermostat over switch."""
|
||||
# super.__init__ calls post_init at the end. So it must be called after regulation initialization
|
||||
super().__init__(hass, unique_id, name, entry_infos)
|
||||
@@ -127,7 +130,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
return HVACAction.OFF
|
||||
|
||||
@overrides
|
||||
async def _async_internal_set_temperature(self, temperature):
|
||||
async def _async_internal_set_temperature(self, temperature: float):
|
||||
"""Set the target temperature and the target temperature of underlying climate if any"""
|
||||
await super()._async_internal_set_temperature(temperature)
|
||||
|
||||
@@ -239,7 +242,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await self.async_set_fan_mode(self._auto_deactivated_fan_mode)
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
@@ -281,7 +284,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
else CONF_AUTO_FAN_NONE
|
||||
)
|
||||
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode):
|
||||
def choose_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""Choose or change the regulation mode"""
|
||||
self._auto_regulation_mode = auto_regulation_mode
|
||||
if self._auto_regulation_mode == CONF_AUTO_REGULATION_LIGHT:
|
||||
@@ -357,7 +360,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self.target_temperature, 0, 0, 0, 0, 0.1, 0
|
||||
)
|
||||
|
||||
def choose_auto_fan_mode(self, auto_fan_mode):
|
||||
def choose_auto_fan_mode(self, auto_fan_mode: str):
|
||||
"""Choose the correct fan mode depending of the underlying capacities and the configuration"""
|
||||
|
||||
self._current_auto_fan_mode = auto_fan_mode
|
||||
@@ -369,7 +372,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._auto_activated_fan_mode = self._auto_deactivated_fan_mode = None
|
||||
return
|
||||
|
||||
def find_fan_mode(fan_modes, fan_mode) -> str:
|
||||
def find_fan_mode(fan_modes: list[str], fan_mode: str) -> str | None:
|
||||
"""Return the fan_mode if it exist of None if not"""
|
||||
try:
|
||||
return fan_mode if fan_modes.index(fan_mode) >= 0 else None
|
||||
@@ -427,10 +430,11 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
|
||||
# init auto_regulation_mode
|
||||
self.choose_auto_regulation_mode(self._auto_regulation_mode)
|
||||
# Issue 325 - do only once (in post_init and not here)
|
||||
# self.choose_auto_regulation_mode(self._auto_regulation_mode)
|
||||
|
||||
@overrides
|
||||
def restore_specific_previous_state(self, old_state):
|
||||
def restore_specific_previous_state(self, old_state: State):
|
||||
"""Restore my specific attributes from previous state"""
|
||||
old_error = old_state.attributes.get("regulation_accumulated_error")
|
||||
if old_error:
|
||||
@@ -542,7 +546,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _async_climate_changed(self, event):
|
||||
async def _async_climate_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
"""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
|
||||
@@ -552,7 +556,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
which is important for feedaback and which cannot generates loops.
|
||||
"""
|
||||
|
||||
async def end_climate_changed(changes):
|
||||
async def end_climate_changed(changes: bool):
|
||||
"""To end the event management"""
|
||||
if changes:
|
||||
self.async_write_ha_state()
|
||||
@@ -745,7 +749,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await end_climate_changed(changes)
|
||||
|
||||
@overrides
|
||||
async def async_control_heating(self, force=False, _=None):
|
||||
async def async_control_heating(self, force=False, _=None) -> bool:
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
ret = await super().async_control_heating(force, _)
|
||||
|
||||
@@ -757,27 +761,27 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
return ret
|
||||
|
||||
@property
|
||||
def auto_regulation_mode(self):
|
||||
def auto_regulation_mode(self) -> str | None:
|
||||
"""Get the regulation mode"""
|
||||
return self._auto_regulation_mode
|
||||
|
||||
@property
|
||||
def auto_fan_mode(self):
|
||||
def auto_fan_mode(self) -> str | None:
|
||||
"""Get the auto fan mode"""
|
||||
return self._auto_fan_mode
|
||||
|
||||
@property
|
||||
def regulated_target_temp(self):
|
||||
def regulated_target_temp(self) -> float | None:
|
||||
"""Get the regulated target temperature"""
|
||||
return self._regulated_target_temp
|
||||
|
||||
@property
|
||||
def is_regulated(self):
|
||||
def is_regulated(self) -> bool:
|
||||
"""Check if the ThermostatOverClimate is regulated"""
|
||||
return self.auto_regulation_mode != CONF_AUTO_REGULATION_NONE
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""List of available operation modes."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).hvac_modes
|
||||
@@ -850,13 +854,14 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> float | None:
|
||||
"""Return the supported step of target temperature."""
|
||||
if self.underlying_entity(0):
|
||||
return self.underlying_entity(0).target_temperature_step
|
||||
|
||||
return None
|
||||
# We keep the step configured for the VTherm and not the step of the underlying
|
||||
# @property
|
||||
# def target_temperature_step(self) -> float | None:
|
||||
# """Return the supported step of target temperature."""
|
||||
# if self.underlying_entity(0):
|
||||
# return self.underlying_entity(0).target_temperature_step
|
||||
#
|
||||
# return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
@@ -943,7 +948,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await under.async_turn_aux_heat_off()
|
||||
|
||||
@overrides
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
async def async_set_fan_mode(self, fan_mode: str):
|
||||
"""Set new target fan mode."""
|
||||
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
|
||||
if fan_mode is None:
|
||||
@@ -976,7 +981,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
self._swing_mode = swing_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def service_set_auto_regulation_mode(self, auto_regulation_mode):
|
||||
async def service_set_auto_regulation_mode(self, auto_regulation_mode: str):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_auto_regulation_mode
|
||||
data:
|
||||
@@ -1005,7 +1010,7 @@ class ThermostatOverClimate(BaseThermostat):
|
||||
await self._send_regulated_temperature()
|
||||
self.update_custom_attributes()
|
||||
|
||||
async def service_set_auto_fan_mode(self, auto_fan_mode):
|
||||
async def service_set_auto_fan_mode(self, auto_fan_mode: str):
|
||||
"""Called by a service call:
|
||||
service: versatile_thermostat.set_auto_fan_mode
|
||||
data:
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .const import (
|
||||
@@ -15,7 +19,7 @@ from .const import (
|
||||
overrides,
|
||||
)
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .underlyings import UnderlyingSwitch
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
@@ -51,7 +55,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, config_entry)
|
||||
_is_inversed: bool = None
|
||||
_is_inversed: bool | None = None
|
||||
|
||||
@property
|
||||
def is_over_switch(self) -> bool:
|
||||
@@ -72,7 +76,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
return None
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
@@ -200,7 +204,7 @@ class ThermostatOverSwitch(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_switch_changed(self, event):
|
||||
def _async_switch_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
"""Handle heater switch state changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
old_state = event.data.get("old_state")
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# pylint: disable=line-too-long
|
||||
""" A climate over switch classe """
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
async_track_time_interval,
|
||||
EventStateChangedData,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.typing import EventType as HASSEventType
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.components.climate import HVACMode
|
||||
|
||||
from .base_thermostat import BaseThermostat
|
||||
from .base_thermostat import BaseThermostat, ConfigData
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
from .const import (
|
||||
@@ -18,6 +20,9 @@ from .const import (
|
||||
CONF_VALVE_2,
|
||||
CONF_VALVE_3,
|
||||
CONF_VALVE_4,
|
||||
# This is not really self-regulation but regulation here
|
||||
CONF_AUTO_REGULATION_DTEMP,
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN,
|
||||
overrides,
|
||||
)
|
||||
|
||||
@@ -44,15 +49,25 @@ class ThermostatOverValve(BaseThermostat):
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Useless for now
|
||||
# def __init__(self, hass: HomeAssistant, unique_id, name, config_entry) -> None:
|
||||
# """Initialize the thermostat over switch."""
|
||||
# super().__init__(hass, unique_id, name, config_entry)
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, unique_id: str, name: str, config_entry: ConfigData
|
||||
):
|
||||
"""Initialize the thermostat over switch."""
|
||||
self._valve_open_percent: int = 0
|
||||
self._last_calculation_timestamp: datetime | None = None
|
||||
self._auto_regulation_dpercent: float | None = None
|
||||
self._auto_regulation_period_min: int | None = None
|
||||
|
||||
# Call to super must be done after initialization because it calls post_init at the end
|
||||
super().__init__(hass, unique_id, name, config_entry)
|
||||
|
||||
@property
|
||||
def is_over_valve(self) -> bool:
|
||||
@@ -65,13 +80,25 @@ class ThermostatOverValve(BaseThermostat):
|
||||
if self._hvac_mode == HVACMode.OFF:
|
||||
return 0
|
||||
else:
|
||||
return round(max(0, min(self.proportional_algorithm.on_percent, 1)) * 100)
|
||||
return self._valve_open_percent
|
||||
|
||||
@overrides
|
||||
def post_init(self, config_entry):
|
||||
def post_init(self, config_entry: ConfigData):
|
||||
"""Initialize the Thermostat"""
|
||||
|
||||
super().post_init(config_entry)
|
||||
|
||||
self._auto_regulation_dpercent = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_DTEMP)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_DTEMP) is not None
|
||||
else 0.0
|
||||
)
|
||||
self._auto_regulation_period_min = (
|
||||
config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN)
|
||||
if config_entry.get(CONF_AUTO_REGULATION_PERIOD_MIN) is not None
|
||||
else 0
|
||||
)
|
||||
|
||||
self._prop_algorithm = PropAlgorithm(
|
||||
self._proportional_function,
|
||||
self._tpi_coef_int,
|
||||
@@ -121,7 +148,7 @@ class ThermostatOverValve(BaseThermostat):
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _async_valve_changed(self, event):
|
||||
async def _async_valve_changed(self, event: HASSEventType[EventStateChangedData]):
|
||||
"""Handle unerdlying valve state changes.
|
||||
This method just log the change. It changes nothing to avoid loops.
|
||||
"""
|
||||
@@ -164,6 +191,17 @@ class ThermostatOverValve(BaseThermostat):
|
||||
self._attr_extra_state_attributes["function"] = self._proportional_function
|
||||
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
|
||||
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_regulation_dpercent"
|
||||
] = self._auto_regulation_dpercent
|
||||
self._attr_extra_state_attributes[
|
||||
"auto_regulation_period_min"
|
||||
] = self._auto_regulation_period_min
|
||||
self._attr_extra_state_attributes["last_calculation_timestamp"] = (
|
||||
self._last_calculation_timestamp.astimezone(self._current_tz).isoformat()
|
||||
if self._last_calculation_timestamp
|
||||
else None
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug(
|
||||
@@ -177,7 +215,21 @@ class ThermostatOverValve(BaseThermostat):
|
||||
"""A utility function to force the calculation of a the algo and
|
||||
update the custom attributes and write the state
|
||||
"""
|
||||
_LOGGER.debug("%s - recalculate all", self)
|
||||
_LOGGER.debug("%s - recalculate the open percent", self)
|
||||
|
||||
# For testing purpose. Should call _set_now() before
|
||||
now = self.now
|
||||
|
||||
if self._last_calculation_timestamp is not None:
|
||||
period = (now - self._last_calculation_timestamp).total_seconds() / 60
|
||||
if period < self._auto_regulation_period_min:
|
||||
_LOGGER.info(
|
||||
"%s - do not calculate TPI because regulation_period (%d) is not exceeded",
|
||||
self,
|
||||
period,
|
||||
)
|
||||
return
|
||||
|
||||
self._prop_algorithm.calculate(
|
||||
self._target_temp,
|
||||
self._cur_temp,
|
||||
@@ -185,9 +237,34 @@ class ThermostatOverValve(BaseThermostat):
|
||||
self._hvac_mode == HVACMode.COOL,
|
||||
)
|
||||
|
||||
new_valve_percent = round(
|
||||
max(0, min(self.proportional_algorithm.on_percent, 1)) * 100
|
||||
)
|
||||
|
||||
dpercent = new_valve_percent - self.valve_open_percent
|
||||
if (
|
||||
dpercent >= -1 * self._auto_regulation_dpercent
|
||||
and dpercent < self._auto_regulation_dpercent
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - do not calculate TPI because regulation_dpercent (%.1f) is not exceeded",
|
||||
self,
|
||||
dpercent,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if self._valve_open_percent == new_valve_percent:
|
||||
_LOGGER.debug("%s - no change in valve_open_percent.", self)
|
||||
return
|
||||
|
||||
self._valve_open_percent = new_valve_percent
|
||||
|
||||
for under in self._underlyings:
|
||||
under.set_valve_open_percent()
|
||||
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"device_power": "Device power",
|
||||
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
|
||||
"use_window_feature": "Use window detection",
|
||||
@@ -77,7 +78,7 @@
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to inverse the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
@@ -257,6 +258,7 @@
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"temp_min": "Minimal temperature allowed",
|
||||
"temp_max": "Maximal temperature allowed",
|
||||
"step_temperature": "Temperature step",
|
||||
"device_power": "Device power",
|
||||
"use_central_mode": "Enable the control by central entity (need central config). Check to enable the control of the VTherm with the select central_mode entities.",
|
||||
"use_window_feature": "Use window detection",
|
||||
@@ -311,7 +313,7 @@
|
||||
"valve_entity3_id": "3rd valve number entity id",
|
||||
"valve_entity4_id": "4th valve number entity id",
|
||||
"auto_regulation_mode": "Auto adjustment of the target temperature",
|
||||
"auto_regulation_dtemp": "The threshold in ° under which the temperature change will not be sent",
|
||||
"auto_regulation_dtemp": "The threshold in ° (or % for valve) under which the temperature change will not be sent",
|
||||
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
|
||||
"inverse_switch_command": "For switch with pilot wire and diode you may need to invert the command",
|
||||
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"temp_min": "Température minimale permise",
|
||||
"temp_max": "Température maximale permise",
|
||||
"step_temperature": "Pas de température",
|
||||
"device_power": "Puissance de l'équipement",
|
||||
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
|
||||
"use_window_feature": "Avec détection des ouvertures",
|
||||
@@ -77,7 +78,7 @@
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la température cible",
|
||||
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
|
||||
@@ -269,6 +270,7 @@
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"temp_min": "Température minimale permise",
|
||||
"temp_max": "Température maximale permise",
|
||||
"step_temperature": "Pas de température",
|
||||
"device_power": "Puissance de l'équipement",
|
||||
"use_central_mode": "Autoriser le controle par une entity centrale ('nécessite une config. centrale`). Cochez pour autoriser le contrôle du VTherm par la liste déroulante 'central_mode' de l'entité configuration centrale.",
|
||||
"use_window_feature": "Avec détection des ouvertures",
|
||||
@@ -323,7 +325,7 @@
|
||||
"valve_entity3_id": "Entity id de la 3ème valve",
|
||||
"valve_entity4_id": "Entity id de la 4ème valve",
|
||||
"auto_regulation_mode": "Ajustement automatique de la consigne",
|
||||
"auto_regulation_dtemp": "Le seuil en ° au-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_dtemp": "Le seuil en ° (ou % pour les valves) en-dessous duquel la régulation ne sera pas envoyée",
|
||||
"auto_regulation_periode_min": "La durée en minutes entre deux mise à jour faites par la régulation",
|
||||
"inverse_switch_command": "Inverse la commande du switch pour une installation avec fil pilote et diode",
|
||||
"auto_fan_mode": "Active la ventilation automatiquement en cas d'écart important"
|
||||
|
||||
@@ -765,7 +765,7 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
await self.send_percent_open()
|
||||
|
||||
async def turn_on(self):
|
||||
"""Nothing to do for Valve because it cannot be turned off"""
|
||||
"""Nothing to do for Valve because it cannot be turned on"""
|
||||
self.set_valve_open_percent()
|
||||
|
||||
async def set_hvac_mode(self, hvac_mode: HVACMode) -> bool:
|
||||
|
||||
@@ -146,6 +146,7 @@ FULL_CENTRAL_CONFIG = {
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
"frost_temp": 10,
|
||||
@@ -186,6 +187,7 @@ FULL_CENTRAL_CONFIG_WITH_BOILER = {
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0.02,
|
||||
"frost_temp": 10,
|
||||
@@ -263,6 +265,7 @@ class MockClimate(ClimateEntity):
|
||||
self._attr_target_temperature = 20
|
||||
self._attr_current_temperature = 15
|
||||
self._attr_hvac_action = hvac_action
|
||||
self._attr_target_temperature_step = 0.2
|
||||
self._fan_modes = fan_modes if fan_modes else None
|
||||
self._attr_fan_mode = None
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ MOCK_TH_OVER_4SWITCH_USER_CONFIG = {
|
||||
CONF_CYCLE_MIN: 8,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
CONF_DEVICE_POWER: 1,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
@@ -59,6 +60,7 @@ MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG = {
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
@@ -66,6 +68,7 @@ MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG = {
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_STEP_TEMPERATURE: 0.1,
|
||||
# Keep default values which are False
|
||||
}
|
||||
|
||||
|
||||
@@ -591,6 +591,7 @@ async def test_bug_272(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
# default value are min 15°, max 30°, step 0.1
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
@@ -623,6 +624,8 @@ async def test_bug_272(
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# The VTherm value and not the underlying value
|
||||
assert entity.target_temperature_step == 0.1
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.is_regulated is True
|
||||
|
||||
|
||||
@@ -161,16 +161,19 @@ async def test_update_central_boiler_state_simple(
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count >= 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.service_call(
|
||||
"switch",
|
||||
"turn_on",
|
||||
service_data={},
|
||||
target={"entity_id": "switch.pompe_chaudiere"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Sometimes this test fails
|
||||
# mock_service_call.assert_has_calls(
|
||||
# [
|
||||
# call.service_call(
|
||||
# "switch",
|
||||
# "turn_on",
|
||||
# service_data={},
|
||||
# target={"entity_id": "switch.pompe_chaudiere"},
|
||||
# ),
|
||||
# ]
|
||||
# )
|
||||
|
||||
assert mock_send_event.call_count >= 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
@@ -760,7 +763,7 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
climate1.set_hvac_action(HVACAction.HEATING)
|
||||
climate1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
@@ -801,7 +804,7 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
climate1.set_hvac_action(HVACAction.IDLE)
|
||||
climate1.async_write_ha_state()
|
||||
# Wait for state event propagation
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ async def test_add_a_central_config(hass: HomeAssistant, skip_hass_states_is_sta
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
# @pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_minimal_over_switch_wo_central_config(
|
||||
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
|
||||
):
|
||||
@@ -124,6 +124,7 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
CONF_STEP_TEMPERATURE: 0.3,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
@@ -165,8 +166,9 @@ async def test_minimal_over_switch_wo_central_config(
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity._attr_min_temp == 8
|
||||
assert entity._attr_max_temp == 18
|
||||
assert entity.min_temp == 8
|
||||
assert entity.max_temp == 18
|
||||
assert entity.target_temperature_step == 0.3
|
||||
assert entity.preset_modes == ["none", "frost", "eco", "comfort", "boost"]
|
||||
assert entity.is_window_auto_enabled is False
|
||||
assert entity.nb_underlying_entities == 1
|
||||
@@ -202,6 +204,7 @@ async def test_full_over_switch_wo_central_config(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
CONF_STEP_TEMPERATURE: 0.3,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
@@ -257,8 +260,9 @@ async def test_full_over_switch_wo_central_config(
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity._attr_min_temp == 8
|
||||
assert entity._attr_max_temp == 18
|
||||
assert entity.min_temp == 8
|
||||
assert entity.max_temp == 18
|
||||
assert entity.target_temperature_step == 0.3
|
||||
assert entity.preset_modes == [
|
||||
"none",
|
||||
"frost",
|
||||
@@ -318,6 +322,7 @@ async def test_full_over_switch_with_central_config(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
CONF_STEP_TEMPERATURE: 0.3,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
@@ -369,8 +374,9 @@ async def test_full_over_switch_with_central_config(
|
||||
assert entity._temp_sensor_entity_id == "sensor.mock_temp_sensor"
|
||||
assert entity._ext_temp_sensor_entity_id == "sensor.mock_ext_temp_sensor"
|
||||
assert entity._cycle_min == 5
|
||||
assert entity._attr_min_temp == 15
|
||||
assert entity._attr_max_temp == 30
|
||||
assert entity.min_temp == 15
|
||||
assert entity.max_temp == 30
|
||||
assert entity.target_temperature_step == 0.1
|
||||
assert entity.preset_modes == [
|
||||
"none",
|
||||
"frost",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=line-too-long
|
||||
# pylint: disable=line-too-long, disable=protected-access
|
||||
|
||||
""" Test the normal start of a Switch AC Thermostat """
|
||||
from unittest.mock import patch, call
|
||||
@@ -324,3 +324,232 @@ async def test_over_valve_full_start(
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
assert entity.target_temperature == 17.1 # eco
|
||||
assert entity.valve_open_percent == 10
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_over_valve_regulation(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverValveMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverValveMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_VALVE: "number.mock_valve",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
PRESET_FROST_PROTECTION + "_temp": 7,
|
||||
PRESET_ECO + "_temp": 17,
|
||||
PRESET_COMFORT + "_temp": 19,
|
||||
PRESET_BOOST + "_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 60,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
# only send new valve open percent if dtemp is > 30%
|
||||
CONF_AUTO_REGULATION_DTEMP: 5,
|
||||
# only send new valve open percent last mesure was more than 5 min ago
|
||||
CONF_AUTO_REGULATION_PERIOD_MIN: 5,
|
||||
},
|
||||
)
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# 1. prepare the Valve at now
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theovervalvemockname"
|
||||
)
|
||||
assert entity
|
||||
assert isinstance(entity, ThermostatOverValve)
|
||||
|
||||
assert entity.name == "TheOverValveMockName"
|
||||
assert entity.is_over_valve is True
|
||||
assert entity._auto_regulation_dpercent == 5
|
||||
assert entity._auto_regulation_period_min == 5
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity._prop_algorithm is not None
|
||||
|
||||
# 2. Set the HVACMode to HEAT, with manual preset and target_temp to 18 before receiving temperature
|
||||
# at now +1
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
# Select a hvacmode, presence and preset
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# No heating now
|
||||
assert entity.valve_open_percent == 0
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.HEAT},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 3. Set the preset
|
||||
# at now +1
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
# set preset
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
assert entity.preset_mode == PRESET_BOOST
|
||||
assert entity.target_temperature == 21
|
||||
# the preset have changed
|
||||
assert mock_send_event.call_count == 1
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(
|
||||
EventType.PRESET_EVENT,
|
||||
{"preset": PRESET_BOOST},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
# Still no heating because we don't have temperature
|
||||
assert entity.valve_open_percent == 0
|
||||
assert entity.hvac_action == HVACAction.IDLE
|
||||
|
||||
# 4. Set temperature and external temperature
|
||||
# at now + 1 (but the _last_calculation_timestamp is still not send)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(entity_id="number.mock_valve", state="90"),
|
||||
):
|
||||
# Change temperature
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
await send_temperature_change_event(entity, 18, now)
|
||||
assert entity.valve_open_percent == 90
|
||||
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 90},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 5. Set external temperature
|
||||
# at now + 1
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(entity_id="number.mock_valve", state="90"),
|
||||
):
|
||||
# Change external temperature
|
||||
now = now + timedelta(minutes=1)
|
||||
entity._set_now(now)
|
||||
|
||||
await send_ext_temperature_change_event(entity, 10, now)
|
||||
|
||||
# Should not have change due to regulation (period_min !)
|
||||
assert entity.valve_open_percent == 90
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 6. Set temperature
|
||||
# at now + 5 (to avoid the period_min threshold)
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(entity_id="number.mock_valve", state="90"),
|
||||
):
|
||||
# Change external temperature
|
||||
now = now + timedelta(minutes=5)
|
||||
entity._set_now(now)
|
||||
|
||||
await send_ext_temperature_change_event(entity, 15, now)
|
||||
|
||||
# Should have change this time to 96
|
||||
assert entity.valve_open_percent == 96
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
"number",
|
||||
"set_value",
|
||||
{"entity_id": "number.mock_valve", "value": 96},
|
||||
),
|
||||
]
|
||||
)
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
# 7. Set small temperature update to test dtemp threshold
|
||||
# at now + 5
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"homeassistant.core.ServiceRegistry.async_call"
|
||||
) as mock_service_call, patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(entity_id="number.mock_valve", state="96"),
|
||||
):
|
||||
# Change external temperature
|
||||
now = now + timedelta(minutes=5)
|
||||
entity._set_now(now)
|
||||
|
||||
# this generate a delta percent of -3
|
||||
await send_temperature_change_event(entity, 18.1, now)
|
||||
|
||||
# Should not have due to dtemp
|
||||
assert entity.valve_open_percent == 96
|
||||
assert entity.is_device_active is True
|
||||
assert entity.hvac_action == HVACAction.HEATING
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
Reference in New Issue
Block a user