Compare commits

..

22 Commits

Author SHA1 Message Date
Jean-Marc Collin
70f91f3cbe Issue #524 switch from cool to heat don't change the target temp (#529)
* Preparation tests ok

* Fixed

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-10-08 17:06:16 +02:00
Jean-Marc Collin
668053b352 Fix unit_test 2024-10-07 05:22:57 +00:00
Jean-Marc Collin
6ff9ff1ee5 Fix variables in error log 2024-10-07 04:52:33 +00:00
Jean-Marc Collin
3f95ed74f4 FIX TypeError: '>' not supported between instances of 'float' and 'NoneType' error message 2024-10-06 09:04:47 +00:00
Jean-Marc Collin
6e42904ddf Issue #518 - Fix ThermostatOverClimate object has no attribute __attr_preset_modes 2024-10-06 08:58:58 +00:00
Jean-Marc Collin
4c1fc396fb Issue #500 - check feature is use central config is checked (#513)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-09-29 11:34:39 +02:00
Jean-Marc Collin
d6ec7a86be issue #506 - Add some check to verify tpi algorithm parameters are correctly set. (#512)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-09-29 09:54:39 +02:00
Jean-Marc Collin
a3f8715fe5 HA 2024.9.3 and issue 508 (#510)
* HA 2024.9.3 and issue 508

* Fix strings trailing spaces

---------

Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-09-28 19:36:46 +02:00
domozer
a1a9a8bbab Update README-fr.md (#487)
Remplacement de "Awesome Thermostat" par "Versatile Thermostat" dans la section "Encore mieux avec le composant Scheduler !"

Idem pour le fichier README.md
2024-07-23 19:24:56 +02:00
Jean-Marc Collin
d5c5869276 Update settings 2024-07-17 06:50:01 +00:00
Jean-Marc Collin
c4b03f8c1e Update manifest.json 2024-07-07 16:49:22 +02:00
Paulo Ferreira de Castro
ac206a949f Fix Home Assistant deprecation warnings (EventType, helpers.service) (#484)
* Type hints: Replace deprecated helpers.typing.EventType with core.Event

* Replace deprecated use of hass.helpers.service.async_register_admin_service
2024-07-07 16:47:30 +02:00
Jean-Marc Collin
4bccb746b8 Release 6.2.8 2024-07-02 05:18:29 +00:00
Jean-Marc Collin
e999705286 Issue 474 - TPI in AC mode is wrong 2024-07-02 05:17:14 +00:00
Jean-Marc Collin
b4873bfd27 FIX issue_479 (#480)
Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
2024-07-02 07:04:47 +02:00
Jean-Marc Collin
b00dc09c80 Update manifest.json 2024-06-30 09:05:14 +02:00
Jean-Marc Collin
da6d6cbce6 Release 6.2.6 2024-06-30 09:04:51 +02:00
Paulo Ferreira de Castro
864e904e21 Reduce keep-alive warning logs when the controlled entity becomes unavailable (#476) 2024-06-28 09:53:40 +02:00
cddu33
0ee4fe355d issue with ac and mouvement detection (#471)
* Update base_thermostat.py

issue with ac and mouvement detection

* issue ac with detection mouvement

modif class find_preset_temps for preset activity
2024-06-17 10:18:03 +02:00
Maxwell Gonsalves
53dce224cd Change VTherm temperature unit to HA's preferred unit. (#461)
* Change VTherm temperature unit to HA's preferred unit.

* fix pytest issue

* update current_temperature, explicitly convert temps (fixes pytest error)
2024-06-10 18:46:45 +02:00
Jean-Marc Collin
2fd60074c7 Beers ! 2024-05-28 08:47:14 +02:00
jkreiss-coexya
549423b313 Enhance temperature regulation when working with internal device temperature (#453)
* [feature/autoregulation-send-for-underlyingtemp] Do not forget regulation send when using underlying device temperature for offset

* [feature/autoregulation-send-for-underlyingtemp] Add unit test for dtemp = 0

* [feature/autoregulation-send-for-underlyingtemp] Test with dtemp lower than 0.5

* [feature/autoregulation-send-for-underlyingtemp] Comments
2024-05-13 08:27:52 +02:00
30 changed files with 1059 additions and 171 deletions

View File

@@ -4,7 +4,6 @@
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modifications"
},
"pylint.lintOnChange": false,
"files.associations": {
"*.yaml": "home-assistant"
},

View File

@@ -212,7 +212,7 @@ En conséquence toute la phase de paramètrage d'un VTherm a été profondemment
</details>
# 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, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R., @John Burgess, @Sylvoliv, @cdenfert, @stephane.l 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, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R., @John Burgess, @Sylvoliv, @cdenfert, @stephane.l, @jms92100 pour les bières. Ca fait très plaisir et ça m'encourage à continuer !
# Quand l'utiliser et ne pas l'utiliser
@@ -1202,9 +1202,9 @@ Une carte spéciale pour le Versatile Thermostat a été développée (sur la ba
## Encore mieux avec le composant Scheduler !
Afin de profiter de toute la puissance du Versatile Thermostat, je vous invite à l'utiliser avec https://github.com/nielsfaber/scheduler-component
En effet, le composant scheduler propose une gestion de la base climatique sur les modes prédéfinis. Cette fonctionnalité a un intérêt limité avec le thermostat générique mais elle devient très puissante avec le thermostat Awesome :
En effet, le composant scheduler propose une gestion de la base climatique sur les modes prédéfinis. Cette fonctionnalité a un intérêt limité avec le thermostat générique mais elle devient très puissante avec le Versatile Thermostat :
À partir d'ici, je suppose que vous avez installé Awesome Thermostat et Scheduler Component.
À partir d'ici, je suppose que vous avez installé Versatile Thermostat et Scheduler Component.
Dans Scheduler, ajoutez un planning :

View File

@@ -213,7 +213,7 @@ Consequently, the entire configuration phase of a VTherm has been profoundly mod
</details>
# 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, @giopeco, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R., @John Burgess, @Sylvoliv, @cdenfert, @stephane.l 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, @fredericselier, @philpagan, @studiogriffanti, @Edwin, @Sebbou, @Gerard R., @John Burgess, @Sylvoliv, @cdenfert, @stephane.l, @jms92100 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:

View File

@@ -13,6 +13,7 @@ from homeassistant.const import SERVICE_RELOAD, EVENT_HOMEASSISTANT_STARTED
from homeassistant.config_entries import ConfigEntry, ConfigType
from homeassistant.core import HomeAssistant, CoreState, callback
from homeassistant.helpers.service import async_register_admin_service
from .base_thermostat import BaseThermostat
@@ -115,7 +116,8 @@ async def async_setup(
else:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_startup_internal)
hass.helpers.service.async_register_admin_service(
async_register_admin_service(
hass,
DOMAIN,
SERVICE_RELOAD,
_handle_reload,

View File

@@ -13,7 +13,6 @@ from homeassistant.util import dt as dt_util
from homeassistant.core import (
HomeAssistant,
callback,
CoreState,
Event,
State,
)
@@ -22,7 +21,6 @@ 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,
@@ -58,7 +56,6 @@ from homeassistant.const import (
STATE_UNKNOWN,
STATE_OFF,
STATE_ON,
EVENT_HOMEASSISTANT_START,
STATE_HOME,
STATE_NOT_HOME,
)
@@ -300,7 +297,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._presets: dict[str, Any] = {} # presets
self._presets_away: dict[str, Any] = {} # presets_away
self._attr_preset_modes: list[str] | None
self._attr_preset_modes: list[str] = []
self._use_central_config_temperature = False
@@ -737,37 +734,37 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
need_write_state = True
# try to acquire window entity state
if self._window_sensor_entity_id:
window_state = self.hass.states.get(self._window_sensor_entity_id)
if window_state and window_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._window_state = window_state.state == STATE_ON
_LOGGER.debug(
"%s - Window state have been retrieved: %s",
self,
self._window_state,
)
need_write_state = True
# try to acquire window entity state
if self._window_sensor_entity_id:
window_state = self.hass.states.get(self._window_sensor_entity_id)
if window_state and window_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._window_state = window_state.state == STATE_ON
_LOGGER.debug(
"%s - Window state have been retrieved: %s",
self,
self._window_state,
)
need_write_state = True
# try to acquire motion entity state
if self._motion_sensor_entity_id:
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
if motion_state and motion_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._motion_state = motion_state.state
_LOGGER.debug(
"%s - Motion state have been retrieved: %s",
self,
self._motion_state,
)
# recalculate the right target_temp in activity mode
await self._async_update_motion_temp()
need_write_state = True
# try to acquire motion entity state
if self._motion_sensor_entity_id:
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
if motion_state and motion_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._motion_state = motion_state.state
_LOGGER.debug(
"%s - Motion state have been retrieved: %s",
self,
self._motion_state,
)
# recalculate the right target_temp in activity mode
await self._async_update_motion_temp()
need_write_state = True
if self._presence_on:
# try to acquire presence entity state
@@ -1238,7 +1235,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
if self._hvac_mode == HVACMode.COOL and self.preset_mode != PRESET_NONE:
if self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL] and self.preset_mode != PRESET_NONE:
if self.preset_mode != PRESET_FROST_PROTECTION:
await self._async_set_preset_mode_internal(self.preset_mode, True)
else:
@@ -1377,11 +1374,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if preset_mode == PRESET_POWER:
return self._power_temp
if preset_mode == PRESET_ACTIVITY:
motion_preset = (
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
if self._ac_mode and self._hvac_mode == HVACMode.COOL:
motion_preset = (
self._motion_preset + PRESET_AC_SUFFIX
if self._motion_state == STATE_ON
else self._no_motion_preset + PRESET_AC_SUFFIX
)
else:
motion_preset = (
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
if motion_preset in self._presets:
return self._presets[motion_preset]
else:
@@ -1646,6 +1651,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
self._motion_state = new_state.state
if self._attr_preset_mode == PRESET_ACTIVITY:
new_preset = (
self._motion_preset
if self._motion_state == STATE_ON
@@ -1658,6 +1664,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
# We take the presence into account
await self._async_internal_set_temperature(
self.find_preset_temp(new_preset)
)
@@ -1780,7 +1787,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.error("Unable to update external temperature from sensor: %s", ex)
@callback
async def _async_power_changed(self, event: HASSEventType[EventStateChangedData]):
async def _async_power_changed(self, event: Event[EventStateChangedData]):
"""Handle power changes."""
_LOGGER.debug("Thermostat %s - Receive new Power event", self.name)
_LOGGER.debug(event)
@@ -1806,9 +1813,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
@callback
async def _async_max_power_changed(
self, event: HASSEventType[EventStateChangedData]
):
async def _async_max_power_changed(self, event: Event[EventStateChangedData]):
"""Handle power max changes."""
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
_LOGGER.debug(event)
@@ -1833,9 +1838,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
@callback
async def _async_presence_changed(
self, event: HASSEventType[EventStateChangedData]
):
async def _async_presence_changed(self, event: Event[EventStateChangedData]):
"""Handle presence changes."""
new_state = event.data.get("new_state")
_LOGGER.info(
@@ -1896,16 +1899,23 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
):
return
new_preset = (
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
)
_LOGGER.info(
"%s - Motion condition have changes. New preset temp will be %s",
self,
new_preset,
)
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
# We take the presence into account
await self._async_internal_set_temperature(
self._presets.get(
(
self._motion_preset
if self._motion_state == STATE_ON
else self._no_motion_preset
),
None,
)
self.find_preset_temp(new_preset)
)
_LOGGER.debug(
"%s - regarding motion, target_temp have been set to %.2f",
self,
@@ -2171,7 +2181,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
new_central_mode,
)
first_init = self._last_central_mode == None
first_init = self._last_central_mode is None
self._last_central_mode = new_central_mode

View File

@@ -72,6 +72,13 @@ async def async_setup_entry(
entity = ThermostatOverClimate(hass, unique_id, name, entry.data)
elif vt_type == CONF_THERMOSTAT_VALVE:
entity = ThermostatOverValve(hass, unique_id, name, entry.data)
else:
_LOGGER.error(
"Cannot create Versatile Thermostat name=%s of type %s which is unknown",
name,
vt_type,
)
return
async_add_entities([entity], True)

View File

@@ -99,30 +99,31 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
def _init_feature_flags(self, _):
"""Fix features selection depending to infos"""
is_empty: bool = False # TODO remove this not bool(infos)
is_central_config = (
self._infos.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CENTRAL_CONFIG
)
self._infos[CONF_USE_WINDOW_FEATURE] = (
is_empty
self._infos.get(CONF_USE_WINDOW_CENTRAL_CONFIG)
or self._infos.get(CONF_WINDOW_SENSOR) is not None
or self._infos.get(CONF_WINDOW_AUTO_OPEN_THRESHOLD) is not None
)
self._infos[CONF_USE_MOTION_FEATURE] = (
is_empty
or self._infos.get(CONF_MOTION_SENSOR) is not None
or is_central_config
)
self._infos[CONF_USE_POWER_FEATURE] = is_empty or (
self._infos[CONF_USE_MOTION_FEATURE] = self._infos.get(
CONF_USE_MOTION_FEATURE
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
CONF_USE_POWER_CENTRAL_CONFIG
) or (
self._infos.get(CONF_POWER_SENSOR) is not None
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
)
self._infos[CONF_USE_PRESENCE_FEATURE] = (
is_empty or self._infos.get(CONF_PRESENCE_SENSOR) is not None
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG)
or self._infos.get(CONF_PRESENCE_SENSOR) is not None
)
self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] = is_empty or (
self._infos[CONF_USE_CENTRAL_BOILER_FEATURE] = (
self._infos.get(CONF_CENTRAL_BOILER_ACTIVATION_SRV) is not None
and self._infos.get(CONF_CENTRAL_BOILER_DEACTIVATION_SRV) is not None
)

View File

@@ -10,6 +10,7 @@ the keep_alive setting of Home Assistant's Generic Thermostat integration:
import logging
from collections.abc import Awaitable, Callable
from datetime import timedelta, datetime
from time import monotonic
from homeassistant.core import HomeAssistant, CALLBACK_TYPE
from homeassistant.helpers.event import async_track_time_interval
@@ -18,6 +19,79 @@ from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
class BackoffTimer:
"""Exponential backoff timer with a non-blocking polling-style implementation.
Usage example:
timer = BackoffTimer(multiplier=1.5, upper_limit_sec=600)
while some_condition:
if timer.is_ready():
do_something()
"""
def __init__(
self,
*,
multiplier=2.0,
lower_limit_sec=30,
upper_limit_sec=86400,
initially_ready=True,
):
"""Initialize a BackoffTimer instance.
Args:
multiplier (int, optional): Period multiplier applied when is_ready() is True.
lower_limit_sec (int, optional): Initial backoff period in seconds.
upper_limit_sec (int, optional): Maximum backoff period in seconds.
initially_ready (bool, optional): Whether is_ready() should return True the
first time it is called, or after a call to reset().
"""
self._multiplier = multiplier
self._lower_limit_sec = lower_limit_sec
self._upper_limit_sec = upper_limit_sec
self._initially_ready = initially_ready
self._timestamp = 0
self._period_sec = self._lower_limit_sec
@property
def in_progress(self) -> bool:
"""Whether the backoff timer is in progress (True after a call to is_ready())."""
return bool(self._timestamp)
def reset(self):
"""Reset a BackoffTimer instance."""
self._timestamp = 0
self._period_sec = self._lower_limit_sec
def is_ready(self) -> bool:
"""Check whether an exponentially increasing period of time has passed.
Whenever is_ready() returns True, the timer period is multiplied so that
it takes longer until is_ready() returns True again.
Returns:
bool: True if enough time has passed since one of the following events,
in relation to an instance of this class:
- The last time when this method returned True, if it ever did.
- Or else, when this method was first called after a call to reset().
- Or else, when this method was first called.
False otherwise.
"""
now = monotonic()
if self._timestamp == 0:
self._timestamp = now
return self._initially_ready
elif now - self._timestamp >= self._period_sec:
self._timestamp = now
self._period_sec = max(
self._lower_limit_sec,
min(self._upper_limit_sec, self._period_sec * self._multiplier),
)
return True
return False
class IntervalCaller:
"""Repeatedly call a given async action function at a given regular interval.
@@ -28,6 +102,7 @@ class IntervalCaller:
self._hass = hass
self._interval_sec = interval_sec
self._remove_handle: CALLBACK_TYPE | None = None
self.backoff_timer = BackoffTimer()
@property
def interval_sec(self) -> float:

View File

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

View File

@@ -14,6 +14,10 @@ PROPORTIONAL_MIN_DURATION_SEC = 10
FUNCTION_TYPE = [PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR]
def is_number(value):
return isinstance(value, (int, float))
class PropAlgorithm:
"""This class aims to do all calculation of the Proportional alogorithm"""
@@ -36,6 +40,30 @@ class PropAlgorithm:
cycle_min,
minimal_activation_delay,
)
# Issue 506 - check parameters
if (
vtherm_entity_id is None
or not is_number(tpi_coef_int)
or not is_number(tpi_coef_ext)
or not is_number(cycle_min)
or not is_number(minimal_activation_delay)
or function_type != PROPORTIONAL_FUNCTION_TPI
):
_LOGGER.error(
"%s - configuration is wrong. function_type=%s, entity_id is %s, tpi_coef_int is %s, tpi_coef_ext is %s, cycle_min is %s, minimal_activation_delay is %s",
vtherm_entity_id,
function_type,
vtherm_entity_id,
tpi_coef_int,
tpi_coef_ext,
cycle_min,
minimal_activation_delay,
)
raise TypeError(
f"TPI parameters are not set correctly. VTherm will not work as expected. Please reconfigure it correctly. See previous log for values"
)
self._vtherm_entity_id = vtherm_entity_id
self._function = function_type
self._tpi_coef_int = tpi_coef_int
@@ -70,9 +98,9 @@ class PropAlgorithm:
if hvac_mode == HVACMode.COOL:
delta_temp = current_temp - target_temp
delta_ext_temp = (
ext_current_temp
ext_current_temp - target_temp
if ext_current_temp is not None
else 0 - target_temp
else 0
)
else:
delta_temp = target_temp - current_temp

View File

@@ -90,7 +90,7 @@
"auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -113,7 +113,7 @@
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"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"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
@@ -325,7 +325,7 @@
"auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -348,7 +348,7 @@
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"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"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {

View File

@@ -3,13 +3,12 @@
import logging
from datetime import timedelta, datetime
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.core import Event, 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,
@@ -143,7 +142,17 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
"""Sends the regulated temperature to all underlying"""
if self.hvac_mode == HVACMode.OFF:
_LOGGER.debug("%s - don't send regulated temperature cause VTherm is off ")
_LOGGER.debug(
"%s - don't send regulated temperature cause VTherm is off ", self
)
return
if self.target_temperature is None:
_LOGGER.warning(
"%s - don't send regulated temperature cause VTherm target_temp (%s) is None. This should be a temporary warning message.",
self,
self.target_temperature,
)
return
_LOGGER.info(
@@ -168,12 +177,19 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
_LOGGER.info("%s - regulation calculation will be done", self)
new_regulated_temp = round_to_nearest(
self._regulation_algo.calculate_regulated_temperature(
self.current_temperature, self._cur_ext_temp
),
self._auto_regulation_dtemp,
)
# use _attr_target_temperature_step to round value if _auto_regulation_dtemp is equal to 0
regulation_step = self._auto_regulation_dtemp if self._auto_regulation_dtemp else self._attr_target_temperature_step
_LOGGER.debug("%s - usage regulation_step: %.2f ", self, regulation_step)
if self.current_temperature is not None:
new_regulated_temp = round_to_nearest(
self._regulation_algo.calculate_regulated_temperature(
self.current_temperature, self._cur_ext_temp
),
regulation_step,
)
else:
new_regulated_temp = self.target_temperature
dtemp = new_regulated_temp - self._regulated_target_temp
if not force and abs(dtemp) < self._auto_regulation_dtemp:
@@ -198,8 +214,10 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
offset_temp = 0
device_temp = 0
if (
# current_temperature is set
self.current_temperature is not None
# regulation can use the device_temp
self.auto_regulation_use_device_temp
and self.auto_regulation_use_device_temp
# and we have access to the device temp
and (device_temp := under.underlying_current_temperature) is not None
# and target is not reach (ie we need regulation)
@@ -216,7 +234,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
):
offset_temp = device_temp - self.current_temperature
target_temp = round_to_nearest(self.regulated_target_temp + offset_temp, self._auto_regulation_dtemp)
target_temp = round_to_nearest(self.regulated_target_temp + offset_temp, regulation_step)
_LOGGER.debug(
"%s - The device offset temp for regulation is %.2f - internal temp is %.2f. New target is %.2f",
@@ -594,7 +612,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
)
@callback
async def _async_climate_changed(self, event: HASSEventType[EventStateChangedData]):
async def _async_climate_changed(self, event: Event[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
@@ -894,10 +912,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
if self.underlying_entity(0):
return self.underlying_entity(0).temperature_unit
return self._unit
return self.hass.config.units.temperature_unit
@property
def supported_features(self):

View File

@@ -2,12 +2,11 @@
""" A climate over switch classe """
import logging
from homeassistant.core import callback
from homeassistant.core import Event, callback
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 (
@@ -212,7 +211,7 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
)
@callback
def _async_switch_changed(self, event: HASSEventType[EventStateChangedData]):
def _async_switch_changed(self, event: Event[EventStateChangedData]):
"""Handle heater switch state changes."""
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")

View File

@@ -8,8 +8,7 @@ from homeassistant.helpers.event import (
async_track_time_interval,
EventStateChangedData,
)
from homeassistant.helpers.typing import EventType as HASSEventType
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.components.climate import HVACMode
from .base_thermostat import BaseThermostat, ConfigData
@@ -149,7 +148,7 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
)
@callback
async def _async_valve_changed(self, event: HASSEventType[EventStateChangedData]):
async def _async_valve_changed(self, event: Event[EventStateChangedData]):
"""Handle unerdlying valve state changes.
This method just log the change. It changes nothing to avoid loops.
"""

View File

@@ -43,7 +43,7 @@
"auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
@@ -64,7 +64,7 @@
"auto_regulation_dtemp": "Το όριο σε ° κάτω από το οποίο η αλλαγή θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"inverse_switch_command": "Για διακόπτη με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστρέψετε την εντολή",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
@@ -216,7 +216,7 @@
"auto_regulation_dtemp": "Όριο ρύθμισης",
"auto_regulation_periode_min": "Ελάχιστη περίοδος ρύθμισης",
"inverse_switch_command": "Αντίστροφη εντολή διακόπτη",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Υποχρεωτική ταυτότητα οντότητας θερμαντήρα",
@@ -237,7 +237,7 @@
"auto_regulation_dtemp": "Το κατώφλι σε °C κάτω από το οποίο η αλλαγή της θερμοκρασίας δεν θα αποστέλλεται",
"auto_regulation_periode_min": "Διάρκεια σε λεπτά μεταξύ δύο ενημερώσεων ρύθμισης",
"inverse_switch_command": "Για διακόπτες με πιλοτικό καλώδιο και δίοδο μπορεί να χρειαστεί να αντιστραφεί η εντολή",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {

View File

@@ -90,7 +90,7 @@
"auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -113,7 +113,7 @@
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"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"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
@@ -325,7 +325,7 @@
"auto_regulation_periode_min": "Regulation minimum period",
"auto_regulation_use_device_temp": "Use internal temperature of the underlying",
"inverse_switch_command": "Inverse switch command",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Mandatory heater entity id",
@@ -348,7 +348,7 @@
"auto_regulation_periode_min": "Duration in minutes between two regulation update",
"auto_regulation_use_device_temp": "Use the eventual internal temperature sensor of the underlying to speedup the self-regulation",
"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"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {

View File

@@ -337,7 +337,7 @@
"auto_regulation_periode_min": "Période minimale de régulation",
"auto_regulation_use_device_temp": "Utiliser la température interne du sous-jacent",
"inverse_switch_command": "Inverser la commande",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Entity id du 1er radiateur obligatoire",

View File

@@ -42,7 +42,7 @@
"valve_entity4_id": "Quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -62,7 +62,7 @@
"valve_entity4_id": "Entity id della quarta valvola",
"auto_regulation_mode": "Regolazione automatica della temperatura target",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {
@@ -206,7 +206,7 @@
"valve_entity4_id": "Quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Comando inverso",
"auto_fan_mode": " Auto fan mode"
"auto_fan_mode": "Auto fan mode"
},
"data_description": {
"heater_entity_id": "Entity id obbligatoria del primo riscaldatore",
@@ -226,7 +226,7 @@
"valve_entity4_id": "Entity id della quarta valvola",
"auto_regulation_mode": "Autoregolamentazione",
"inverse_switch_command": "Inverte il controllo dell'interruttore per un'installazione con filo pilota e diodo",
"auto_fan_mode": " Automatically activate fan when huge heating/cooling is necessary"
"auto_fan_mode": "Automatically activate fan when huge heating/cooling is necessary"
}
},
"tpi": {

View File

@@ -5,7 +5,7 @@ import logging
from typing import Any
from enum import StrEnum
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, UnitOfTemperature
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import State
from homeassistant.exceptions import ServiceNotFound
@@ -30,6 +30,7 @@ from homeassistant.components.number import SERVICE_SET_VALUE
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_call_later
from homeassistant.util.unit_conversion import TemperatureConverter
from .const import UnknownEntity, overrides
from .keep_alive import IntervalCaller
@@ -252,7 +253,28 @@ class UnderlyingSwitch(UnderlyingEntity):
async def _keep_alive_callback(self):
"""Keep alive: Turn on if already turned on, turn off if already turned off."""
await (self.turn_on() if self.is_device_active else self.turn_off())
timer = self._keep_alive.backoff_timer
state: State | None = self._hass.states.get(self._entity_id)
# Normal, expected state.state values are "on" and "off". An absent
# underlying MQTT switch was observed to produce either state == None
# or state.state == STATE_UNAVAILABLE ("unavailable").
if state is None or state.state == STATE_UNAVAILABLE:
if timer.is_ready():
_LOGGER.warning(
"Entity %s is not available (state: %s). Will keep trying "
"keep alive calls, but won't log this condition every time.",
self._entity_id,
state.state if state else "None",
)
else:
if timer.in_progress:
timer.reset()
_LOGGER.warning(
"Entity %s has recovered (state: %s).",
self._entity_id,
state.state,
)
await (self.turn_on() if self.is_device_active else self.turn_off())
# @overrides this breaks some unit tests TypeError: object MagicMock can't be used in 'await' expression
async def turn_off(self):
@@ -590,12 +612,22 @@ class UnderlyingClimate(UnderlyingEntity):
if not self.is_initialized:
return
data = {
ATTR_ENTITY_ID: self._entity_id,
"temperature": self.cap_sent_value(temperature),
"target_temp_high": max_temp,
"target_temp_low": min_temp,
}
# Issue 508 we have to take care of service set_temperature or set_range
target_temp = self.cap_sent_value(temperature)
if (
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
in self._underlying_climate.supported_features
):
data = {
ATTR_ENTITY_ID: self._entity_id,
"target_temp_high": target_temp,
"target_temp_low": target_temp,
}
else:
data = {
ATTR_ENTITY_ID: self._entity_id,
"temperature": target_temp,
}
await self._hass.services.async_call(
CLIMATE_DOMAIN,
@@ -704,7 +736,7 @@ class UnderlyingClimate(UnderlyingEntity):
if not hasattr(self._underlying_climate, "current_temperature"):
return None
return self._underlying_climate.current_temperature
return self._hass.states.get(self._entity_id).attributes.get("current_temperature")
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
@@ -731,8 +763,12 @@ class UnderlyingClimate(UnderlyingEntity):
self._underlying_climate.min_temp is not None
and self._underlying_climate is not None
):
min_val = self._underlying_climate.min_temp
max_val = self._underlying_climate.max_temp
min_val = TemperatureConverter.convert(
self._underlying_climate.min_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
)
max_val = TemperatureConverter.convert(
self._underlying_climate.max_temp, self._underlying_climate.temperature_unit, self._hass.config.units.temperature_unit
)
new_value = max(min_val, min(value, max_val))
else:

View File

@@ -3,5 +3,5 @@
"content_in_root": false,
"render_readme": true,
"hide_default_branch": false,
"homeassistant": "2024.4.3"
"homeassistant": "2024.9.3"
}

View File

@@ -1 +1 @@
homeassistant==2024.4.3
homeassistant==2024.9.3

View File

@@ -435,6 +435,86 @@ class MagicMockClimate(MagicMock):
return 19
class MagicMockClimateWithTemperatureRange(MagicMock):
"""A Magic Mock class for a underlying climate entity"""
@property
def temperature_unit(self): # pylint: disable=missing-function-docstring
return UnitOfTemperature.CELSIUS
@property
def hvac_mode(self): # pylint: disable=missing-function-docstring
return HVACMode.HEAT
@property
def hvac_action(self): # pylint: disable=missing-function-docstring
return HVACAction.IDLE
@property
def target_temperature(self): # pylint: disable=missing-function-docstring
return 15
@property
def current_temperature(self): # pylint: disable=missing-function-docstring
return 14
@property
def target_temperature_step( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 0.5
@property
def target_temperature_high( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 35
@property
def target_temperature_low( # pylint: disable=missing-function-docstring
self,
) -> float | None:
return 7
@property
def hvac_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return [HVACMode.HEAT, HVACMode.OFF, HVACMode.COOL]
@property
def fan_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return None
@property
def swing_modes( # pylint: disable=missing-function-docstring
self,
) -> list[str] | None:
return None
@property
def fan_mode(self) -> str | None: # pylint: disable=missing-function-docstring
return None
@property
def swing_mode(self) -> str | None: # pylint: disable=missing-function-docstring
return None
@property
def supported_features(self): # pylint: disable=missing-function-docstring
return ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
@property
def min_temp(self): # pylint: disable=missing-function-docstring
return 10
@property
def max_temp(self): # pylint: disable=missing-function-docstring
return 31
class MockSwitch(SwitchEntity):
"""A fake switch to be used instead real switch"""
@@ -907,3 +987,8 @@ async def set_climate_preset_temp(
)
if temp_entity:
await temp_entity.async_set_native_value(temp)
else:
_LOGGER.warning(
"commons tests set_cliamte_preset_temp: cannot find number entity with entity_id '%s'",
number_entity_id,
)

View File

@@ -53,18 +53,6 @@ async def test_over_climate_regulation(
return_value=fake_underlying_climate,
):
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
# entry.add_to_hass(hass)
# await hass.config_entries.async_setup(entry.entry_id)
# assert entry.state is ConfigEntryState.LOADED
#
# def find_my_entity(entity_id) -> ClimateEntity:
# """Find my new entity"""
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# for entity in component.entities:
# if entity.entity_id == entity_id:
# return entity
#
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
@@ -163,18 +151,6 @@ async def test_over_climate_regulation_ac_mode(
return_value=fake_underlying_climate,
):
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
# entry.add_to_hass(hass)
# await hass.config_entries.async_setup(entry.entry_id)
# assert entry.state is ConfigEntryState.LOADED
#
# def find_my_entity(entity_id) -> ClimateEntity:
# """Find my new entity"""
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
# for entity in component.entities:
# if entity.entity_id == entity_id:
# return entity
#
# entity: ThermostatOverClimate = find_my_entity("climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
@@ -544,3 +520,112 @@ async def test_over_climate_regulation_use_device_temp(
),
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_climate_regulation_dtemp_null(
hass: HomeAssistant, skip_hass_states_is_state, skip_send_event
):
"""Test the regulation of an over climate thermostat with no Dtemp limitation"""
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# This is include a medium regulation
data=PARTIAL_CLIMATE_AC_CONFIG | {CONF_AUTO_REGULATION_DTEMP: 0, CONF_STEP_TEMPERATURE: 0.1},
)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {})
# Creates the regulated VTherm over climate
# change temperature so that the heating will start
event_timestamp = now - timedelta(minutes=20)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert entity
assert isinstance(entity, ThermostatOverClimate)
assert entity.name == "TheOverClimateMockName"
assert entity.is_over_climate is True
assert entity.is_regulated is True
# Activate the heating by changing HVACMode and temperature
# Select a hvacmode, presence and preset
await entity.async_set_hvac_mode(HVACMode.HEAT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.hvac_action == HVACAction.OFF
# change temperature so that the heating will start
await send_temperature_change_event(entity, 15, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# set manual target temp
event_timestamp = now - timedelta(minutes=17)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await entity.async_set_temperature(temperature=20)
fake_underlying_climate.set_hvac_action(
HVACAction.HEATING
) # simulate under cooling
assert entity.hvac_action == HVACAction.HEATING
assert entity.preset_mode == PRESET_NONE # Manual mode
# the regulated temperature should be lower
assert entity.regulated_target_temp > entity.target_temperature
assert (
entity.regulated_target_temp == 20 + 2.4
) # In medium we could go up to +3 degre
assert entity.hvac_action == HVACAction.HEATING
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=15)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 19, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 20 + 0.9
# change temperature so that the regulated temperature should slow down
event_timestamp = now - timedelta(minutes=13)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 20, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be greater
assert entity.regulated_target_temp > entity.target_temperature
assert entity.regulated_target_temp == 20 + 0.5
old_regulated_temp = entity.regulated_target_temp
# Test if a small temperature change is taken into account : change temperature so that dtemp < 0.5 and time is > period_min (+ 3min)
event_timestamp = now - timedelta(minutes=10)
with patch(
"custom_components.versatile_thermostat.commons.NowClass.get_now",
return_value=event_timestamp,
):
await send_temperature_change_event(entity, 19.6, event_timestamp)
await send_ext_temperature_change_event(entity, 10, event_timestamp)
# the regulated temperature should be greater. This does not work if dtemp is not null
assert entity.regulated_target_temp > old_regulated_temp

View File

@@ -12,6 +12,18 @@ from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
)
from custom_components.versatile_thermostat.config_flow import (
VersatileThermostatBaseConfigFlow,
)
from custom_components.versatile_thermostat.thermostat_valve import ThermostatOverValve
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
from .commons import *
logging.getLogger().setLevel(logging.DEBUG)
@@ -657,8 +669,8 @@ async def test_bug_272(
{
"entity_id": "climate.mock_climate",
"temperature": 17.5,
"target_temp_high": 30,
"target_temp_low": 15,
# "target_temp_high": 30,
# "target_temp_low": 15,
},
),
]
@@ -687,8 +699,8 @@ async def test_bug_272(
{
"entity_id": "climate.mock_climate",
"temperature": 15, # the minimum acceptable
"target_temp_high": 30,
"target_temp_low": 15,
# "target_temp_high": 30,
# "target_temp_low": 15,
},
),
]
@@ -714,8 +726,8 @@ async def test_bug_272(
{
"entity_id": "climate.mock_climate",
"temperature": 19, # the maximum acceptable
"target_temp_high": 30,
"target_temp_low": 15,
# "target_temp_high": 30,
# "target_temp_low": 15,
},
),
]
@@ -924,3 +936,278 @@ async def test_bug_339(
assert api.nb_active_device_for_boiler == 1
entity.remove_thermostat()
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_508(
hass: HomeAssistant,
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
):
"""Test that it not possible to set the target temperature under the min_temp setting"""
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="uniqueId",
# default value are min 15°, max 31°, step 0.1
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
)
# Min_temp is 10 and max_temp is 31 and features contains TARGET_TEMPERATURE_RANGE
fake_underlying_climate = MagicMockClimateWithTemperatureRange()
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
), patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
), patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call:
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
assert entity
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
assert mock_service_call.call_count == 0
# Set the hvac_mode to HEAT
await entity.async_set_hvac_mode(HVACMode.HEAT)
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
await entity.async_set_temperature(temperature=8.5)
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
# "temperature": 17.5,
"target_temp_high": 10,
"target_temp_low": 10,
},
),
]
)
with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
# Not In the accepted interval -> should be converted into 10 (the min) and send with target_temp_high and target_temp_low
await entity.async_set_temperature(temperature=32)
# MagicMock climate is already HEAT by default. So there is no SET_HAVC_MODE call
assert mock_service_call.call_count == 1
mock_service_call.assert_has_calls(
[
call.async_call(
"climate",
SERVICE_SET_TEMPERATURE,
{
"entity_id": "climate.mock_climate",
"target_temp_high": 31,
"target_temp_low": 31,
},
),
]
)
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_1(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: True,
CONF_USE_POWER_CENTRAL_CONFIG: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
CONF_USE_MOTION_FEATURE: True,
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
assert flow._infos[CONF_USE_POWER_FEATURE] is True
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_2(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_USE_MOTION_FEATURE: False,
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is False
assert flow._infos[CONF_USE_POWER_FEATURE] is False
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is False
assert flow._infos[CONF_USE_MOTION_FEATURE] is False
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
"""Test that the form is served with no input"""
config = {
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
CONF_WINDOW_SENSOR: "sensor.theWindowSensor",
CONF_USE_POWER_CENTRAL_CONFIG: False,
CONF_POWER_SENSOR: "sensor.thePowerSensor",
CONF_MAX_POWER_SENSOR: "sensor.theMaxPowerSensor",
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
CONF_PRESENCE_SENSOR: "sensor.thePresenceSensor",
CONF_USE_MOTION_FEATURE: True, # motion sensor need to be checked AND a motion sensor set
CONF_MOTION_SENSOR: "sensor.theMotionSensor",
}
flow = VersatileThermostatBaseConfigFlow(config)
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
assert flow._infos[CONF_USE_POWER_FEATURE] is True
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_524(hass: HomeAssistant, skip_hass_states_is_state):
"""Test when switching from Cool to Heat the new temperature in Heat mode should be used"""
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# The temperatures to set
temps = {
"frost": 7.0,
"eco": 17.0,
"comfort": 19.0,
"boost": 21.0,
"eco_ac": 27.0,
"comfort_ac": 25.0,
"boost_ac": 23.0,
"frost_away": 7.1,
"eco_away": 17.1,
"comfort_away": 19.1,
"boost_away": 21.1,
"eco_ac_away": 27.1,
"comfort_ac_away": 25.1,
"boost_ac_away": 23.1,
}
config_entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverClimateMockName",
unique_id="overClimateUniqueId",
data={
CONF_NAME: "overClimate",
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True,
CONF_PRESENCE_SENSOR: "binary_sensor.presence_sensor",
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SECURITY_DELAY_MIN: 5,
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
CONF_AC_MODE: True,
},
# | temps,
)
fake_underlying_climate = MockClimate(
hass=hass,
unique_id="mock_climate",
name="mock_climate",
hvac_modes=[HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.FAN_ONLY],
)
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=fake_underlying_climate,
):
vtherm: ThermostatOverClimate = await create_thermostat(
hass, config_entry, "climate.overclimate"
)
assert vtherm is not None
# We search for NumberEntities
for preset_name, value in temps.items():
await set_climate_preset_temp(vtherm, preset_name, value)
temp_entity: NumberEntity = search_entity(
hass,
"number.overclimate_preset_" + preset_name + PRESET_TEMP_SUFFIX,
NUMBER_DOMAIN,
)
assert temp_entity
# Because set_value is not implemented in Number class (really don't understand why...)
assert temp_entity.state == value
# 1. Set mode to Heat and preset to Comfort
await send_presence_change_event(vtherm, True, False, datetime.now())
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await vtherm.async_set_preset_mode(PRESET_COMFORT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# 2. Only change the HVAC_MODE (and keep preset to comfort)
await vtherm.async_set_hvac_mode(HVACMode.COOL)
await hass.async_block_till_done()
assert vtherm.target_temperature == 25.0
# 3. Only change the HVAC_MODE (and keep preset to comfort)
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.0
# 4. Change presence to off
await send_presence_change_event(vtherm, False, True, datetime.now())
await hass.async_block_till_done()
assert vtherm.target_temperature == 19.1
# 5. Change hvac_mode to AC
await vtherm.async_set_hvac_mode(HVACMode.COOL)
await hass.async_block_till_done()
assert vtherm.target_temperature == 25.1
# 6. Change presence to on
await send_presence_change_event(vtherm, True, False, datetime.now())
await hass.async_block_till_done()
assert vtherm.target_temperature == 25

View File

@@ -731,7 +731,7 @@ async def test_update_central_boiler_state_simple_climate(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
return_value=climate1,
):
entity: ThermostatOverValve = await create_thermostat(
entity: ThermostatOverClimate = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
)
assert entity

View File

@@ -5,9 +5,6 @@ from unittest.mock import patch, call
from datetime import timedelta, datetime
import logging
from custom_components.versatile_thermostat.thermostat_climate import (
ThermostatOverClimate,
)
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)

View File

@@ -254,6 +254,9 @@ async def test_over_switch_deactivate_preset(
CONF_HEATER_KEEP_ALIVE: 0,
CONF_SECURITY_DELAY_MIN: 10,
CONF_MINIMAL_ACTIVATION_DELAY: 10,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.6,
CONF_TPI_COEF_EXT: 0.01,
},
)

View File

@@ -6,6 +6,7 @@ from unittest.mock import ANY, _Call, call, patch
from datetime import datetime, timedelta
from typing import cast
from custom_components.versatile_thermostat.keep_alive import BackoffTimer
from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch,
)
@@ -52,6 +53,7 @@ class CommonMocks:
hass: HomeAssistant
thermostat: ThermostatOverSwitch
mock_is_state: MagicMock
mock_get_state: MagicMock
mock_service_call: MagicMock
mock_async_track_time_interval: MagicMock
mock_send_event: MagicMock
@@ -73,15 +75,18 @@ async def common_mocks(
thermostat = cast(ThermostatOverSwitch, await create_thermostat(
hass, config_entry, "climate.theoverswitchmockname"
))
yield CommonMocks(
config_entry=config_entry,
hass=hass,
thermostat=thermostat,
mock_is_state=mock_is_state,
mock_service_call=mock_service_call,
mock_async_track_time_interval=mock_async_track_time_interval,
mock_send_event=mock_send_event,
)
with patch("homeassistant.core.StateMachine.get") as mock_get_state:
mock_get_state.return_value.state = "off"
yield CommonMocks(
config_entry=config_entry,
hass=hass,
thermostat=thermostat,
mock_is_state=mock_is_state,
mock_get_state=mock_get_state,
mock_service_call=mock_service_call,
mock_async_track_time_interval=mock_async_track_time_interval,
mock_send_event=mock_send_event,
)
# Clean the entity
thermostat.remove_thermostat()
@@ -256,3 +261,123 @@ class TestKeepAlive:
call("switch", SERVICE_TURN_OFF, {"entity_id": "switch.mock_switch"}),
],
)
class TestBackoffTimer:
"""Test the keep_alive.BackoffTimer helper class."""
def test_exponential_period_increase(self):
"""Test that consecutive calls to is_ready() produce increasing wait periods."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
timer = BackoffTimer(
multiplier=2,
lower_limit_sec=30,
upper_limit_sec=86400,
initially_ready=True,
)
mock_monotonic.return_value = 100
assert timer.is_ready()
mock_monotonic.return_value = 129
assert not timer.is_ready()
mock_monotonic.return_value = 130
assert timer.is_ready()
mock_monotonic.return_value = 188
assert not timer.is_ready()
mock_monotonic.return_value = 189
assert not timer.is_ready()
mock_monotonic.return_value = 190
assert timer.is_ready()
mock_monotonic.return_value = 309
assert not timer.is_ready()
def test_the_upper_limit_option(self):
"""Test the timer.in_progress property and the effect of timer.reset()."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
timer = BackoffTimer(
multiplier=2,
lower_limit_sec=30,
upper_limit_sec=50,
initially_ready=True,
)
mock_monotonic.return_value = 100
assert timer.is_ready()
mock_monotonic.return_value = 129
assert not timer.is_ready()
mock_monotonic.return_value = 130
assert timer.is_ready()
mock_monotonic.return_value = 178
assert not timer.is_ready()
mock_monotonic.return_value = 179
assert not timer.is_ready()
mock_monotonic.return_value = 180
assert timer.is_ready()
mock_monotonic.return_value = 229
assert not timer.is_ready()
mock_monotonic.return_value = 230
assert timer.is_ready()
def test_the_lower_limit_option(self):
"""Test the timer.in_progress property and the effect of timer.reset()."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
timer = BackoffTimer(
multiplier=0.5,
lower_limit_sec=30,
upper_limit_sec=50,
initially_ready=True,
)
mock_monotonic.return_value = 100
assert timer.is_ready()
mock_monotonic.return_value = 129
assert not timer.is_ready()
mock_monotonic.return_value = 130
assert timer.is_ready()
mock_monotonic.return_value = 158
assert not timer.is_ready()
mock_monotonic.return_value = 159
assert not timer.is_ready()
mock_monotonic.return_value = 160
assert timer.is_ready()
def test_initial_is_ready_result(self):
"""Test that the first call to is_ready() produces the initially_ready option value."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
for initial in [True, False]:
timer = BackoffTimer(
multiplier=2,
lower_limit_sec=30,
upper_limit_sec=86400,
initially_ready=initial,
)
mock_monotonic.return_value = 100
assert timer.is_ready() == initial
assert not timer.is_ready()
def test_in_progress_and_reset(self):
"""Test the timer.in_progress property and the effect of timer.reset()."""
with patch(
"custom_components.versatile_thermostat.keep_alive.monotonic"
) as mock_monotonic:
timer = BackoffTimer(
multiplier=2,
lower_limit_sec=30,
upper_limit_sec=86400,
initially_ready=True,
)
mock_monotonic.return_value = 100
assert not timer.in_progress
assert timer.is_ready()
assert timer.in_progress
assert not timer.is_ready()
timer.reset()
assert not timer.in_progress
assert timer.is_ready()
assert timer.in_progress
assert not timer.is_ready()

View File

@@ -79,6 +79,7 @@ async def test_add_number_for_central_config(
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.2,
CONF_USE_CENTRAL_BOILER_FEATURE: False,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
}
| temps,
)
@@ -156,6 +157,7 @@ async def test_add_number_for_central_config_without_temp(
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
@@ -250,6 +252,7 @@ async def test_add_number_for_central_config_without_temp_ac_mode(
CONF_AC_MODE: True,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
@@ -343,6 +346,7 @@ async def test_add_number_for_central_config_without_temp_restore(
CONF_AC_MODE: False,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_WINDOW_DELAY: 15,
CONF_WINDOW_AUTO_OPEN_THRESHOLD: 4,
CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1,
@@ -441,6 +445,7 @@ async def test_add_number_for_over_switch_use_central(
CONF_AC_MODE: False,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
CONF_USE_MAIN_CENTRAL_CONFIG: True,
@@ -666,6 +671,7 @@ async def test_add_number_for_over_switch_use_central_presets_and_restore(
CONF_AC_MODE: False,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_CYCLE_MIN: 5,
CONF_HEATER: "switch.mock_switch1",
CONF_USE_PRESENCE_FEATURE: True,
@@ -788,6 +794,7 @@ async def test_change_central_config_temperature(
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_CYCLE_MIN: 5,
CONF_VALVE: "switch.mock_valve",
CONF_USE_PRESENCE_FEATURE: True,
@@ -823,6 +830,7 @@ async def test_change_central_config_temperature(
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_CYCLE_MIN: 5,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_VALVE: "switch.mock_valve",
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
@@ -905,6 +913,7 @@ async def test_change_vtherm_temperature(
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_CYCLE_MIN: 5,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_VALVE: "switch.mock_valve",
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: True,
@@ -939,6 +948,7 @@ async def test_change_vtherm_temperature(
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_CYCLE_MIN: 5,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_VALVE: "switch.mock_valve",
CONF_USE_PRESENCE_FEATURE: True,
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
@@ -1022,6 +1032,7 @@ async def test_change_vtherm_temperature_with_presence(
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_CYCLE_MIN: 5,
CONF_AC_MODE: True,
CONF_VALVE: "switch.mock_valve",
@@ -1063,6 +1074,7 @@ async def test_change_vtherm_temperature_with_presence(
CONF_TEMP_MAX: 30,
CONF_TPI_COEF_INT: 0.5,
CONF_TPI_COEF_EXT: 0.02,
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_CYCLE_MIN: 5,
CONF_VALVE: "switch.mock_valve",
CONF_USE_PRESENCE_FEATURE: True,

View File

@@ -3,7 +3,10 @@
from homeassistant.components.climate import HVACMode
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
from custom_components.versatile_thermostat.prop_algorithm import (
PropAlgorithm,
PROPORTIONAL_FUNCTION_TPI,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -121,3 +124,123 @@ async def test_tpi_calculation(
assert tpi_algo.calculated_on_percent == 0
assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_wrong_tpi_parameters(
hass: HomeAssistant, skip_hass_states_is_state: None
): # pylint: disable=unused-argument
"""Test the wrong TPI parameters"""
# Nominal case
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
0.01,
5,
1,
"entity_id",
)
# We should not be there
assert True
except TypeError as e:
# the normal case
assert False
# Test TPI function
try:
algo = PropAlgorithm(
"WRONG",
1,
0,
2,
3,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test coef_int
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
None,
0,
2,
3,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test coef_ext
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
None,
2,
3,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test cycle_min
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
0.00001,
None,
3,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test minimal_activation_delay
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
0.00001,
0,
None,
"entity_id",
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass
# Test vtherm_entity_id
try:
algo = PropAlgorithm(
PROPORTIONAL_FUNCTION_TPI,
0.6,
0.00001,
0,
12,
None,
)
# We should not be there
assert False
except TypeError as e:
# the normal case
pass