Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73a9ca4e53 | ||
|
|
1334bdbd8f | ||
|
|
646ef47f6f | ||
|
|
c344c43185 | ||
|
|
062f8a617d | ||
|
|
70f91f3cbe | ||
|
|
668053b352 | ||
|
|
6ff9ff1ee5 | ||
|
|
3f95ed74f4 | ||
|
|
6e42904ddf | ||
|
|
4c1fc396fb | ||
|
|
d6ec7a86be | ||
|
|
a3f8715fe5 | ||
|
|
a1a9a8bbab | ||
|
|
d5c5869276 | ||
|
|
c4b03f8c1e | ||
|
|
ac206a949f | ||
|
|
4bccb746b8 | ||
|
|
e999705286 | ||
|
|
b4873bfd27 | ||
|
|
b00dc09c80 | ||
|
|
da6d6cbce6 | ||
|
|
864e904e21 | ||
|
|
0ee4fe355d | ||
|
|
53dce224cd | ||
|
|
2fd60074c7 | ||
|
|
549423b313 | ||
|
|
6bd1b1137e | ||
|
|
189418e69a | ||
|
|
4ab932f44e | ||
|
|
e1ff23fb30 | ||
|
|
7b657ffabf |
@@ -1,14 +1,30 @@
|
||||
default_config:
|
||||
|
||||
recorder:
|
||||
auto_purge: true
|
||||
purge_keep_days: 1
|
||||
commit_interval: 5
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
- binary_sensor
|
||||
- number
|
||||
- input_select
|
||||
- versatile_thermostat
|
||||
|
||||
logger:
|
||||
default: warning
|
||||
logs:
|
||||
custom_components.versatile_thermostat: debug
|
||||
# custom_components.versatile_thermostat.underlyings: info
|
||||
# custom_components.versatile_thermostat.climate: info
|
||||
# custom_components.versatile_thermostat.base_thermostat: debug
|
||||
custom_components.versatile_thermostat.sensor: info
|
||||
custom_components.versatile_thermostat.binary_sensor: info
|
||||
custom_components.versatile_thermostat: debug
|
||||
# custom_components.versatile_thermostat.underlyings: info
|
||||
# custom_components.versatile_thermostat.climate: info
|
||||
# custom_components.versatile_thermostat.base_thermostat: debug
|
||||
custom_components.versatile_thermostat.sensor: info
|
||||
custom_components.versatile_thermostat.binary_sensor: info
|
||||
|
||||
# If you need to debug uncommment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
|
||||
debugpy:
|
||||
@@ -176,20 +192,6 @@ input_datetime:
|
||||
has_date: true
|
||||
has_time: true
|
||||
|
||||
recorder:
|
||||
commit_interval: 0
|
||||
include:
|
||||
domains:
|
||||
- input_boolean
|
||||
- input_number
|
||||
- switch
|
||||
- climate
|
||||
- sensor
|
||||
- binary_sensor
|
||||
- number
|
||||
- input_select
|
||||
- versatile_thermostat
|
||||
|
||||
template:
|
||||
- binary_sensor:
|
||||
- name: maison_occupee
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -4,7 +4,6 @@
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "modifications"
|
||||
},
|
||||
"pylint.lintOnChange": false,
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
},
|
||||
|
||||
@@ -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 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 :
|
||||
|
||||
|
||||
@@ -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 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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,16 +13,16 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
CoreState,
|
||||
Event,
|
||||
State,
|
||||
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.entity import Entity
|
||||
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 +58,6 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
)
|
||||
@@ -300,7 +299,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 +736,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
|
||||
@@ -1162,10 +1161,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
return len(self._underlyings)
|
||||
|
||||
@property
|
||||
def underlying_entities(self) -> int:
|
||||
def underlying_entities(self) -> list | None:
|
||||
"""Returns the underlying entities"""
|
||||
return self._underlyings
|
||||
|
||||
def find_underlying_by_entity_id(self, entity_id: str) -> Entity | None:
|
||||
"""Get the underlying entity by a entity_id"""
|
||||
for under in self._underlyings:
|
||||
if under.entity_id == entity_id:
|
||||
return under
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""True if the VTherm is on (! HVAC_OFF)"""
|
||||
@@ -1238,7 +1244,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 +1383,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 +1660,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 +1673,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 +1796,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 +1822,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 +1847,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 +1908,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 +2190,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"quality_scale": "silver",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"version": "6.2.2",
|
||||
"version": "6.3.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
@@ -11,13 +11,15 @@ from homeassistant.components.number import (
|
||||
NumberMode,
|
||||
NumberDeviceClass,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
DEFAULT_STEP,
|
||||
)
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
)
|
||||
from homeassistant.components.sensor import UnitOfTemperature
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -53,6 +55,7 @@ from .const import (
|
||||
CONF_USE_PRESENCE_FEATURE,
|
||||
CONF_USE_CENTRAL_BOILER_FEATURE,
|
||||
overrides,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG,
|
||||
)
|
||||
|
||||
PRESET_ICON_MAPPING = {
|
||||
@@ -279,7 +282,7 @@ class CentralConfigTemperatureNumber(
|
||||
self.entity_id = f"{NUMBER_DOMAIN}.{slugify(name)}_preset_{preset_name}"
|
||||
self._attr_unique_id = f"central_configuration_preset_{preset_name}"
|
||||
self._attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
self._attr_native_unit_of_measurement = hass.config.units.temperature_unit
|
||||
|
||||
self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5)
|
||||
self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN)
|
||||
@@ -341,7 +344,10 @@ class CentralConfigTemperatureNumber(
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""The value have change from the Number Entity in UI"""
|
||||
float_value = float(value)
|
||||
old_value = float(self._attr_native_value)
|
||||
old_value = (
|
||||
None if self._attr_native_value is None else float(self._attr_native_value)
|
||||
)
|
||||
|
||||
if float_value == old_value:
|
||||
return
|
||||
|
||||
@@ -364,7 +370,7 @@ class CentralConfigTemperatureNumber(
|
||||
# TODO Kelvin ? It seems not because all internal values are stored in
|
||||
# ° Celsius but only the render in front can be in °K depending on the
|
||||
# user configuration.
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
|
||||
class TemperatureNumber( # pylint: disable=abstract-method
|
||||
@@ -393,11 +399,13 @@ class TemperatureNumber( # pylint: disable=abstract-method
|
||||
|
||||
self._attr_unique_id = f"{self._device_name}_preset_{preset_name}"
|
||||
self._attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
self._attr_native_unit_of_measurement = hass.config.units.temperature_unit
|
||||
|
||||
self._attr_native_step = entry_infos.get(CONF_STEP_TEMPERATURE, 0.5)
|
||||
self._attr_native_min_value = entry_infos.get(CONF_TEMP_MIN)
|
||||
self._attr_native_max_value = entry_infos.get(CONF_TEMP_MAX)
|
||||
self._has_central_main_attributes = entry_infos.get(
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG, False
|
||||
)
|
||||
|
||||
self.init_min_max_step(entry_infos)
|
||||
|
||||
# Initialize the values if included into the entry_infos. This will do
|
||||
# the temperature migration.
|
||||
@@ -459,7 +467,9 @@ class TemperatureNumber( # pylint: disable=abstract-method
|
||||
return
|
||||
|
||||
float_value = float(value)
|
||||
old_value = float(self._attr_native_value)
|
||||
old_value = (
|
||||
None if self._attr_native_value is None else float(self._attr_native_value)
|
||||
)
|
||||
|
||||
if float_value == old_value:
|
||||
return
|
||||
@@ -476,6 +486,10 @@ class TemperatureNumber( # pylint: disable=abstract-method
|
||||
)
|
||||
)
|
||||
|
||||
# We set the min, max and step from central config if relevant because it is possible
|
||||
# that central config was not loaded at startup
|
||||
self.init_min_max_step()
|
||||
|
||||
def __str__(self):
|
||||
return f"VersatileThermostat-{self.name}"
|
||||
|
||||
@@ -483,5 +497,28 @@ class TemperatureNumber( # pylint: disable=abstract-method
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""The unit of measurement"""
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
def init_min_max_step(self, entry_infos=None):
|
||||
"""Initialize min, max and step value from config or from central config"""
|
||||
if self._has_central_main_attributes:
|
||||
vthermapi: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
central_config = vthermapi.find_central_configuration()
|
||||
if central_config:
|
||||
self._attr_native_step = central_config.data.get(CONF_STEP_TEMPERATURE)
|
||||
self._attr_native_min_value = central_config.data.get(CONF_TEMP_MIN)
|
||||
self._attr_native_max_value = central_config.data.get(CONF_TEMP_MAX)
|
||||
|
||||
return
|
||||
|
||||
if entry_infos:
|
||||
self._attr_native_step = entry_infos.get(
|
||||
CONF_STEP_TEMPERATURE, DEFAULT_STEP
|
||||
)
|
||||
self._attr_native_min_value = entry_infos.get(
|
||||
CONF_TEMP_MIN, DEFAULT_MIN_VALUE
|
||||
)
|
||||
self._attr_native_max_value = entry_infos.get(
|
||||
CONF_TEMP_MAX, DEFAULT_MAX_VALUE
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
""" The TPI calculation module """
|
||||
# pylint: disable='line-too-long'
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
@@ -14,6 +15,11 @@ PROPORTIONAL_MIN_DURATION_SEC = 10
|
||||
FUNCTION_TYPE = [PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR]
|
||||
|
||||
|
||||
def is_number(value):
|
||||
"""check if value is a number"""
|
||||
return isinstance(value, (int, float))
|
||||
|
||||
|
||||
class PropAlgorithm:
|
||||
"""This class aims to do all calculation of the Proportional alogorithm"""
|
||||
|
||||
@@ -36,6 +42,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(
|
||||
"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 +100,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
|
||||
|
||||
@@ -3,19 +3,15 @@
|
||||
""" Implements the VersatileThermostat select component """
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import HomeAssistant, CoreState, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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,
|
||||
ConfigData,
|
||||
)
|
||||
|
||||
@@ -126,6 +122,12 @@ class CentralModeSelect(SelectEntity, RestoreEntity):
|
||||
self._attr_current_option = option
|
||||
await self.notify_central_mode_change(old_central_mode=old_option)
|
||||
|
||||
@overrides
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option"""
|
||||
# Update the VTherms which have temperature in central config
|
||||
self.hass.create_task(self.async_select_option(option))
|
||||
|
||||
async def notify_central_mode_change(self, old_central_mode: str | None = None):
|
||||
"""Notify all VTherm that the central_mode have change"""
|
||||
api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self.hass)
|
||||
|
||||
@@ -570,7 +570,7 @@ class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
@property
|
||||
@@ -621,7 +621,7 @@ class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
if not self.my_climate:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self.hass.config.units.temperature_unit
|
||||
return self.my_climate.temperature_unit
|
||||
|
||||
@property
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.const import STATE_ON
|
||||
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,
|
||||
@@ -57,6 +57,13 @@ from .underlyings import UnderlyingClimate
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
HVACAction.COOLING,
|
||||
HVACAction.DRYING,
|
||||
HVACAction.FAN,
|
||||
HVACAction.HEATING,
|
||||
]
|
||||
|
||||
|
||||
class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
"""Representation of a base class for a Versatile Thermostat over a climate"""
|
||||
@@ -143,7 +150,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 +185,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 +222,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 +242,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 +620,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
|
||||
@@ -607,7 +633,8 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
async def end_climate_changed(changes: bool):
|
||||
"""To end the event management"""
|
||||
if changes:
|
||||
self.async_write_ha_state()
|
||||
# already done by update_custom_attribute
|
||||
# self.async_write_ha_state()
|
||||
self.update_custom_attributes()
|
||||
await self.async_control_heating()
|
||||
|
||||
@@ -616,6 +643,15 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
# Find the underlying which have change
|
||||
under = self.find_underlying_by_entity_id(new_state.entity_id)
|
||||
|
||||
if not under:
|
||||
_LOGGER.warning(
|
||||
"We have a receive an event from entity %s which is NOT one of our underlying entities. This is not normal and should be reported to the developper of the integration"
|
||||
)
|
||||
return
|
||||
|
||||
changes = False
|
||||
new_hvac_mode = new_state.state
|
||||
|
||||
@@ -650,20 +686,44 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
new_state.last_updated if new_state and new_state.last_updated else None
|
||||
)
|
||||
|
||||
new_target_temp = (
|
||||
new_state.attributes.get("temperature")
|
||||
if new_state and new_state.attributes
|
||||
else None
|
||||
)
|
||||
|
||||
# Issue 99 - some AC turn hvac_mode=cool and hvac_action=idle when sending a HVACMode_OFF command
|
||||
# Issue 114 - Remove this because hvac_mode is now managed by local _hvac_mode and use idle action as is
|
||||
# if self._hvac_mode == HVACMode.OFF and new_hvac_action == HVACAction.IDLE:
|
||||
# _LOGGER.debug("The underlying switch to idle instead of OFF. We will consider it as OFF")
|
||||
# new_hvac_mode = HVACMode.OFF
|
||||
|
||||
# Forget event when the event holds no real changes
|
||||
if (
|
||||
new_hvac_mode == self._hvac_mode
|
||||
and new_hvac_action == old_hvac_action
|
||||
and new_target_temp is None
|
||||
and (new_fan_mode is None or new_fan_mode == self._attr_fan_mode)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - a underlying state change event is received but no real change have been found. Forget the event",
|
||||
self,
|
||||
)
|
||||
return
|
||||
|
||||
# A real changes have to be managed
|
||||
_LOGGER.info(
|
||||
"%s - Underlying climate %s changed. Event.new_hvac_mode is %s, current_hvac_mode=%s, new_hvac_action=%s, old_hvac_action=%s",
|
||||
"%s - Underlying climate %s have changed. new_hvac_mode is %s (vs %s), new_hvac_action=%s (vs %s), new_target_temp=%s (vs %s), new_fan_mode=%s (vs %s)",
|
||||
self,
|
||||
new_state.entity_id,
|
||||
under.entity_id,
|
||||
new_hvac_mode,
|
||||
self._hvac_mode,
|
||||
new_hvac_action,
|
||||
old_hvac_action,
|
||||
new_target_temp,
|
||||
self.target_temperature,
|
||||
new_fan_mode,
|
||||
self._attr_fan_mode,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -677,12 +737,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
)
|
||||
|
||||
# Interpretation of hvac action
|
||||
HVAC_ACTION_ON = [ # pylint: disable=invalid-name
|
||||
HVACAction.COOLING,
|
||||
HVACAction.DRYING,
|
||||
HVACAction.FAN,
|
||||
HVACAction.HEATING,
|
||||
]
|
||||
if old_hvac_action not in HVAC_ACTION_ON and new_hvac_action in HVAC_ACTION_ON:
|
||||
self._underlying_climate_start_hvac_action_date = (
|
||||
self.get_last_updated_date_or_now(new_state)
|
||||
@@ -715,6 +769,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
)
|
||||
changes = True
|
||||
|
||||
# Filter new state when received just after a change from VTherm
|
||||
# Issue #120 - Some TRV are changing target temperature a very long time (6 sec) after the change.
|
||||
# In that case a loop is possible if a user change multiple times during this 6 sec.
|
||||
if new_state_date_updated and self._last_change_time:
|
||||
@@ -727,6 +782,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
await end_climate_changed(changes)
|
||||
return
|
||||
|
||||
# Update all underlyings hvac_mode state if it has change
|
||||
if (
|
||||
new_hvac_mode
|
||||
in [
|
||||
@@ -741,7 +797,6 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
]
|
||||
and self._hvac_mode != new_hvac_mode
|
||||
):
|
||||
# Update all underlyings state
|
||||
# Issue #334 - if all underlyings are not aligned with the same hvac_mode don't change the underlying and wait they are aligned
|
||||
if self.is_over_climate:
|
||||
for under in self._underlyings:
|
||||
@@ -772,27 +827,31 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self._attr_fan_mode = new_fan_mode
|
||||
changes = True
|
||||
|
||||
# try to manage new target temperature set if state if no other changes have been found
|
||||
if not changes:
|
||||
# try to manage new target temperature set if state
|
||||
_LOGGER.debug(
|
||||
"Do temperature check. temperature is %s, new_state.attributes is %s",
|
||||
self.target_temperature,
|
||||
new_state.attributes,
|
||||
"Do temperature check. under.last_sent_temperature is %s, new_target_temp is %s",
|
||||
under.last_sent_temperature,
|
||||
new_target_temp,
|
||||
)
|
||||
if (
|
||||
# we do not change target temperature on regulated VTherm
|
||||
not self.is_regulated
|
||||
and new_state.attributes
|
||||
and (new_target_temp := new_state.attributes.get("temperature"))
|
||||
and new_target_temp != self.target_temperature
|
||||
# if the underlying have change its target temperature
|
||||
new_target_temp is not None
|
||||
and new_target_temp != under.last_sent_temperature
|
||||
):
|
||||
_LOGGER.info(
|
||||
"%s - Target temp in underlying have change to %s",
|
||||
"%s - Target temp in underlying have change to %s (vs %s)",
|
||||
self,
|
||||
new_target_temp,
|
||||
under.last_sent_temperature,
|
||||
)
|
||||
await self.async_set_temperature(temperature=new_target_temp)
|
||||
changes = True
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - Forget the eventual underlying temperature change because VTherm is regulated",
|
||||
self,
|
||||
)
|
||||
|
||||
await end_climate_changed(changes)
|
||||
|
||||
@@ -894,10 +953,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):
|
||||
@@ -1088,3 +1144,29 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
|
||||
self.choose_auto_fan_mode(CONF_AUTO_FAN_TURBO)
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
@overrides
|
||||
async def async_turn_off(self) -> None:
|
||||
# if window is open, don't overwrite the saved_hvac_mode
|
||||
if self.window_state != STATE_ON:
|
||||
self.save_hvac_mode()
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
@overrides
|
||||
async def async_turn_on(self) -> None:
|
||||
|
||||
# don't turn_on if window is open
|
||||
if self.window_state == STATE_ON:
|
||||
_LOGGER.info(
|
||||
"%s - refuse to turn on because window is open. We keep the save_hvac_mode",
|
||||
self,
|
||||
)
|
||||
return
|
||||
|
||||
if self._saved_hvac_mode is not None: # pylint: disable=protected-access
|
||||
await self.restore_hvac_mode(True)
|
||||
else:
|
||||
if self._ac_mode:
|
||||
await self.async_set_hvac_mode(HVACMode.COOL)
|
||||
else:
|
||||
await self.async_set_hvac_mode(HVACMode.HEAT)
|
||||
|
||||
@@ -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 (
|
||||
@@ -187,7 +186,8 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
self._hvac_mode or HVACMode.OFF,
|
||||
)
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
# already done bu update_custom_attributes
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
@@ -204,6 +204,8 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||
self,
|
||||
@@ -212,7 +214,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")
|
||||
|
||||
@@ -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
|
||||
@@ -34,26 +33,24 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method
|
||||
"""Representation of a class for a Versatile Thermostat over a Valve"""
|
||||
|
||||
_entity_component_unrecorded_attributes = (
|
||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
||||
frozenset(
|
||||
{
|
||||
"is_over_valve",
|
||||
"underlying_valve_0",
|
||||
"underlying_valve_1",
|
||||
"underlying_valve_2",
|
||||
"underlying_valve_3",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
}
|
||||
)
|
||||
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||
frozenset(
|
||||
{
|
||||
"is_over_valve",
|
||||
"underlying_valve_0",
|
||||
"underlying_valve_1",
|
||||
"underlying_valve_2",
|
||||
"underlying_valve_3",
|
||||
"on_time_sec",
|
||||
"off_time_sec",
|
||||
"cycle_min",
|
||||
"function",
|
||||
"tpi_coef_int",
|
||||
"tpi_coef_ext",
|
||||
"auto_regulation_dpercent",
|
||||
"auto_regulation_period_min",
|
||||
"last_calculation_timestamp",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -149,7 +146,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.
|
||||
"""
|
||||
@@ -242,10 +239,16 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
max(0, min(self.proportional_algorithm.on_percent, 1)) * 100
|
||||
)
|
||||
|
||||
# Issue 533 - don't filter with dtemp if valve should be close. Else it will never close
|
||||
if new_valve_percent < self._auto_regulation_dpercent:
|
||||
new_valve_percent = 0
|
||||
|
||||
dpercent = new_valve_percent - self.valve_open_percent
|
||||
if (
|
||||
dpercent >= -1 * self._auto_regulation_dpercent
|
||||
and dpercent < self._auto_regulation_dpercent
|
||||
new_valve_percent > 0
|
||||
and -1 * self._auto_regulation_dpercent
|
||||
<= dpercent
|
||||
< self._auto_regulation_dpercent
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - do not calculate TPI because regulation_dpercent (%.1f) is not exceeded",
|
||||
@@ -267,7 +270,8 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
self._last_calculation_timestamp = now
|
||||
|
||||
self.update_custom_attributes()
|
||||
self.async_write_ha_state()
|
||||
# already done in update_custom_attributes
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@overrides
|
||||
def incremente_energy(self):
|
||||
@@ -284,6 +288,8 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
|
||||
else:
|
||||
self._total_energy += added_energy
|
||||
|
||||
self.update_custom_attributes()
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - added energy is %.3f . Total energy is now: %.3f",
|
||||
self,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -12,6 +12,25 @@
|
||||
"thermostat_type": "Len jeden centrálny typ konfigurácie je možný"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.",
|
||||
"menu_options": {
|
||||
"main": "Hlavné atribúty",
|
||||
"central_boiler": "Centrálny kotol",
|
||||
"type": "Podklady",
|
||||
"tpi": "TPI parametre",
|
||||
"features": "Vlastnosti",
|
||||
"presets": "Predvoľby",
|
||||
"window": "Detekcia okien",
|
||||
"motion": "Detekcia pohybu",
|
||||
"power": "Správa napájania",
|
||||
"presence": "Detekcia prítomnosti",
|
||||
"advanced": "Pokročilé parametre",
|
||||
"finalize": "Všetko hotové",
|
||||
"configuration_not_complete": "Konfigurácia nie je dokončená"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Pridajte nový všestranný termostat",
|
||||
"description": "Hlavné povinné atribúty",
|
||||
@@ -19,22 +38,32 @@
|
||||
"name": "Názov",
|
||||
"thermostat_type": "Termostat typ",
|
||||
"temperature_sensor_entity_id": "ID entity snímača teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty",
|
||||
"external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
|
||||
"cycle_min": "Trvanie cyklu (minúty)",
|
||||
"temp_min": "Minimálna povolená teplota",
|
||||
"temp_max": "Maximálna povolená teplota",
|
||||
"step_temperature": "Krok teploty",
|
||||
"device_power": "Napájanie zariadenia",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
|
||||
"use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).",
|
||||
"used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "ID entity snímača izbovej teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Vlastnosti",
|
||||
"description": "Vlastnosti termostatu",
|
||||
"data": {
|
||||
"use_window_feature": "Použite detekciu okien",
|
||||
"use_motion_feature": "Použite detekciu pohybu",
|
||||
"use_power_feature": "Použite správu napájania",
|
||||
"use_presence_feature": "Použite detekciu prítomnosti",
|
||||
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
|
||||
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Zrušte začiarknutie, ak chcete použiť špecifickú hlavnú konfiguráciu pre tento VTherm",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
"use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -45,6 +74,7 @@
|
||||
"heater_entity2_id": "2. spínač ohrievača",
|
||||
"heater_entity3_id": "3. spínač ohrievača",
|
||||
"heater_entity4_id": "4. spínač ohrievača",
|
||||
"heater_keep_alive": "Prepnite interval udržiavania v sekundách",
|
||||
"proportional_function": "Algoritmus",
|
||||
"climate_entity_id": "1. základná klíma",
|
||||
"climate_entity2_id": "2. základná klíma",
|
||||
@@ -58,6 +88,7 @@
|
||||
"auto_regulation_mode": "Samoregulácia",
|
||||
"auto_regulation_dtemp": "Regulačný prah",
|
||||
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
|
||||
"auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
|
||||
"inverse_switch_command": "Inverzný prepínací príkaz",
|
||||
"auto_fan_mode": "Režim automatického ventilátora"
|
||||
},
|
||||
@@ -66,6 +97,7 @@
|
||||
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.",
|
||||
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
|
||||
"climate_entity_id": "ID základnej klimatickej entity",
|
||||
"climate_entity2_id": "2. základné identifikačné číslo klimatickej entity",
|
||||
@@ -79,6 +111,7 @@
|
||||
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
|
||||
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
|
||||
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
|
||||
"auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
|
||||
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
|
||||
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
|
||||
}
|
||||
@@ -101,24 +134,7 @@
|
||||
"title": "Predvoľby",
|
||||
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
|
||||
"data": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavení Frost protection",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
},
|
||||
"data_description": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
@@ -130,7 +146,8 @@
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna",
|
||||
"window_action": "Akcia"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
@@ -138,7 +155,8 @@
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm",
|
||||
"window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -181,26 +199,11 @@
|
||||
"title": "Riadenie prítomnosti",
|
||||
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
|
||||
"presence_sensor_entity_id": "Senzora prítomnosti",
|
||||
"use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické teplotné entity, zrušte výber"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -244,6 +247,25 @@
|
||||
"thermostat_type": "Je možný len jeden centrálny typ konfigurácie"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu",
|
||||
"description": "Nakonfigurujte si termostat. Po zadaní všetkých požadovaných parametrov budete môcť dokončiť konfiguráciu.",
|
||||
"menu_options": {
|
||||
"main": "Hlavné atribúty",
|
||||
"central_boiler": "Centrálny kotol",
|
||||
"type": "Podklady",
|
||||
"tpi": "TPI parametre",
|
||||
"features": "Vlastnosti",
|
||||
"presets": "Predvoľby",
|
||||
"window": "Detekcia okien",
|
||||
"motion": "Detekcia pohybu",
|
||||
"power": "Správa napájania",
|
||||
"presence": "Detekcia prítomnosti",
|
||||
"advanced": "Pokročilé parametre",
|
||||
"finalize": "Všetko hotové",
|
||||
"configuration_not_complete": "Konfigurácia nie je dokončená"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"title": "Hlavný - {name}",
|
||||
"description": "Hlavné povinné atribúty",
|
||||
@@ -251,22 +273,32 @@
|
||||
"name": "Názov",
|
||||
"thermostat_type": "Termostat typ",
|
||||
"temperature_sensor_entity_id": "ID entity snímača teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Dátum posledného zobrazenia izbovej teploty",
|
||||
"external_temperature_sensor_entity_id": "ID entity externého snímača teploty",
|
||||
"cycle_min": "Trvanie cyklu (minúty)",
|
||||
"temp_min": "Minimálna povolená teplota",
|
||||
"temp_max": "Maximálna povolená teplota",
|
||||
"step_temperature": "Krok teploty",
|
||||
"device_power": "Výkon zariadenia (kW)",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (potrebná centrálna konfigurácia)",
|
||||
"use_central_mode": "Povoliť ovládanie centrálnou entitou (vyžaduje centrálnu konfiguráciu). Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode.",
|
||||
"use_main_central_config": "Použite dodatočnú centrálnu hlavnú konfiguráciu. Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu (vonkajšia teplota, min, max, krok, ...).",
|
||||
"used_by_controls_central_boiler": "Používa sa centrálnym kotlom. Skontrolujte, či má mať tento VTherm ovládanie na centrálnom kotli"
|
||||
},
|
||||
"data_description": {
|
||||
"temperature_sensor_entity_id": "ID entity snímača izbovej teploty",
|
||||
"last_seen_temperature_sensor_entity_id": "Naposledy videný snímač izbovej teploty ID entity. Mal by to byť snímač dátumu a času",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Vlastnosti - {name}",
|
||||
"description": "Vlastnosti termostatu",
|
||||
"data": {
|
||||
"use_window_feature": "Použite detekciu okien",
|
||||
"use_motion_feature": "Použite detekciu pohybu",
|
||||
"use_power_feature": "Použite správu napájania",
|
||||
"use_presence_feature": "Použite detekciu prítomnosti",
|
||||
"use_main_central_config": "Použite centrálnu hlavnú konfiguráciu"
|
||||
},
|
||||
"data_description": {
|
||||
"use_central_mode": "Zaškrtnutím povolíte ovládanie VTherm pomocou vybraných entít central_mode",
|
||||
"use_main_central_config": "Začiarknite, ak chcete použiť centrálnu hlavnú konfiguráciu. Ak chcete použiť špecifickú konfiguráciu pre tento VTherm, zrušte začiarknutie",
|
||||
"external_temperature_sensor_entity_id": "ID entity snímača vonkajšej teploty. Nepoužíva sa, ak je zvolená centrálna konfigurácia"
|
||||
"use_central_boiler_feature": "Použite centrálny kotol. Začiarknutím tohto políčka pridáte ovládanie do centrálneho kotla. Po zaškrtnutí tohto políčka budete musieť nakonfigurovať VTherm, ktorý bude mať ovládanie centrálneho kotla, aby sa prejavilo. Ak jeden VTherm vyžaduje ohrev, kotol sa zapne. Ak žiadny VTherm nevyžaduje ohrev, kotol sa vypne. Príkazy na zapnutie/vypnutie centrálneho kotla sú uvedené na príslušnej konfiguračnej stránke"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
@@ -277,6 +309,7 @@
|
||||
"heater_entity2_id": "2. spínač ohrievača",
|
||||
"heater_entity3_id": "3. spínač ohrievača",
|
||||
"heater_entity4_id": "4. spínač ohrievača",
|
||||
"heater_keep_alive": "Prepnite interval udržiavania v sekundách",
|
||||
"proportional_function": "Algoritmus",
|
||||
"climate_entity_id": "Základná klíma",
|
||||
"climate_entity2_id": "2. základná klíma",
|
||||
@@ -290,6 +323,7 @@
|
||||
"auto_regulation_mode": "Samoregulácia",
|
||||
"auto_regulation_dtemp": "Regulačný prah",
|
||||
"auto_regulation_periode_min": "Regulačné minimálne obdobie",
|
||||
"auto_regulation_use_device_temp": "Použite vnútornú teplotu podkladu",
|
||||
"inverse_switch_command": "Inverzný prepínací príkaz",
|
||||
"auto_fan_mode": "Režim automatického ventilátora"
|
||||
},
|
||||
@@ -298,6 +332,7 @@
|
||||
"heater_entity2_id": "Voliteľné ID entity 2. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity3_id": "Voliteľné ID entity 3. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_entity4_id": "Voliteľné ID entity 4. ohrievača. Ak sa nepoužíva, nechajte prázdne",
|
||||
"heater_keep_alive": "Voliteľný interval obnovy stavu spínača ohrievača. Ak to nie je potrebné, nechajte prázdne.",
|
||||
"proportional_function": "Algoritmus, ktorý sa má použiť (TPI je zatiaľ jediný)",
|
||||
"climate_entity_id": "ID základnej klimatickej entity",
|
||||
"climate_entity2_id": "2. základný identifikátor klimatickej entity",
|
||||
@@ -311,6 +346,7 @@
|
||||
"auto_regulation_mode": "Automatické nastavenie cieľovej teploty",
|
||||
"auto_regulation_dtemp": "Hranica v °, pod ktorou sa zmena teploty neodošle",
|
||||
"auto_regulation_periode_min": "Trvanie v minútach medzi dvoma aktualizáciami predpisov",
|
||||
"auto_regulation_use_device_temp": "Na urýchlenie samoregulácie použite prípadný vnútorný snímač teploty podkladu",
|
||||
"inverse_switch_command": "V prípade spínača s pilotným vodičom a diódou možno budete musieť príkaz invertovať",
|
||||
"auto_fan_mode": "Automaticky aktivujte ventilátor, keď je potrebné veľké vykurovanie/chladenie"
|
||||
}
|
||||
@@ -333,24 +369,7 @@
|
||||
"title": "Predvoľby - {name}",
|
||||
"description": "Pre každú predvoľbu zadajte cieľovú teplotu (0, ak chcete predvoľbu ignorovať)",
|
||||
"data": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavení Frost protection",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Použite konfiguráciu centrálnych predvolieb"
|
||||
},
|
||||
"data_description": {
|
||||
"eco_temp": "Teplota v predvoľbe Eco",
|
||||
"comfort_temp": "Prednastavená teplota v komfortnom režime",
|
||||
"boost_temp": "Teplota v prednastavení Boost",
|
||||
"frost_temp": "Teplota v prednastavenej ochrane proti mrazu",
|
||||
"eco_ac_temp": "Teplota v režime Eco prednastavená pre režim AC",
|
||||
"comfort_ac_temp": "Teplota v režime Comfort je prednastavená pre režim AC",
|
||||
"boost_ac_temp": "Prednastavená teplota v režime Boost pre režim AC",
|
||||
"use_presets_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnych predvolieb. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu predvolieb pre tento VTherm"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
@@ -362,7 +381,8 @@
|
||||
"window_auto_open_threshold": "Prah poklesu teploty pre automatickú detekciu otvoreného okna (v °/hodina)",
|
||||
"window_auto_close_threshold": "Prahová hodnota zvýšenia teploty pre koniec automatickej detekcie (v °/hodina)",
|
||||
"window_auto_max_duration": "Maximálne trvanie automatickej detekcie otvoreného okna (v min)",
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna"
|
||||
"use_window_central_config": "Použite centrálnu konfiguráciu okna",
|
||||
"window_action": "Akcia"
|
||||
},
|
||||
"data_description": {
|
||||
"window_sensor_entity_id": "Nechajte prázdne, ak nemáte použiť žiadny okenný senzor",
|
||||
@@ -370,7 +390,8 @@
|
||||
"window_auto_open_threshold": "Odporúčaná hodnota: medzi 3 a 10. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_close_threshold": "Odporúčaná hodnota: 0. Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"window_auto_max_duration": "Odporúčaná hodnota: 60 (jedna hodina). Ak sa nepoužíva automatická detekcia otvoreného okna, nechajte prázdne",
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm"
|
||||
"use_window_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálneho okna. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu okna pre tento VTherm",
|
||||
"window_action": "Akcia, ktorá sa má vykonať, ak sa okno zistí ako otvorené"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
@@ -410,29 +431,14 @@
|
||||
}
|
||||
},
|
||||
"presence": {
|
||||
"title": "Riadenie prítomnosti",
|
||||
"description": "Atribúty správy prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, ak je niekto prítomný).\nPotom zadajte buď predvoľbu, ktorá sa má použiť, keď je senzor prítomnosti nepravdivý, alebo posun teploty, ktorý sa má použiť.\nAk je zadaná predvoľba, posun sa nepoužije.\nAk sa nepoužije, ponechajte zodpovedajúce entity_id prázdne.",
|
||||
"title": "Prítommnosť - {name}",
|
||||
"description": "Atribúty riadenia prítomnosti.\nPoskytuje senzor prítomnosti vášho domova (pravda, je niekto prítomný) a poskytuje zodpovedajúce prednastavené nastavenie teploty.",
|
||||
"data": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti (pravda je prítomná)",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Prednastavená teplota v režime Frost protection, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Použite centrálnu konfiguráciu prítomnosti"
|
||||
"presence_sensor_entity_id": "Senzor prítomnosti",
|
||||
"use_presence_central_config": "Použite konfiguráciu centrálnej prítomnosti teploty. Ak chcete použiť špecifické entity teploty, zrušte začiarknutie"
|
||||
},
|
||||
"data_description": {
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti",
|
||||
"eco_away_temp": "Teplota v prednastavenej Eco, keď nie je žiadna prítomnosť",
|
||||
"comfort_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný",
|
||||
"boost_away_temp": "Prednastavená teplota v režime Boost, keď nie je prítomný",
|
||||
"frost_away_temp": "Teplota v Prednastavená ochrana pred mrazom, keď nie je prítomný",
|
||||
"eco_ac_away_temp": "Teplota v prednastavenej Eco, keď nie je prítomná v režime AC",
|
||||
"comfort_ac_away_temp": "Teplota v režime Comfort je prednastavená, keď nie je prítomný v režime AC",
|
||||
"boost_ac_away_temp": "Teplota v prednastavenom Boost, keď nie je prítomný v režime AC",
|
||||
"use_presence_central_config": "Začiarknite, ak chcete použiť konfiguráciu centrálnej prítomnosti. Zrušte začiarknutie, ak chcete použiť špecifickú konfiguráciu prítomnosti pre tento VTherm"
|
||||
"presence_sensor_entity_id": "ID entity senzora prítomnosti"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -458,7 +464,8 @@
|
||||
"unknown": "Neočakávaná chyba",
|
||||
"unknown_entity": "Neznáme ID entity",
|
||||
"window_open_detection_method": "Mala by sa použiť iba jedna metóda detekcie otvoreného okna. Použite senzor alebo automatickú detekciu cez teplotný prah, ale nie oboje",
|
||||
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“."
|
||||
"no_central_config": "Nemôžete zaškrtnúť „použiť centrálnu konfiguráciu“, pretože sa nenašla žiadna centrálna konfigurácia. Aby ste ho mohli používať, musíte si vytvoriť všestranný termostat typu „Central Configuration“.",
|
||||
"service_configuration_format": "Formát konfigurácie služby je nesprávny"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Zariadenie je už nakonfigurované"
|
||||
@@ -491,6 +498,22 @@
|
||||
"auto_fan_high": "Vysoký",
|
||||
"auto_fan_turbo": "Turbo"
|
||||
}
|
||||
},
|
||||
"window_action": {
|
||||
"options": {
|
||||
"window_turn_off": "Vypnúť",
|
||||
"window_fan_only": "Len ventilátor",
|
||||
"window_frost_temp": "Ochrana pred mrazom",
|
||||
"window_eco_temp": "Eco"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"options": {
|
||||
"frost": "Ochrana proti mrazu",
|
||||
"eco": "Eco",
|
||||
"comfort": "Komfort",
|
||||
"boost": "Boost"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -506,6 +529,53 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"frost_temp": {
|
||||
"name": "Mráz"
|
||||
},
|
||||
"eco_temp": {
|
||||
"name": "Eco"
|
||||
},
|
||||
"comfort_temp": {
|
||||
"name": "Komfort"
|
||||
},
|
||||
"boost_temp": {
|
||||
"name": "Boost"
|
||||
},
|
||||
"frost_ac_temp": {
|
||||
"name": "Mráz ac"
|
||||
},
|
||||
"eco_ac_temp": {
|
||||
"name": "Eco ac"
|
||||
},
|
||||
"comfort_ac_temp": {
|
||||
"name": "Komfort ac"
|
||||
},
|
||||
"boost_ac_temp": {
|
||||
"name": "Boost ac"
|
||||
},
|
||||
"frost_away_temp": {
|
||||
"name": "Mráz mimo"
|
||||
},
|
||||
"eco_away_temp": {
|
||||
"name": "Eko mimo"
|
||||
},
|
||||
"comfort_away_temp": {
|
||||
"name": "Komfort mimo"
|
||||
},
|
||||
"boost_away_temp": {
|
||||
"name": "Boost mimo"
|
||||
},
|
||||
"eco_ac_away_temp": {
|
||||
"name": "Eco ac mimo"
|
||||
},
|
||||
"comfort_ac_away_temp": {
|
||||
"name": "Komfort ac mimo"
|
||||
},
|
||||
"boost_ac_away_temp": {
|
||||
"name": "Boost ac mimo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
@@ -466,6 +488,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
entity_id=climate_entity_id,
|
||||
)
|
||||
self._underlying_climate = None
|
||||
self._last_sent_temperature = None
|
||||
|
||||
def find_underlying_climate(self) -> ClimateEntity:
|
||||
"""Find the underlying climate entity"""
|
||||
@@ -590,12 +613,24 @@ 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,
|
||||
# issue 518 - we should send also the target temperature, even in TARGET RANGE
|
||||
"temperature": target_temp,
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
"temperature": target_temp,
|
||||
}
|
||||
|
||||
await self._hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
@@ -603,6 +638,13 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
data,
|
||||
)
|
||||
|
||||
self._last_sent_temperature = target_temp
|
||||
|
||||
@property
|
||||
def last_sent_temperature(self) -> float | None:
|
||||
"""Get the last send temperature. None if no temperature have been sent yet"""
|
||||
return self._last_sent_temperature
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Get the hvac action of the underlying"""
|
||||
@@ -663,7 +705,7 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
def temperature_unit(self) -> str:
|
||||
"""Get the temperature_unit"""
|
||||
if not self.is_initialized:
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return self._hass.config.units.temperature_unit
|
||||
return self._underlying_climate.temperature_unit
|
||||
|
||||
@property
|
||||
@@ -687,6 +729,13 @@ class UnderlyingClimate(UnderlyingEntity):
|
||||
return 15
|
||||
return self._underlying_climate.target_temperature_low
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Get the target_temperature"""
|
||||
if not self.is_initialized:
|
||||
return None
|
||||
return self._underlying_climate.target_temperature
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Get the is_aux_heat"""
|
||||
@@ -704,7 +753,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 +780,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:
|
||||
@@ -844,8 +897,10 @@ class UnderlyingValve(UnderlyingEntity):
|
||||
):
|
||||
"""We use this function to change the on_percent"""
|
||||
if force:
|
||||
self._percent_open = self.cap_sent_value(self._percent_open)
|
||||
await self.send_percent_open()
|
||||
# self._percent_open = self.cap_sent_value(self._percent_open)
|
||||
# await self.send_percent_open()
|
||||
# avoid to send 2 times the same value at startup
|
||||
self.set_valve_open_percent()
|
||||
|
||||
@overrides
|
||||
def cap_sent_value(self, value) -> float:
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"content_in_root": false,
|
||||
"render_readme": true,
|
||||
"hide_default_branch": false,
|
||||
"homeassistant": "2024.4.3"
|
||||
"homeassistant": "2024.9.3"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
homeassistant==2024.4.3
|
||||
homeassistant==2024.9.3
|
||||
|
||||
131
tests/commons.py
131
tests/commons.py
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, abstract-method, too-many-lines, redefined-builtin
|
||||
|
||||
""" Some common resources """
|
||||
import asyncio
|
||||
@@ -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"""
|
||||
|
||||
@@ -472,7 +552,14 @@ class MockNumber(NumberEntity):
|
||||
"""A fake switch to be used instead real switch"""
|
||||
|
||||
def __init__( # pylint: disable=unused-argument, dangerous-default-value
|
||||
self, hass: HomeAssistant, unique_id, name, entry_infos={}
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
unique_id,
|
||||
name,
|
||||
min=0,
|
||||
max=100,
|
||||
step=1,
|
||||
entry_infos={},
|
||||
):
|
||||
"""Init the switch"""
|
||||
super().__init__()
|
||||
@@ -482,7 +569,9 @@ class MockNumber(NumberEntity):
|
||||
self.entity_id = self.platform + "." + unique_id
|
||||
self._name = name
|
||||
self._attr_native_value = 0
|
||||
self._attr_native_min_value = 0
|
||||
self._attr_native_min_value = min
|
||||
self._attr_native_max_value = max
|
||||
self._attr_step = step
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -842,6 +931,7 @@ async def send_climate_change_event_with_temperature(
|
||||
date,
|
||||
temperature,
|
||||
sleep=True,
|
||||
underlying_entity_id=None,
|
||||
):
|
||||
"""Sending a new climate event simulating a change on the underlying climate state"""
|
||||
_LOGGER.info(
|
||||
@@ -854,18 +944,21 @@ async def send_climate_change_event_with_temperature(
|
||||
temperature,
|
||||
entity,
|
||||
)
|
||||
if not underlying_entity_id:
|
||||
underlying_entity_id = entity.entity_id
|
||||
|
||||
climate_event = Event(
|
||||
EVENT_STATE_CHANGED,
|
||||
{
|
||||
"new_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=underlying_entity_id,
|
||||
state=new_hvac_mode,
|
||||
attributes={"hvac_action": new_hvac_action, "temperature": temperature},
|
||||
last_changed=date,
|
||||
last_updated=date,
|
||||
),
|
||||
"old_state": State(
|
||||
entity_id=entity.entity_id,
|
||||
entity_id=underlying_entity_id,
|
||||
state=old_hvac_mode,
|
||||
attributes={"hvac_action": old_hvac_action},
|
||||
last_changed=date,
|
||||
@@ -907,3 +1000,31 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
async def set_all_climate_preset_temp(
|
||||
hass, vtherm: BaseThermostat, temps: dict, number_entity_base_name: str
|
||||
):
|
||||
"""Initialize all temp of preset for a VTherm entity"""
|
||||
# We initialize
|
||||
for preset_name, value in temps.items():
|
||||
|
||||
await set_climate_preset_temp(vtherm, preset_name, value)
|
||||
|
||||
# Search the number entity to control it is correctly set
|
||||
number_entity_name = (
|
||||
f"number.{number_entity_base_name}_preset_{preset_name}{PRESET_TEMP_SUFFIX}"
|
||||
)
|
||||
temp_entity: NumberEntity = search_entity(
|
||||
hass,
|
||||
number_entity_name,
|
||||
NUMBER_DOMAIN,
|
||||
)
|
||||
assert temp_entity
|
||||
# Because set_value is not implemented in Number class (really don't understand why...)
|
||||
assert temp_entity.state == value
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
|
||||
|
||||
""" Test the Window management """
|
||||
import asyncio
|
||||
@@ -8,92 +8,27 @@ from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import (
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.config_flow import (
|
||||
VersatileThermostatBaseConfigFlow,
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_56(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that in over_climate mode there is no error when underlying climate is not available"""
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=None, # dont find the underlying climate
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
# cause the underlying climate was not found
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.underlying_entity(0)._underlying_climate is None
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
ret = await entity.async_control_heating()
|
||||
# an exception should be send
|
||||
assert ret is False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# This time the underlying will be found
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying, # dont find the underlying climate
|
||||
):
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
await entity.async_control_heating()
|
||||
except UnknownEntity:
|
||||
assert False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_63(
|
||||
@@ -193,391 +128,6 @@ async def test_bug_64(
|
||||
assert entity
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_66(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to open/close the window rapidly without side effect"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
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: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
||||
CONF_DEVICE_POWER: 200,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# Open the window and let the thermostat shut down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, now, False
|
||||
)
|
||||
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert mock_condition.call_count == 1
|
||||
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Close the window but too shortly
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should not have change
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Reopen immediatly with sufficient time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# still no change
|
||||
assert entity.window_state == STATE_ON
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
|
||||
# Close the window but with sufficient time this time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=2)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should be Off this time and old state should have been restored
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_82(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockUnavailableClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
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 = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
# assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# assert entity.hvac_mode is None
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force safety mode
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (
|
||||
entity._last_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
assert (
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# Tries to turns on the Thermostat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == "none"
|
||||
assert entity._saved_preset_mode == "none"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_101(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Underlying is in HEAT mode but should be shutdown at startup
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
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 = find_my_entity("climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
# Underlying should have been shutdown
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.OFF),
|
||||
]
|
||||
)
|
||||
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force preset mode
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
now,
|
||||
12.75,
|
||||
)
|
||||
# Should NOT have been switched to Manual preset
|
||||
assert entity.target_temperature == 17
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
12.75,
|
||||
)
|
||||
assert entity.target_temperature == 12.75
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_272(
|
||||
@@ -657,8 +207,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 +237,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 +264,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,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -849,78 +399,217 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
assert entity.overpowering_state is True
|
||||
|
||||
|
||||
async def test_bug_339(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the counter of active Vtherm in central boiler is
|
||||
correctly updated with underlying is in auto and device is active
|
||||
"""
|
||||
@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"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
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",
|
||||
}
|
||||
|
||||
climate1 = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="climate1",
|
||||
name="theClimate1",
|
||||
hvac_mode=HVACMode.AUTO,
|
||||
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
|
||||
hvac_action=HVACAction.HEATING,
|
||||
)
|
||||
flow = VersatileThermostatBaseConfigFlow(config)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
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_465(hass: HomeAssistant, skip_hass_states_is_state):
|
||||
"""Test store and restore hvac_mode on toggle hvac state"""
|
||||
|
||||
# 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,
|
||||
}
|
||||
|
||||
# 0. initialisation
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
unique_id="overClimateUniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
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: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||
CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF,
|
||||
CONF_WINDOW_DELAY: 1,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: climate1.entity_id,
|
||||
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_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
CONF_AUTO_FAN_MODE: CONF_AUTO_FAN_TURBO,
|
||||
CONF_AC_MODE: True,
|
||||
},
|
||||
)
|
||||
|
||||
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=climate1,
|
||||
return_value=fake_underlying_climate,
|
||||
):
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
vtherm: ThermostatOverClimate = await create_thermostat(
|
||||
hass, config_entry, "climate.overclimate"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate
|
||||
assert entity.underlying_entities[0].entity_id == "climate.climate1"
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
assert vtherm is not None
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.AUTO)
|
||||
# Simulate a state change in underelying
|
||||
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
|
||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overclimate")
|
||||
|
||||
# The VTherm should be active
|
||||
assert entity.underlying_entity(0).is_device_active is True
|
||||
assert entity.is_device_active is True
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
now: datetime = datetime.now(tz=get_tz(hass))
|
||||
|
||||
entity.remove_thermostat()
|
||||
# 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_BOOST)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 21.0
|
||||
|
||||
# 2. Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 3. (re)Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 4. Toggle from COOL
|
||||
await vtherm.async_set_hvac_mode(HVACMode.COOL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 23.0
|
||||
|
||||
# 5. Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 6. (re)Toggle the VTherm state
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
|
||||
###
|
||||
# Same test with an open window and initial state is COOL
|
||||
#
|
||||
# 7. open the window
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
try_window_condition = await send_window_change_event(
|
||||
vtherm, True, False, now, False
|
||||
)
|
||||
await try_window_condition(None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.window_state is STATE_ON
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 8. call toggle -> we should stay in OFF (command is ignored)
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
# 9. Close the window (we should come back to Cool this time)
|
||||
now = now + timedelta(minutes=2)
|
||||
with patch("homeassistant.helpers.condition.state", return_value=True):
|
||||
try_window_condition = await send_window_change_event(
|
||||
vtherm, False, True, now, False
|
||||
)
|
||||
await try_window_condition(None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.window_state is STATE_OFF
|
||||
assert vtherm.hvac_mode == HVACMode.COOL
|
||||
|
||||
# 9. call toggle -> we should come back in OFF
|
||||
await vtherm.async_toggle()
|
||||
await hass.async_block_till_done()
|
||||
assert vtherm.hvac_mode == HVACMode.OFF
|
||||
|
||||
@@ -69,7 +69,7 @@ async def test_add_a_central_config_with_boiler(
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold is 1 # the default value is 1
|
||||
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1
|
||||
|
||||
|
||||
async def test_update_central_boiler_state_simple(
|
||||
@@ -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
|
||||
@@ -842,3 +842,80 @@ async def test_update_central_boiler_state_simple_climate(
|
||||
assert boiler_binary_sensor.state == STATE_OFF
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
async def test_bug_339(
|
||||
hass: HomeAssistant,
|
||||
# skip_hass_states_is_state,
|
||||
init_central_config_with_boiler_fixture,
|
||||
):
|
||||
"""Test that the counter of active Vtherm in central boiler is
|
||||
correctly updated with underlying is in auto and device is active
|
||||
"""
|
||||
|
||||
api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||
|
||||
climate1 = MockClimate(
|
||||
hass=hass,
|
||||
unique_id="climate1",
|
||||
name="theClimate1",
|
||||
hvac_mode=HVACMode.AUTO,
|
||||
hvac_modes=[HVACMode.AUTO, HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
|
||||
hvac_action=HVACAction.HEATING,
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 8,
|
||||
CONF_TEMP_MAX: 18,
|
||||
"frost_temp": 10,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 21,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: climate1.entity_id,
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1,
|
||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||
CONF_USE_PRESETS_CENTRAL_CONFIG: True,
|
||||
CONF_USE_ADVANCED_CENTRAL_CONFIG: True,
|
||||
CONF_USED_BY_CENTRAL_BOILER: True,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=climate1,
|
||||
):
|
||||
entity: ThermostatOverValve = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate
|
||||
assert entity.underlying_entities[0].entity_id == "climate.climate1"
|
||||
assert api.nb_active_device_for_boiler_threshold == 1
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.AUTO)
|
||||
# Simulate a state change in underelying
|
||||
await api.nb_active_device_for_boiler_entity.calculate_nb_active_devices(None)
|
||||
|
||||
# The VTherm should be active
|
||||
assert entity.underlying_entity(0).is_device_active is True
|
||||
assert entity.is_device_active is True
|
||||
assert api.nb_active_device_for_boiler == 1
|
||||
|
||||
entity.remove_thermostat()
|
||||
|
||||
@@ -514,4 +514,4 @@ async def test_migration_of_central_config(
|
||||
assert api.nb_active_device_for_boiler == 0
|
||||
|
||||
assert api.nb_active_device_for_boiler_threshold_entity is not None
|
||||
assert api.nb_active_device_for_boiler_threshold is 1 # the default value is 1
|
||||
assert api.nb_active_device_for_boiler_threshold == 1 # the default value is 1
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ async def test_movement_management_time_not_enough(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
# starts detecting motion with time not enough
|
||||
with patch(
|
||||
@@ -110,7 +110,7 @@ async def test_movement_management_time_not_enough(
|
||||
assert entity.target_temperature == 18
|
||||
# state is not changed if time is not enough
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is not confirmed
|
||||
@@ -141,8 +141,8 @@ async def test_movement_management_time_not_enough(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
# stop detecting motion with off delay too low
|
||||
with patch(
|
||||
@@ -167,8 +167,8 @@ async def test_movement_management_time_not_enough(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# The heater must heat now
|
||||
@@ -199,8 +199,8 @@ async def test_movement_management_time_not_enough(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# The heater must stop heating now
|
||||
@@ -280,7 +280,7 @@ async def test_movement_management_time_enough_and_presence(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -302,8 +302,8 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
@@ -331,8 +331,8 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "on"
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "on"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
assert mock_heater_on.call_count == 0
|
||||
@@ -412,7 +412,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -434,8 +434,8 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost away mode
|
||||
assert entity.target_temperature == 19.1
|
||||
assert entity.motion_state is "on"
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
@@ -463,8 +463,8 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18.1
|
||||
assert entity.motion_state is "off"
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
# 18.1 starts heating with a low on_percent
|
||||
@@ -546,7 +546,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -569,7 +569,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# Send a stop detection
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
@@ -580,7 +580,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
# Resend a start detection
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
@@ -592,10 +592,10 @@ async def test_movement_management_with_stop_during_condition(
|
||||
# still no motion detected
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.presence_state is "off"
|
||||
assert entity.presence_state == "off"
|
||||
|
||||
await try_condition1(None)
|
||||
# We should have switch this time
|
||||
assert entity.target_temperature == 19 # Boost
|
||||
assert entity.motion_state is "on" # switch to movement on
|
||||
assert entity.presence_state is "off" # Non change
|
||||
assert entity.motion_state == "on" # switch to movement on
|
||||
assert entity.presence_state == "off" # Non change
|
||||
|
||||
@@ -596,6 +596,7 @@ async def test_multiple_climates_underlying_changes(
|
||||
HVACAction.IDLE,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
underlying_entity_id="switch.mock_climate3",
|
||||
)
|
||||
|
||||
# Should be call for all Switch
|
||||
|
||||
508
tests/test_overclimate.py
Normal file
508
tests/test_overclimate.py
Normal file
@@ -0,0 +1,508 @@
|
||||
# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, too-many-lines
|
||||
|
||||
""" Test the Window management """
|
||||
from unittest.mock import patch, call
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.components.climate import (
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
|
||||
from custom_components.versatile_thermostat.thermostat_climate import (
|
||||
ThermostatOverClimate,
|
||||
)
|
||||
|
||||
from .commons import *
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_56(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that in over_climate mode there is no error when underlying climate is not available"""
|
||||
|
||||
the_mock_underlying = MagicMockClimate()
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=None, # dont find the underlying climate
|
||||
):
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverClimateMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_CLIMATE: "climate.mock_climate",
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverclimatemockname"
|
||||
)
|
||||
assert entity
|
||||
# cause the underlying climate was not found
|
||||
assert entity.is_over_climate is True
|
||||
assert entity.underlying_entity(0)._underlying_climate is None
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
ret = await entity.async_control_heating()
|
||||
# an exception should be send
|
||||
assert ret is False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# This time the underlying will be found
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=the_mock_underlying, # dont find the underlying climate
|
||||
):
|
||||
# try to call async_control_heating
|
||||
try:
|
||||
await entity.async_control_heating()
|
||||
except UnknownEntity:
|
||||
assert False
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
assert False
|
||||
|
||||
# Should not failed
|
||||
entity.update_custom_attributes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_82(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate is not available the VTherm doesn't go into safety mode"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
fake_underlying_climate = MockUnavailableClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate:
|
||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
||||
|
||||
assert entity
|
||||
|
||||
assert entity.name == "TheOverClimateMockName"
|
||||
assert entity.is_over_climate is True
|
||||
# assert entity.hvac_action is HVACAction.OFF
|
||||
assert entity.hvac_mode is HVACMode.OFF
|
||||
# assert entity.hvac_mode is None
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_modes == [
|
||||
PRESET_NONE,
|
||||
PRESET_FROST_PROTECTION,
|
||||
PRESET_ECO,
|
||||
PRESET_COMFORT,
|
||||
PRESET_BOOST,
|
||||
]
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# Force safety mode
|
||||
assert entity._last_ext_temperature_measure is not None
|
||||
assert entity._last_temperature_measure is not None
|
||||
assert (
|
||||
entity._last_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
assert (
|
||||
entity._last_ext_temperature_measure.astimezone(tz) - now
|
||||
).total_seconds() < 1
|
||||
|
||||
# Tries to turns on the Thermostat
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
|
||||
# 2. activate security feature when date is expired
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
|
||||
# set temperature to 15 so that on_percent will be > security_min_on_percent (0.2)
|
||||
await send_temperature_change_event(entity, 15, event_timestamp)
|
||||
# Should stay False
|
||||
assert entity.security_state is False
|
||||
assert entity.preset_mode == "none"
|
||||
assert entity._saved_preset_mode == "none"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_101(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverClimateMockName",
|
||||
unique_id="uniqueId",
|
||||
data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay
|
||||
)
|
||||
|
||||
# Underlying is in HEAT mode but should be shutdown at startup
|
||||
fake_underlying_climate = MockClimate(
|
||||
hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING
|
||||
)
|
||||
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||
return_value=fake_underlying_climate,
|
||||
) as mock_find_climate, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||
) as mock_underlying_set_hvac_mode:
|
||||
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
|
||||
# because in MockClimate HVACAction is HEATING if hvac_mode is not set
|
||||
assert entity.hvac_action is HVACAction.HEATING
|
||||
# Underlying should have been shutdown
|
||||
assert mock_underlying_set_hvac_mode.call_count == 1
|
||||
mock_underlying_set_hvac_mode.assert_has_calls(
|
||||
[
|
||||
call.set_hvac_mode(HVACMode.OFF),
|
||||
]
|
||||
)
|
||||
|
||||
assert entity.target_temperature == entity.min_temp
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
# should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT
|
||||
assert mock_send_event.call_count == 2
|
||||
mock_send_event.assert_has_calls(
|
||||
[
|
||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}),
|
||||
call.send_event(
|
||||
EventType.HVAC_MODE_EVENT,
|
||||
{"hvac_mode": HVACMode.OFF},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert mock_find_climate.call_count == 1
|
||||
assert mock_find_climate.mock_calls[0] == call()
|
||||
mock_find_climate.assert_has_calls([call.find_underlying_entity()])
|
||||
|
||||
# 1. Force preset mode
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
assert entity.hvac_mode == HVACMode.HEAT
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
assert entity.preset_mode == PRESET_COMFORT
|
||||
|
||||
# 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121)
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
now,
|
||||
12.75,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
# Should NOT have been switched to Manual preset
|
||||
assert entity.target_temperature == 17
|
||||
assert entity.preset_mode is PRESET_COMFORT
|
||||
|
||||
# 3. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken
|
||||
# Wait 11 sec
|
||||
event_timestamp = now + timedelta(seconds=11)
|
||||
assert entity.is_regulated is False
|
||||
await send_climate_change_event_with_temperature(
|
||||
entity,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.HEAT,
|
||||
HVACAction.OFF,
|
||||
HVACAction.OFF,
|
||||
event_timestamp,
|
||||
12.75,
|
||||
True,
|
||||
"climate.mock_climate", # the underlying climate entity id
|
||||
)
|
||||
assert entity.target_temperature == 12.75
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
|
||||
|
||||
@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"""
|
||||
|
||||
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,
|
||||
"temperature": 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,
|
||||
"temperature": 31,
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
@@ -433,6 +433,7 @@ async def test_power_management_energy_over_climate(
|
||||
new_hvac_action=HVACAction.HEATING,
|
||||
old_hvac_action=HVACAction.OFF,
|
||||
date=event_timestamp,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
# We have the start event and not the end event
|
||||
assert (entity._underlying_climate_start_hvac_action_date - now).total_seconds() < 1
|
||||
@@ -448,6 +449,7 @@ async def test_power_management_energy_over_climate(
|
||||
new_hvac_action=HVACAction.IDLE,
|
||||
old_hvac_action=HVACAction.HEATING,
|
||||
date=now,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
# We have the end event -> we should have some power and on_percent
|
||||
assert entity._underlying_climate_start_hvac_action_date is None
|
||||
|
||||
@@ -283,6 +283,7 @@ async def test_sensors_over_climate(
|
||||
new_hvac_action=HVACAction.HEATING,
|
||||
old_hvac_action=HVACAction.OFF,
|
||||
date=event_timestamp,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
|
||||
# Send a climate_change event with HVACAction=IDLE (end of heating)
|
||||
@@ -293,6 +294,7 @@ async def test_sensors_over_climate(
|
||||
new_hvac_action=HVACAction.IDLE,
|
||||
old_hvac_action=HVACAction.HEATING,
|
||||
date=now,
|
||||
underlying_entity_id="climate.mock_climate",
|
||||
)
|
||||
|
||||
# 60 minutes heating with 1.5 kW heating -> 1.5 kWh
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,10 +6,6 @@ from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.components.climate import HVACAction, HVACMode
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
|
||||
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
@@ -551,3 +547,168 @@ async def test_over_valve_regulation(
|
||||
|
||||
assert mock_service_call.call_count == 0
|
||||
assert mock_send_event.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_533(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
): # pylint: disable=unused-argument
|
||||
"""Test that with an over_valve and _auto_regulation_dpercent is set that the valve could close totally"""
|
||||
|
||||
# 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,
|
||||
}
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverValveMockName",
|
||||
unique_id="overValveUniqueId",
|
||||
data={
|
||||
CONF_NAME: "overValve",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_VALVE,
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.5,
|
||||
CONF_TPI_COEF_EXT: 0,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
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: False,
|
||||
CONF_VALVE: "number.mock_valve",
|
||||
CONF_AUTO_REGULATION_DTEMP: 10, # This parameter makes the bug
|
||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||
CONF_SECURITY_DELAY_MIN: 60,
|
||||
},
|
||||
# | temps,
|
||||
)
|
||||
|
||||
# Not used because number is not registred so we can use directly the underlying number
|
||||
# fake_underlying_number = MockNumber(
|
||||
# hass=hass, unique_id="mock_number", name="mock_number"
|
||||
# )
|
||||
|
||||
vtherm: ThermostatOverValve = await create_thermostat(
|
||||
hass, config_entry, "climate.overvalve"
|
||||
)
|
||||
|
||||
assert vtherm is not None
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# Set all temps and check they are correctly initialized
|
||||
await set_all_climate_preset_temp(hass, vtherm, temps, "overvalve")
|
||||
await send_temperature_change_event(vtherm, 15, now)
|
||||
await send_ext_temperature_change_event(vtherm, 15, now)
|
||||
|
||||
# 1. Set mode to Heat and preset to Comfort
|
||||
await vtherm.async_set_hvac_mode(HVACMode.HEAT)
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="number.mock_valve",
|
||||
state="100",
|
||||
attributes={"min": 0, "max": 100},
|
||||
),
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await vtherm.async_set_preset_mode(PRESET_COMFORT)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert vtherm.target_temperature == 19.0
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 100},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 2. set current temperature to 18 -> still 50% open, so there is a call
|
||||
now = now + timedelta(minutes=1)
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="number.mock_valve",
|
||||
state="100",
|
||||
attributes={"min": 0, "max": 100},
|
||||
),
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(vtherm, 18, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 50},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 3. set current temperature to 18.8 -> still 10% open, so there is one call
|
||||
now = now + timedelta(minutes=1)
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="number.mock_valve",
|
||||
state="50",
|
||||
attributes={"min": 0, "max": 100},
|
||||
),
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(vtherm, 18.8, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 10},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# 4. set current temperature to 19 -> should have 0% open and one call to set the 0
|
||||
now = now + timedelta(minutes=1)
|
||||
with patch(
|
||||
"homeassistant.core.StateMachine.get",
|
||||
return_value=State(
|
||||
entity_id="number.mock_valve",
|
||||
state="10", # the previous value
|
||||
attributes={"min": 0, "max": 100},
|
||||
),
|
||||
), patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call:
|
||||
await send_temperature_change_event(vtherm, 19, now)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_service_call.call_count == 1
|
||||
mock_service_call.assert_has_calls(
|
||||
[
|
||||
call.async_call(
|
||||
domain="number",
|
||||
service="set_value",
|
||||
service_data={"value": 0},
|
||||
target={"entity_id": "number.mock_valve"},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1925,3 +1925,162 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
|
||||
|
||||
# Clean the entity
|
||||
entity.remove_thermostat()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_bug_66(
|
||||
hass: HomeAssistant,
|
||||
skip_hass_states_is_state,
|
||||
skip_turn_on_off_heater,
|
||||
skip_send_event,
|
||||
):
|
||||
"""Test that it should be possible to open/close the window rapidly without side effect"""
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="TheOverSwitchMockName",
|
||||
unique_id="uniqueId",
|
||||
data={
|
||||
CONF_NAME: "TheOverSwitchMockName",
|
||||
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: True,
|
||||
CONF_USE_MOTION_FEATURE: False,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: False,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
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: 5,
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.5,
|
||||
CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here
|
||||
CONF_DEVICE_POWER: 200,
|
||||
CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor",
|
||||
CONF_WINDOW_DELAY: 0, # important to not been obliged to wait
|
||||
},
|
||||
)
|
||||
|
||||
entity: BaseThermostat = await create_thermostat(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.window_state is STATE_OFF
|
||||
|
||||
# Open the window and let the thermostat shut down
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=True,
|
||||
):
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, now, False
|
||||
)
|
||||
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
assert mock_send_event.call_count == 1
|
||||
assert mock_heater_on.call_count == 1
|
||||
assert mock_heater_off.call_count >= 1
|
||||
assert mock_condition.call_count == 1
|
||||
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Close the window but too shortly
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=False
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=1)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should not have change
|
||||
assert entity.window_state == STATE_ON
|
||||
|
||||
# Reopen immediatly with sufficient time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# still no change
|
||||
assert entity.window_state == STATE_ON
|
||||
assert entity.hvac_mode == HVACMode.OFF
|
||||
|
||||
# Close the window but with sufficient time this time
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||
) as mock_heater_on, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||
) as mock_heater_off, patch(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition, patch(
|
||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||
return_value=False,
|
||||
):
|
||||
event_timestamp = now + timedelta(minutes=2)
|
||||
try_window_condition = await send_window_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
# simulate the call to try_window_condition
|
||||
await try_window_condition(None)
|
||||
|
||||
# window state should be Off this time and old state should have been restored
|
||||
assert entity.window_state == STATE_OFF
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_BOOST
|
||||
|
||||
Reference in New Issue
Block a user