Issue_766-enhance_power_management (#778)
* First implem + tests (not finished) * With tests of calculate_shedding ok * Commit for rebase * All tests ok for central_feature_power_manager * All tests not ok * All tests ok * integrattion tests - Do startup works * enhance the overpowering algo if current_power > max_power * Change shedding calculation delay to 20 sec (vs 60 sec) * Integration tests ok * Fix overpowering is set even if other heater have on_percent = 0 * Fix too much shedding in over_climate * Add logs * Add temporal filter for calculate_shedding Add restore overpowering state at startup * Fix restore overpowering_state * Removes poweer_entity_id from vtherm non central config * Release * Add Sonoff TRVZB in creation.md * Add comment on Sonoff TRVZB Closing degree --------- Co-authored-by: Jean-Marc Collin <jean-marc.collin-extern@renault.com>
This commit is contained in:
@@ -54,6 +54,7 @@
|
|||||||
"python.analysis.autoSearchPaths": true,
|
"python.analysis.autoSearchPaths": true,
|
||||||
"pylint.lintOnChange": false,
|
"pylint.lintOnChange": false,
|
||||||
"python.formatting.provider": "black",
|
"python.formatting.provider": "black",
|
||||||
|
"python.formatting.blackArgs": ["--line-length", "180"],
|
||||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||||
"editor.formatOnPaste": false,
|
"editor.formatOnPaste": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
|||||||
43
README-fr.md
43
README-fr.md
@@ -39,27 +39,28 @@ Un grand merci à tous mes fournisseurs de bières pour leurs dons et leurs enco
|
|||||||
|
|
||||||
La documentation est maintenant découpée en plusieurs pages pour faciliter la lecture et la recherche d'informations :
|
La documentation est maintenant découpée en plusieurs pages pour faciliter la lecture et la recherche d'informations :
|
||||||
1. [présentation](documentation/fr/presentation.md),
|
1. [présentation](documentation/fr/presentation.md),
|
||||||
2. [choisir un type de VTherm](documentation/fr/creation.md),
|
2. [Installation](documentation/fr/installation.md),
|
||||||
3. [les attributs de base](documentation/fr/base-attributes.md)
|
3. [choisir un type de VTherm](documentation/fr/creation.md),
|
||||||
3. [configurer un VTherm sur un `switch`](documentation/fr/over-switch.md)
|
4. [les attributs de base](documentation/fr/base-attributes.md)
|
||||||
3. [configurer un VTherm sur un `climate`](documentation/fr/over-climate.md)
|
5. [configurer un VTherm sur un `switch`](documentation/fr/over-switch.md)
|
||||||
3. [configurer un VTherm sur une vanne](documentation/fr/over-valve.md)
|
6. [configurer un VTherm sur un `climate`](documentation/fr/over-climate.md)
|
||||||
4. [les pré-régages (preset)](documentation/fr/feature-presets.md)
|
7. [configurer un VTherm sur une vanne](documentation/fr/over-valve.md)
|
||||||
5. [la gestion des ouvertures](documentation/fr/feature-window.md)
|
8. [les pré-régages (preset)](documentation/fr/feature-presets.md)
|
||||||
6. [la gestion de la présence](documentation/fr/feature-presence.md)
|
9. [la gestion des ouvertures](documentation/fr/feature-window.md)
|
||||||
7. [la gestion de mouvement](documentation/fr/feature-motion.md)
|
10. [la gestion de la présence](documentation/fr/feature-presence.md)
|
||||||
8. [la gestion de la puissance](documentation/fr/feature-power.md)
|
11. [la gestion de mouvement](documentation/fr/feature-motion.md)
|
||||||
9. [l'auto start and stop](documentation/fr/feature-auto-start-stop.md)
|
12. [la gestion de la puissance](documentation/fr/feature-power.md)
|
||||||
10. [la contrôle centralisé de tous vos VTherms](documentation/fr/feature-central-mode.md)
|
13. [l'auto start and stop](documentation/fr/feature-auto-start-stop.md)
|
||||||
11. [la commande du chauffage central](documentation/fr/feature-central-boiler.md)
|
14. [la contrôle centralisé de tous vos VTherms](documentation/fr/feature-central-mode.md)
|
||||||
12. [aspects avancés, mode sécurité](documentation/fr/feature-advanced.md)
|
15. [la commande du chauffage central](documentation/fr/feature-central-boiler.md)
|
||||||
12. [l'auto-régulation](documentation/fr/self-regulation.md)
|
16. [aspects avancés, mode sécurité](documentation/fr/feature-advanced.md)
|
||||||
13. [exemples de réglages](documentation/fr/tuning-examples.md)
|
17. [l'auto-régulation](documentation/fr/self-regulation.md)
|
||||||
14. [les différents algorithmes](documentation/fr/algorithms.md)
|
18. [exemples de réglages](documentation/fr/tuning-examples.md)
|
||||||
15. [documentation de référence](documentation/fr/reference.md)
|
19. [les différents algorithmes](documentation/fr/algorithms.md)
|
||||||
16. [exemple de réglages](documentation/fr/tuning-examples.md)
|
20. [documentation de référence](documentation/fr/reference.md)
|
||||||
17. [dépannage](documentation/fr/troubleshooting.md)
|
21. [exemple de réglages](documentation/fr/tuning-examples.md)
|
||||||
18. [notes de version](documentation/fr/releases.md)
|
22. [dépannage](documentation/fr/troubleshooting.md)
|
||||||
|
23. [notes de version](documentation/fr/releases.md)
|
||||||
|
|
||||||
|
|
||||||
# Quelques résultats
|
# Quelques résultats
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class BaseFeatureManager:
|
class BaseFeatureManager:
|
||||||
"""A base class for all feature"""
|
"""A base class for all feature"""
|
||||||
|
|
||||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
def __init__(self, vtherm: Any, hass: HomeAssistant, name: str = None):
|
||||||
"""Init of a featureManager"""
|
"""Init of a featureManager"""
|
||||||
self._vtherm = vtherm
|
self._vtherm = vtherm
|
||||||
self._name = vtherm.name
|
self._name = vtherm.name if vtherm else name
|
||||||
self._active_listener: list[CALLBACK_TYPE] = []
|
self._active_listener: list[CALLBACK_TYPE] = []
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ class BaseFeatureManager:
|
|||||||
"""Initialize the attributes of the FeatureManager"""
|
"""Initialize the attributes of the FeatureManager"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@@ -38,6 +38,10 @@ class BaseFeatureManager:
|
|||||||
|
|
||||||
self._active_listener = []
|
self._active_listener = []
|
||||||
|
|
||||||
|
async def refresh_state(self):
|
||||||
|
"""Refresh the state and return True if a change have been made"""
|
||||||
|
return False
|
||||||
|
|
||||||
def add_listener(self, func: CALLBACK_TYPE) -> None:
|
def add_listener(self, func: CALLBACK_TYPE) -> None:
|
||||||
"""Add a listener to the list of active listener"""
|
"""Add a listener to the list of active listener"""
|
||||||
self._active_listener.append(func)
|
self._active_listener.append(func)
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ from homeassistant.const import (
|
|||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
STATE_ON,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
@@ -99,7 +98,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
"comfort_away_temp",
|
"comfort_away_temp",
|
||||||
"power_temp",
|
"power_temp",
|
||||||
"ac_mode",
|
"ac_mode",
|
||||||
"current_max_power",
|
|
||||||
"saved_preset_mode",
|
"saved_preset_mode",
|
||||||
"saved_target_temp",
|
"saved_target_temp",
|
||||||
"saved_hvac_mode",
|
"saved_hvac_mode",
|
||||||
@@ -191,7 +189,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
|
|
||||||
self._ema_temp = None
|
self._ema_temp = None
|
||||||
self._ema_algo = None
|
self._ema_algo = None
|
||||||
self._now = None
|
|
||||||
|
|
||||||
self._attr_fan_mode = None
|
self._attr_fan_mode = None
|
||||||
|
|
||||||
@@ -446,10 +443,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# start listening for all managers
|
|
||||||
for manager in self._managers:
|
|
||||||
manager.start_listening()
|
|
||||||
|
|
||||||
self.async_on_remove(self.remove_thermostat)
|
self.async_on_remove(self.remove_thermostat)
|
||||||
|
|
||||||
# issue 428. Link to others entities will start at link
|
# issue 428. Link to others entities will start at link
|
||||||
@@ -482,6 +475,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
_LOGGER.debug("%s - Calling async_startup_internal", self)
|
||||||
need_write_state = False
|
need_write_state = False
|
||||||
|
|
||||||
|
# start listening for all managers
|
||||||
|
for manager in self._managers:
|
||||||
|
await manager.start_listening()
|
||||||
|
|
||||||
await self.get_my_previous_state()
|
await self.get_my_previous_state()
|
||||||
|
|
||||||
await self.init_presets(central_configuration)
|
await self.init_presets(central_configuration)
|
||||||
@@ -1454,7 +1451,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
control_heating to turn all off"""
|
control_heating to turn all off"""
|
||||||
|
|
||||||
for under in self._underlyings:
|
for under in self._underlyings:
|
||||||
await under.turn_off()
|
await under.turn_off_and_cancel_cycle()
|
||||||
|
|
||||||
def save_preset_mode(self):
|
def save_preset_mode(self):
|
||||||
"""Save the current preset mode to be restored later
|
"""Save the current preset mode to be restored later
|
||||||
@@ -1466,7 +1463,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
):
|
):
|
||||||
self._saved_preset_mode = self._attr_preset_mode
|
self._saved_preset_mode = self._attr_preset_mode
|
||||||
|
|
||||||
async def restore_preset_mode(self):
|
async def restore_preset_mode(self, force=False):
|
||||||
"""Restore a previous preset mode
|
"""Restore a previous preset mode
|
||||||
We never restore a hidden preset mode. Normally that is not possible
|
We never restore a hidden preset mode. Normally that is not possible
|
||||||
"""
|
"""
|
||||||
@@ -1474,7 +1471,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
self._saved_preset_mode not in HIDDEN_PRESETS
|
self._saved_preset_mode not in HIDDEN_PRESETS
|
||||||
and self._saved_preset_mode is not None
|
and self._saved_preset_mode is not None
|
||||||
):
|
):
|
||||||
await self.async_set_preset_mode_internal(self._saved_preset_mode)
|
await self.async_set_preset_mode_internal(self._saved_preset_mode, force=force)
|
||||||
|
|
||||||
def save_hvac_mode(self):
|
def save_hvac_mode(self):
|
||||||
"""Save the current hvac-mode to be restored later"""
|
"""Save the current hvac-mode to be restored later"""
|
||||||
@@ -1582,15 +1579,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _set_now(self, now: datetime):
|
|
||||||
"""Set the now timestamp. This is only for tests purpose"""
|
|
||||||
self._now = now
|
|
||||||
|
|
||||||
@property
|
|
||||||
def now(self) -> datetime:
|
|
||||||
"""Get now. The local datetime or the overloaded _set_now date"""
|
|
||||||
return self._now if self._now is not None else NowClass.get_now(self._hass)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_initialized(self) -> bool:
|
def is_initialized(self) -> bool:
|
||||||
"""Check if all underlyings are initialized
|
"""Check if all underlyings are initialized
|
||||||
@@ -1620,11 +1608,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Check overpowering condition
|
# Check overpowering condition
|
||||||
# Not necessary for switch because each switch is checking at startup
|
# Not usefull. Will be done at the next power refresh
|
||||||
overpowering = await self._power_manager.check_overpowering()
|
# await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
|
||||||
if overpowering == STATE_ON:
|
|
||||||
_LOGGER.debug("%s - End of cycle (overpowering)", self)
|
|
||||||
return True
|
|
||||||
|
|
||||||
safety: bool = await self._safety_manager.refresh_state()
|
safety: bool = await self._safety_manager.refresh_state()
|
||||||
if safety and self.is_over_climate:
|
if safety and self.is_over_climate:
|
||||||
@@ -1965,3 +1950,34 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
def is_preset_configured(self, preset) -> bool:
|
def is_preset_configured(self, preset) -> bool:
|
||||||
"""Returns True if the preset in argument is configured"""
|
"""Returns True if the preset in argument is configured"""
|
||||||
return self._presets.get(preset, None) is not None
|
return self._presets.get(preset, None) is not None
|
||||||
|
|
||||||
|
# For testing purpose
|
||||||
|
# @deprecated
|
||||||
|
def _set_now(self, now: datetime):
|
||||||
|
"""Set the now timestamp. This is only for tests purpose
|
||||||
|
This method should be replaced by the vthermAPI equivalent"""
|
||||||
|
VersatileThermostatAPI.get_vtherm_api(self._hass)._set_now(now) # pylint: disable=protected-access
|
||||||
|
|
||||||
|
# @deprecated
|
||||||
|
@property
|
||||||
|
def now(self) -> datetime:
|
||||||
|
"""Get now. The local datetime or the overloaded _set_now date
|
||||||
|
This method should be replaced by the vthermAPI equivalent"""
|
||||||
|
return VersatileThermostatAPI.get_vtherm_api(self._hass).now
|
||||||
|
|
||||||
|
@property
|
||||||
|
def power_percent(self) -> float | None:
|
||||||
|
"""Get the current on_percent as a percentage value. valid only for Vtherm with a TPI algo
|
||||||
|
Get the current on_percent value"""
|
||||||
|
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
|
||||||
|
return round(self._prop_algorithm.on_percent * 100, 0)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def on_percent(self) -> float | None:
|
||||||
|
"""Get the current on_percent value. valid only for Vtherm with a TPI algo"""
|
||||||
|
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
|
||||||
|
return self._prop_algorithm.on_percent
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
""" Implements a central Power Feature Manager for Versatile Thermostat """
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
from functools import cmp_to_key
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_OFF
|
||||||
|
from homeassistant.core import HomeAssistant, Event, callback
|
||||||
|
from homeassistant.helpers.event import (
|
||||||
|
async_track_state_change_event,
|
||||||
|
EventStateChangedData,
|
||||||
|
async_call_later,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
ClimateEntity,
|
||||||
|
DOMAIN as CLIMATE_DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
from .commons import ConfigData
|
||||||
|
from .base_manager import BaseFeatureManager
|
||||||
|
|
||||||
|
# circular dependency
|
||||||
|
# from .base_thermostat import BaseThermostat
|
||||||
|
|
||||||
|
MIN_DTEMP_SECS = 20
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CentralFeaturePowerManager(BaseFeatureManager):
|
||||||
|
"""A central Power feature manager"""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, vtherm_api: Any):
|
||||||
|
"""Init of a featureManager"""
|
||||||
|
super().__init__(None, hass, "centralPowerManager")
|
||||||
|
self._hass: HomeAssistant = hass
|
||||||
|
self._vtherm_api = vtherm_api # no type due to circular reference
|
||||||
|
self._is_configured: bool = False
|
||||||
|
self._power_sensor_entity_id: str = None
|
||||||
|
self._max_power_sensor_entity_id: str = None
|
||||||
|
self._current_power: float = None
|
||||||
|
self._current_max_power: float = None
|
||||||
|
self._power_temp: float = None
|
||||||
|
self._cancel_calculate_shedding_call = None
|
||||||
|
# Not used now
|
||||||
|
self._last_shedding_date = None
|
||||||
|
|
||||||
|
def post_init(self, entry_infos: ConfigData):
|
||||||
|
"""Gets the configuration parameters"""
|
||||||
|
central_config = self._vtherm_api.find_central_configuration()
|
||||||
|
if not central_config:
|
||||||
|
_LOGGER.info("No central configuration is found. Power management will be deactivated")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
|
||||||
|
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
||||||
|
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
|
||||||
|
|
||||||
|
self._is_configured = False
|
||||||
|
self._current_power = None
|
||||||
|
self._current_max_power = None
|
||||||
|
if (
|
||||||
|
entry_infos.get(CONF_USE_POWER_FEATURE, False)
|
||||||
|
and self._max_power_sensor_entity_id
|
||||||
|
and self._power_sensor_entity_id
|
||||||
|
and self._power_temp
|
||||||
|
):
|
||||||
|
self._is_configured = True
|
||||||
|
else:
|
||||||
|
_LOGGER.info("Power management is not fully configured and will be deactivated")
|
||||||
|
|
||||||
|
async def start_listening(self):
|
||||||
|
"""Start listening the power sensor"""
|
||||||
|
if not self._is_configured:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stop_listening()
|
||||||
|
|
||||||
|
self.add_listener(
|
||||||
|
async_track_state_change_event(
|
||||||
|
self.hass,
|
||||||
|
[self._power_sensor_entity_id],
|
||||||
|
self._power_sensor_changed,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_listener(
|
||||||
|
async_track_state_change_event(
|
||||||
|
self.hass,
|
||||||
|
[self._max_power_sensor_entity_id],
|
||||||
|
self._max_power_sensor_changed,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def _power_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||||
|
"""Handle power changes."""
|
||||||
|
_LOGGER.debug("Receive new Power event")
|
||||||
|
_LOGGER.debug(event)
|
||||||
|
await self.refresh_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
async def _max_power_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||||
|
"""Handle power max changes."""
|
||||||
|
_LOGGER.debug("Receive new Power Max event")
|
||||||
|
_LOGGER.debug(event)
|
||||||
|
await self.refresh_state()
|
||||||
|
|
||||||
|
@overrides
|
||||||
|
async def refresh_state(self) -> bool:
|
||||||
|
"""Tries to get the last state from sensor
|
||||||
|
Returns True if a change has been made"""
|
||||||
|
|
||||||
|
async def _calculate_shedding_internal(_):
|
||||||
|
_LOGGER.debug("Do the shedding calculation")
|
||||||
|
await self.calculate_shedding()
|
||||||
|
if self._cancel_calculate_shedding_call:
|
||||||
|
self._cancel_calculate_shedding_call()
|
||||||
|
self._cancel_calculate_shedding_call = None
|
||||||
|
|
||||||
|
if not self._is_configured:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Retrieve current power
|
||||||
|
new_power = get_safe_float(self._hass, self._power_sensor_entity_id)
|
||||||
|
power_changed = new_power is not None and self._current_power != new_power
|
||||||
|
if power_changed:
|
||||||
|
self._current_power = new_power
|
||||||
|
_LOGGER.debug("New current power has been retrieved: %.3f", self._current_power)
|
||||||
|
|
||||||
|
# Retrieve max power
|
||||||
|
new_max_power = get_safe_float(self._hass, self._max_power_sensor_entity_id)
|
||||||
|
max_power_changed = new_max_power is not None and self._current_max_power != new_max_power
|
||||||
|
if max_power_changed:
|
||||||
|
self._current_max_power = new_max_power
|
||||||
|
_LOGGER.debug("New current max power has been retrieved: %.3f", self._current_max_power)
|
||||||
|
|
||||||
|
# Schedule shedding calculation if there's any change
|
||||||
|
if power_changed or max_power_changed:
|
||||||
|
if not self._cancel_calculate_shedding_call:
|
||||||
|
self._cancel_calculate_shedding_call = async_call_later(self.hass, timedelta(seconds=MIN_DTEMP_SECS), _calculate_shedding_internal)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# For testing purpose only, do an immediate shedding calculation
|
||||||
|
async def _do_immediate_shedding(self):
|
||||||
|
"""Do an immmediate shedding calculation if a timer was programmed.
|
||||||
|
Else, do nothing"""
|
||||||
|
if self._cancel_calculate_shedding_call:
|
||||||
|
self._cancel_calculate_shedding_call()
|
||||||
|
self._cancel_calculate_shedding_call = None
|
||||||
|
await self.calculate_shedding()
|
||||||
|
|
||||||
|
async def calculate_shedding(self):
|
||||||
|
"""Do the shedding calculation and set/unset VTherm into overpowering state"""
|
||||||
|
if not self.is_configured or self.current_max_power is None or self.current_power is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug("-------- Start of calculate_shedding")
|
||||||
|
# Find all VTherms
|
||||||
|
available_power = self.current_max_power - self.current_power
|
||||||
|
vtherms_sorted = self.find_all_vtherm_with_power_management_sorted_by_dtemp()
|
||||||
|
|
||||||
|
# shedding only
|
||||||
|
if available_power < 0:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"The available power is is < 0 (%s). Set overpowering only for list: %s",
|
||||||
|
available_power,
|
||||||
|
vtherms_sorted,
|
||||||
|
)
|
||||||
|
# we will set overpowering for the nearest target temp first
|
||||||
|
total_power_gain = 0
|
||||||
|
|
||||||
|
for vtherm in vtherms_sorted:
|
||||||
|
if vtherm.is_device_active and not vtherm.power_manager.is_overpowering_detected:
|
||||||
|
device_power = vtherm.power_manager.device_power
|
||||||
|
total_power_gain += device_power
|
||||||
|
_LOGGER.info("vtherm %s should be in overpowering state (device_power=%.2f)", vtherm.name, device_power)
|
||||||
|
await vtherm.power_manager.set_overpowering(True, device_power)
|
||||||
|
|
||||||
|
_LOGGER.debug("after vtherm %s total_power_gain=%s, available_power=%s", vtherm.name, total_power_gain, available_power)
|
||||||
|
if total_power_gain >= -available_power:
|
||||||
|
_LOGGER.debug("We have found enough vtherm to set to overpowering")
|
||||||
|
break
|
||||||
|
# unshedding only
|
||||||
|
else:
|
||||||
|
vtherms_sorted.reverse()
|
||||||
|
_LOGGER.debug("The available power is is > 0 (%s). Do a complete shedding/un-shedding calculation for list: %s", available_power, vtherms_sorted)
|
||||||
|
|
||||||
|
total_power_added = 0
|
||||||
|
|
||||||
|
for vtherm in vtherms_sorted:
|
||||||
|
# We want to do always unshedding in order to initialize the state
|
||||||
|
# so we cannot use is_overpowering_detected which test also UNKNOWN and UNAVAILABLE
|
||||||
|
if vtherm.power_manager.overpowering_state == STATE_OFF:
|
||||||
|
continue
|
||||||
|
|
||||||
|
power_consumption_max = device_power = vtherm.power_manager.device_power
|
||||||
|
# calculate the power_consumption_max
|
||||||
|
if vtherm.on_percent is not None:
|
||||||
|
power_consumption_max = max(
|
||||||
|
device_power / vtherm.nb_underlying_entities,
|
||||||
|
device_power * vtherm.on_percent,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug("vtherm %s power_consumption_max is %s (device_power=%s, overclimate=%s)", vtherm.name, power_consumption_max, device_power, vtherm.is_over_climate)
|
||||||
|
|
||||||
|
# or not ... is for initializing the overpowering state if not already done
|
||||||
|
if total_power_added + power_consumption_max < available_power or not vtherm.power_manager.is_overpowering_detected:
|
||||||
|
# we count the unshedding only if the VTherm was in shedding
|
||||||
|
if vtherm.power_manager.is_overpowering_detected:
|
||||||
|
_LOGGER.info("vtherm %s should not be in overpowering state (power_consumption_max=%.2f)", vtherm.name, power_consumption_max)
|
||||||
|
total_power_added += power_consumption_max
|
||||||
|
|
||||||
|
await vtherm.power_manager.set_overpowering(False)
|
||||||
|
|
||||||
|
if total_power_added >= available_power:
|
||||||
|
_LOGGER.debug("We have found enough vtherm to set to non-overpowering")
|
||||||
|
break
|
||||||
|
|
||||||
|
_LOGGER.debug("after vtherm %s total_power_added=%s, available_power=%s", vtherm.name, total_power_added, available_power)
|
||||||
|
|
||||||
|
self._last_shedding_date = self._vtherm_api.now
|
||||||
|
_LOGGER.debug("-------- End of calculate_shedding")
|
||||||
|
|
||||||
|
def get_climate_components_entities(self) -> list:
|
||||||
|
"""Get all VTherms entitites"""
|
||||||
|
vtherms = []
|
||||||
|
component: EntityComponent[ClimateEntity] = self._hass.data.get(
|
||||||
|
CLIMATE_DOMAIN, None
|
||||||
|
)
|
||||||
|
if component:
|
||||||
|
for entity in component.entities:
|
||||||
|
# A little hack to test if the climate is a VTherm. Cannot use isinstance
|
||||||
|
# due to circular dependency of BaseThermostat
|
||||||
|
if (
|
||||||
|
entity.device_info
|
||||||
|
and entity.device_info.get("model", None) == DOMAIN
|
||||||
|
):
|
||||||
|
vtherms.append(entity)
|
||||||
|
return vtherms
|
||||||
|
|
||||||
|
def find_all_vtherm_with_power_management_sorted_by_dtemp(
|
||||||
|
self,
|
||||||
|
) -> list:
|
||||||
|
"""Returns all the VTherms with power management activated"""
|
||||||
|
entities = self.get_climate_components_entities()
|
||||||
|
vtherms = [
|
||||||
|
vtherm
|
||||||
|
for vtherm in entities
|
||||||
|
if vtherm.power_manager.is_configured and vtherm.is_on
|
||||||
|
]
|
||||||
|
|
||||||
|
# sort the result with the min temp difference first. A and B should be BaseThermostat class
|
||||||
|
def cmp_temps(a, b) -> int:
|
||||||
|
diff_a = float("inf")
|
||||||
|
diff_b = float("inf")
|
||||||
|
a_target = a.target_temperature if not a.power_manager.is_overpowering_detected else a.saved_target_temp
|
||||||
|
b_target = b.target_temperature if not b.power_manager.is_overpowering_detected else b.saved_target_temp
|
||||||
|
if a.current_temperature is not None and a_target is not None:
|
||||||
|
diff_a = a_target - a.current_temperature
|
||||||
|
if b.current_temperature is not None and b_target is not None:
|
||||||
|
diff_b = b_target - b.current_temperature
|
||||||
|
|
||||||
|
if diff_a == diff_b:
|
||||||
|
return 0
|
||||||
|
return 1 if diff_a > diff_b else -1
|
||||||
|
|
||||||
|
vtherms.sort(key=cmp_to_key(cmp_temps))
|
||||||
|
return vtherms
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_configured(self) -> bool:
|
||||||
|
"""True if the FeatureManager is fully configured"""
|
||||||
|
return self._is_configured
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_power(self) -> float | None:
|
||||||
|
"""Return the current power from sensor"""
|
||||||
|
return self._current_power
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_max_power(self) -> float | None:
|
||||||
|
"""Return the current power from sensor"""
|
||||||
|
return self._current_max_power
|
||||||
|
|
||||||
|
@property
|
||||||
|
def power_temperature(self) -> float | None:
|
||||||
|
"""Return the power temperature"""
|
||||||
|
return self._power_temp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def power_sensor_entity_id(self) -> float | None:
|
||||||
|
"""Return the power sensor entity id"""
|
||||||
|
return self._power_sensor_entity_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_power_sensor_entity_id(self) -> float | None:
|
||||||
|
"""Return the max power sensor entity id"""
|
||||||
|
return self._max_power_sensor_entity_id
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "CentralPowerManager"
|
||||||
@@ -28,6 +28,7 @@ from .thermostat_switch import ThermostatOverSwitch
|
|||||||
from .thermostat_climate import ThermostatOverClimate
|
from .thermostat_climate import ThermostatOverClimate
|
||||||
from .thermostat_valve import ThermostatOverValve
|
from .thermostat_valve import ThermostatOverValve
|
||||||
from .thermostat_climate_valve import ThermostatOverClimateValve
|
from .thermostat_climate_valve import ThermostatOverClimateValve
|
||||||
|
from .vtherm_api import VersatileThermostatAPI
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -51,6 +52,9 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
|
||||||
|
# Initialize the central power manager
|
||||||
|
vtherm_api = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||||
|
vtherm_api.central_power_manager.post_init(entry.data)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Instantiate the right base class
|
# Instantiate the right base class
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# pylint: disable=line-too-long
|
# pylint: disable=line-too-long
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Any, TypeVar
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
@@ -132,3 +133,20 @@ def check_and_extract_service_configuration(service_config) -> dict:
|
|||||||
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
|
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
|
||||||
)
|
)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def deprecated(message):
|
||||||
|
"""A decorator to indicate that the method/attribut is deprecated"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
warnings.warn(
|
||||||
|
f"{func.__name__} is deprecated: {message}",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|||||||
@@ -90,11 +90,10 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
CONF_USE_MOTION_FEATURE, False
|
CONF_USE_MOTION_FEATURE, False
|
||||||
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
|
) and (self._infos.get(CONF_MOTION_SENSOR) is not None or is_central_config)
|
||||||
|
|
||||||
self._infos[CONF_USE_POWER_FEATURE] = self._infos.get(
|
self._infos[CONF_USE_POWER_FEATURE] = (
|
||||||
CONF_USE_POWER_CENTRAL_CONFIG, False
|
self._infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False)
|
||||||
) or (
|
or self._infos.get(CONF_USE_POWER_FEATURE, False)
|
||||||
self._infos.get(CONF_POWER_SENSOR) is not None
|
or (is_central_config and self._infos.get(CONF_POWER_SENSOR) is not None and self._infos.get(CONF_MAX_POWER_SENSOR) is not None)
|
||||||
and self._infos.get(CONF_MAX_POWER_SENSOR) is not None
|
|
||||||
)
|
)
|
||||||
self._infos[CONF_USE_PRESENCE_FEATURE] = (
|
self._infos[CONF_USE_PRESENCE_FEATURE] = (
|
||||||
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
|
self._infos.get(CONF_USE_PRESENCE_CENTRAL_CONFIG, False)
|
||||||
@@ -184,7 +183,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
|
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# check the heater_entity_id
|
# check the entity_ids
|
||||||
for conf in [
|
for conf in [
|
||||||
CONF_UNDERLYING_LIST,
|
CONF_UNDERLYING_LIST,
|
||||||
CONF_TEMP_SENSOR,
|
CONF_TEMP_SENSOR,
|
||||||
@@ -330,14 +329,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (
|
if infos.get(CONF_USE_POWER_FEATURE, False) is True and infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False and infos.get(CONF_PRESET_POWER, None) is None:
|
||||||
infos.get(CONF_USE_POWER_FEATURE, False) is True
|
|
||||||
and infos.get(CONF_USE_POWER_CENTRAL_CONFIG, False) is False
|
|
||||||
and (
|
|
||||||
infos.get(CONF_POWER_SENSOR, None) is None
|
|
||||||
or infos.get(CONF_MAX_POWER_SENSOR, None) is None
|
|
||||||
)
|
|
||||||
):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -815,7 +807,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
|
|||||||
"""Handle the specific power flow steps"""
|
"""Handle the specific power flow steps"""
|
||||||
_LOGGER.debug("Into ConfigFlow.async_step_spec_power user_input=%s", user_input)
|
_LOGGER.debug("Into ConfigFlow.async_step_spec_power user_input=%s", user_input)
|
||||||
|
|
||||||
schema = STEP_CENTRAL_POWER_DATA_SCHEMA
|
schema = STEP_NON_CENTRAL_POWER_DATA_SCHEMA
|
||||||
|
|
||||||
self._infos[COMES_FROM] = "async_step_spec_power"
|
self._infos[COMES_FROM] = "async_step_spec_power"
|
||||||
|
|
||||||
|
|||||||
@@ -339,6 +339,12 @@ STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
STEP_NON_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
|
||||||
{
|
{
|
||||||
vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean,
|
vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean,
|
||||||
|
|||||||
@@ -503,6 +503,8 @@ def get_safe_float(hass, entity_id: str):
|
|||||||
if (
|
if (
|
||||||
entity_id is None
|
entity_id is None
|
||||||
or not (state := hass.states.get(entity_id))
|
or not (state := hass.states.get(entity_id))
|
||||||
|
or state.state is None
|
||||||
|
or state.state == "None"
|
||||||
or state.state == "unknown"
|
or state.state == "unknown"
|
||||||
or state.state == "unavailable"
|
or state.state == "unavailable"
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class FeatureAutoStartStopManager(BaseFeatureManager):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class FeatureMotionManager(BaseFeatureManager):
|
|||||||
self._motion_state = STATE_UNKNOWN
|
self._motion_state = STATE_UNKNOWN
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
if self._is_configured:
|
if self._is_configured:
|
||||||
self.stop_listening()
|
self.stop_listening()
|
||||||
|
|||||||
@@ -12,22 +12,15 @@ from homeassistant.const import (
|
|||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
callback,
|
|
||||||
Event,
|
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.event import (
|
|
||||||
async_track_state_change_event,
|
|
||||||
EventStateChangedData,
|
|
||||||
)
|
|
||||||
from homeassistant.components.climate import HVACMode
|
|
||||||
|
|
||||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
from .commons import ConfigData
|
from .commons import ConfigData
|
||||||
|
|
||||||
from .base_manager import BaseFeatureManager
|
from .base_manager import BaseFeatureManager
|
||||||
|
from .vtherm_api import VersatileThermostatAPI
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -50,194 +43,94 @@ class FeaturePowerManager(BaseFeatureManager):
|
|||||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||||
"""Init of a featureManager"""
|
"""Init of a featureManager"""
|
||||||
super().__init__(vtherm, hass)
|
super().__init__(vtherm, hass)
|
||||||
self._power_sensor_entity_id = None
|
|
||||||
self._max_power_sensor_entity_id = None
|
|
||||||
self._current_power = None
|
|
||||||
self._current_max_power = None
|
|
||||||
self._power_temp = None
|
self._power_temp = None
|
||||||
self._overpowering_state = STATE_UNAVAILABLE
|
self._overpowering_state = STATE_UNAVAILABLE
|
||||||
self._is_configured: bool = False
|
self._is_configured: bool = False
|
||||||
self._device_power: float = 0
|
self._device_power: float = 0
|
||||||
|
self._use_power_feature: bool = False
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, entry_infos: ConfigData):
|
def post_init(self, entry_infos: ConfigData):
|
||||||
"""Reinit of the manager"""
|
"""Reinit of the manager"""
|
||||||
|
|
||||||
# Power management
|
# Power management
|
||||||
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
|
|
||||||
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
|
|
||||||
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
|
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
|
||||||
|
|
||||||
self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
|
self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
|
||||||
|
self._use_power_feature = entry_infos.get(CONF_USE_POWER_FEATURE, False)
|
||||||
self._is_configured = False
|
self._is_configured = False
|
||||||
self._current_power = None
|
|
||||||
self._current_max_power = None
|
@overrides
|
||||||
if (
|
async def start_listening(self):
|
||||||
entry_infos.get(CONF_USE_POWER_FEATURE, False)
|
"""Start listening the underlying entity. There is nothing to listen"""
|
||||||
and self._max_power_sensor_entity_id
|
central_power_configuration = (
|
||||||
and self._power_sensor_entity_id
|
VersatileThermostatAPI.get_vtherm_api().central_power_manager.is_configured
|
||||||
and self._device_power
|
)
|
||||||
):
|
|
||||||
|
if self._use_power_feature and self._device_power and central_power_configuration:
|
||||||
self._is_configured = True
|
self._is_configured = True
|
||||||
self._overpowering_state = STATE_UNKNOWN
|
# Try to restore _overpowering_state from previous state
|
||||||
|
old_state = await self._vtherm.async_get_last_state()
|
||||||
|
self._overpowering_state = STATE_ON if old_state and old_state.attributes and old_state.attributes.get("overpowering_state") == STATE_ON else STATE_UNKNOWN
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("%s - Power management is not fully configured", self)
|
if self._use_power_feature:
|
||||||
|
if not central_power_configuration:
|
||||||
@overrides
|
_LOGGER.warning(
|
||||||
def start_listening(self):
|
"%s - Power management is not fully configured. You have to configure the central configuration power",
|
||||||
"""Start listening the underlying entity"""
|
self,
|
||||||
if self._is_configured:
|
)
|
||||||
self.stop_listening()
|
else:
|
||||||
else:
|
_LOGGER.warning(
|
||||||
return
|
"%s - Power management is not fully configured. You have to configure the power feature of the VTherm",
|
||||||
|
self,
|
||||||
self.add_listener(
|
)
|
||||||
async_track_state_change_event(
|
|
||||||
self.hass,
|
|
||||||
[self._power_sensor_entity_id],
|
|
||||||
self._async_power_sensor_changed,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.add_listener(
|
|
||||||
async_track_state_change_event(
|
|
||||||
self.hass,
|
|
||||||
[self._max_power_sensor_entity_id],
|
|
||||||
self._async_max_power_sensor_changed,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@overrides
|
|
||||||
async def refresh_state(self) -> bool:
|
|
||||||
"""Tries to get the last state from sensor
|
|
||||||
Returns True if a change has been made"""
|
|
||||||
ret = False
|
|
||||||
if self._is_configured:
|
|
||||||
# try to acquire current power and power max
|
|
||||||
current_power_state = self.hass.states.get(self._power_sensor_entity_id)
|
|
||||||
if current_power_state and current_power_state.state not in (
|
|
||||||
STATE_UNAVAILABLE,
|
|
||||||
STATE_UNKNOWN,
|
|
||||||
):
|
|
||||||
self._current_power = float(current_power_state.state)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"%s - Current power have been retrieved: %.3f",
|
|
||||||
self,
|
|
||||||
self._current_power,
|
|
||||||
)
|
|
||||||
ret = True
|
|
||||||
|
|
||||||
# Try to acquire power max
|
|
||||||
current_power_max_state = self.hass.states.get(
|
|
||||||
self._max_power_sensor_entity_id
|
|
||||||
)
|
|
||||||
if current_power_max_state and current_power_max_state.state not in (
|
|
||||||
STATE_UNAVAILABLE,
|
|
||||||
STATE_UNKNOWN,
|
|
||||||
):
|
|
||||||
self._current_max_power = float(current_power_max_state.state)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"%s - Current power max have been retrieved: %.3f",
|
|
||||||
self,
|
|
||||||
self._current_max_power,
|
|
||||||
)
|
|
||||||
ret = True
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@callback
|
|
||||||
async def _async_power_sensor_changed(self, event: Event[EventStateChangedData]):
|
|
||||||
"""Handle power changes."""
|
|
||||||
_LOGGER.debug("Thermostat %s - Receive new Power event", self)
|
|
||||||
_LOGGER.debug(event)
|
|
||||||
new_state = event.data.get("new_state")
|
|
||||||
old_state = event.data.get("old_state")
|
|
||||||
if (
|
|
||||||
new_state is None
|
|
||||||
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
|
||||||
or (old_state is not None and new_state.state == old_state.state)
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
current_power = float(new_state.state)
|
|
||||||
if math.isnan(current_power) or math.isinf(current_power):
|
|
||||||
raise ValueError(f"Sensor has illegal state {new_state.state}")
|
|
||||||
self._current_power = current_power
|
|
||||||
|
|
||||||
if self._vtherm.preset_mode == PRESET_POWER:
|
|
||||||
await self._vtherm.async_control_heating()
|
|
||||||
|
|
||||||
except ValueError as ex:
|
|
||||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
async def _async_max_power_sensor_changed(
|
|
||||||
self, event: Event[EventStateChangedData]
|
|
||||||
):
|
|
||||||
"""Handle power max changes."""
|
|
||||||
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
|
|
||||||
_LOGGER.debug(event)
|
|
||||||
new_state = event.data.get("new_state")
|
|
||||||
old_state = event.data.get("old_state")
|
|
||||||
if (
|
|
||||||
new_state is None
|
|
||||||
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
|
||||||
or (old_state is not None and new_state.state == old_state.state)
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
current_power_max = float(new_state.state)
|
|
||||||
if math.isnan(current_power_max) or math.isinf(current_power_max):
|
|
||||||
raise ValueError(f"Sensor has illegal state {new_state.state}")
|
|
||||||
self._current_max_power = current_power_max
|
|
||||||
if self._vtherm.preset_mode == PRESET_POWER:
|
|
||||||
await self._vtherm.async_control_heating()
|
|
||||||
|
|
||||||
except ValueError as ex:
|
|
||||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
|
||||||
|
|
||||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||||
"""Add some custom attributes"""
|
"""Add some custom attributes"""
|
||||||
|
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||||
extra_state_attributes.update(
|
extra_state_attributes.update(
|
||||||
{
|
{
|
||||||
"power_sensor_entity_id": self._power_sensor_entity_id,
|
"power_sensor_entity_id": vtherm_api.central_power_manager.power_sensor_entity_id,
|
||||||
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
|
"max_power_sensor_entity_id": vtherm_api.central_power_manager.max_power_sensor_entity_id,
|
||||||
"overpowering_state": self._overpowering_state,
|
"overpowering_state": self._overpowering_state,
|
||||||
"is_power_configured": self._is_configured,
|
"is_power_configured": self._is_configured,
|
||||||
"device_power": self._device_power,
|
"device_power": self._device_power,
|
||||||
"power_temp": self._power_temp,
|
"power_temp": self._power_temp,
|
||||||
"current_power": self._current_power,
|
"current_power": vtherm_api.central_power_manager.current_power,
|
||||||
"current_max_power": self._current_max_power,
|
"current_max_power": vtherm_api.central_power_manager.current_max_power,
|
||||||
"mean_cycle_power": self.mean_cycle_power,
|
"mean_cycle_power": self.mean_cycle_power,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def check_overpowering(self) -> bool:
|
async def check_power_available(self) -> bool:
|
||||||
"""Check the overpowering condition
|
"""Check if the Vtherm can be started considering overpowering.
|
||||||
Turn the preset_mode of the heater to 'power' if power conditions are exceeded
|
Returns True if no overpowering conditions are found
|
||||||
Returns True if overpowering is 'on'
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._is_configured:
|
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||||
return False
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self._current_power is None
|
not self._is_configured
|
||||||
|
or not vtherm_api.central_power_manager.is_configured
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
current_power = vtherm_api.central_power_manager.current_power
|
||||||
|
current_max_power = vtherm_api.central_power_manager.current_max_power
|
||||||
|
if (
|
||||||
|
current_power is None
|
||||||
|
or current_max_power is None
|
||||||
or self._device_power is None
|
or self._device_power is None
|
||||||
or self._current_max_power is None
|
|
||||||
):
|
):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"%s - power not valued. check_overpowering not available", self
|
"%s - power not valued. check_power_available not available", self
|
||||||
)
|
)
|
||||||
return False
|
return True
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
|
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
|
||||||
self,
|
self,
|
||||||
self._current_power,
|
current_power,
|
||||||
self._current_max_power,
|
current_max_power,
|
||||||
self._device_power,
|
self._device_power,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -253,62 +146,78 @@ class FeaturePowerManager(BaseFeatureManager):
|
|||||||
self._device_power * self._vtherm.proportional_algorithm.on_percent,
|
self._device_power * self._vtherm.proportional_algorithm.on_percent,
|
||||||
)
|
)
|
||||||
|
|
||||||
ret = (self._current_power + power_consumption_max) >= self._current_max_power
|
ret = (current_power + power_consumption_max) < current_max_power
|
||||||
if (
|
if not ret:
|
||||||
self._overpowering_state == STATE_OFF
|
_LOGGER.info(
|
||||||
and ret
|
"%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f",
|
||||||
and self._vtherm.hvac_mode != HVACMode.OFF
|
self,
|
||||||
):
|
current_power,
|
||||||
|
current_max_power,
|
||||||
|
self._device_power,
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
async def set_overpowering(self, overpowering: bool, power_consumption_max=0):
|
||||||
|
"""Force the overpowering state for the VTherm"""
|
||||||
|
|
||||||
|
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||||
|
current_power = vtherm_api.central_power_manager.current_power
|
||||||
|
current_max_power = vtherm_api.central_power_manager.current_max_power
|
||||||
|
|
||||||
|
if overpowering and not self.is_overpowering_detected:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
||||||
self,
|
self,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._overpowering_state = STATE_ON
|
||||||
|
|
||||||
if self._vtherm.is_over_climate:
|
if self._vtherm.is_over_climate:
|
||||||
self._vtherm.save_hvac_mode()
|
self._vtherm.save_hvac_mode()
|
||||||
|
|
||||||
self._vtherm.save_preset_mode()
|
self._vtherm.save_preset_mode()
|
||||||
await self._vtherm.async_underlying_entity_turn_off()
|
await self._vtherm.async_underlying_entity_turn_off()
|
||||||
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER)
|
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER, force=True)
|
||||||
self._vtherm.send_event(
|
self._vtherm.send_event(
|
||||||
EventType.POWER_EVENT,
|
EventType.POWER_EVENT,
|
||||||
{
|
{
|
||||||
"type": "start",
|
"type": "start",
|
||||||
"current_power": self._current_power,
|
"current_power": current_power,
|
||||||
"device_power": self._device_power,
|
"device_power": self._device_power,
|
||||||
"current_max_power": self._current_max_power,
|
"current_max_power": current_max_power,
|
||||||
"current_power_consumption": power_consumption_max,
|
"current_power_consumption": power_consumption_max,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
elif not overpowering and self.is_overpowering_detected:
|
||||||
# Check if we need to remove the POWER preset
|
|
||||||
if (
|
|
||||||
self._overpowering_state == STATE_ON
|
|
||||||
and not ret
|
|
||||||
and self._vtherm.preset_mode == PRESET_POWER
|
|
||||||
):
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"%s - end of overpowering is detected. Heater preset will be restored to '%s'",
|
"%s - end of overpowering is detected. Heater preset will be restored to '%s'",
|
||||||
self,
|
self,
|
||||||
self._vtherm._saved_preset_mode, # pylint: disable=protected-access
|
self._vtherm._saved_preset_mode, # pylint: disable=protected-access
|
||||||
)
|
)
|
||||||
|
self._overpowering_state = STATE_OFF
|
||||||
|
|
||||||
|
# restore state
|
||||||
if self._vtherm.is_over_climate:
|
if self._vtherm.is_over_climate:
|
||||||
await self._vtherm.restore_hvac_mode(False)
|
await self._vtherm.restore_hvac_mode()
|
||||||
await self._vtherm.restore_preset_mode()
|
await self._vtherm.restore_preset_mode()
|
||||||
|
# restart cycle
|
||||||
|
await self._vtherm.async_control_heating(force=True)
|
||||||
self._vtherm.send_event(
|
self._vtherm.send_event(
|
||||||
EventType.POWER_EVENT,
|
EventType.POWER_EVENT,
|
||||||
{
|
{
|
||||||
"type": "end",
|
"type": "end",
|
||||||
"current_power": self._current_power,
|
"current_power": current_power,
|
||||||
"device_power": self._device_power,
|
"device_power": self._device_power,
|
||||||
"current_max_power": self._current_max_power,
|
"current_max_power": current_max_power,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
elif not overpowering and self._overpowering_state != STATE_OFF:
|
||||||
new_overpowering_state = STATE_ON if ret else STATE_OFF
|
# just set to not overpowering the state which was not set
|
||||||
if self._overpowering_state != new_overpowering_state:
|
self._overpowering_state = STATE_OFF
|
||||||
self._overpowering_state = new_overpowering_state
|
else:
|
||||||
self._vtherm.update_custom_attributes()
|
# Nothing to do (already in the right state)
|
||||||
|
return
|
||||||
return self._overpowering_state == STATE_ON
|
self._vtherm.update_custom_attributes()
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
@property
|
@property
|
||||||
@@ -325,14 +234,9 @@ class FeaturePowerManager(BaseFeatureManager):
|
|||||||
return self._overpowering_state
|
return self._overpowering_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_power_sensor_entity_id(self) -> bool:
|
def is_overpowering_detected(self) -> str | None:
|
||||||
"""Return the power max entity id"""
|
"""Return True if the Vtherm is in overpowering state"""
|
||||||
return self._max_power_sensor_entity_id
|
return self._overpowering_state == STATE_ON
|
||||||
|
|
||||||
@property
|
|
||||||
def power_sensor_entity_id(self) -> bool:
|
|
||||||
"""Return the power entity id"""
|
|
||||||
return self._power_sensor_entity_id
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def power_temperature(self) -> bool:
|
def power_temperature(self) -> bool:
|
||||||
@@ -344,16 +248,6 @@ class FeaturePowerManager(BaseFeatureManager):
|
|||||||
"""Return the device power"""
|
"""Return the device power"""
|
||||||
return self._device_power
|
return self._device_power
|
||||||
|
|
||||||
@property
|
|
||||||
def current_power(self) -> bool:
|
|
||||||
"""Return the current power from sensor"""
|
|
||||||
return self._current_power
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_max_power(self) -> bool:
|
|
||||||
"""Return the current power from sensor"""
|
|
||||||
return self._current_max_power
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mean_cycle_power(self) -> float | None:
|
def mean_cycle_power(self) -> float | None:
|
||||||
"""Returns the mean power consumption during the cycle"""
|
"""Returns the mean power consumption during the cycle"""
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class FeaturePresenceManager(BaseFeatureManager):
|
|||||||
self._presence_state = STATE_UNKNOWN
|
self._presence_state = STATE_UNKNOWN
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
if self._is_configured:
|
if self._is_configured:
|
||||||
self.stop_listening()
|
self.stop_listening()
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class FeatureSafetyManager(BaseFeatureManager):
|
|||||||
self._is_configured = True
|
self._is_configured = True
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class FeatureWindowManager(BaseFeatureManager):
|
|||||||
self._window_state = STATE_UNKNOWN
|
self._window_state = STATE_UNKNOWN
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
if self._is_configured:
|
if self._is_configured:
|
||||||
self.stop_listening()
|
self.stop_listening()
|
||||||
|
|||||||
@@ -263,14 +263,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
|||||||
"""True if the Thermostat is regulated by valve"""
|
"""True if the Thermostat is regulated by valve"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def power_percent(self) -> float | None:
|
|
||||||
"""Get the current on_percent value"""
|
|
||||||
if self._prop_algorithm:
|
|
||||||
return round(self._prop_algorithm.on_percent * 100, 0)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# @property
|
# @property
|
||||||
# def hvac_modes(self) -> list[HVACMode]:
|
# def hvac_modes(self) -> list[HVACMode]:
|
||||||
# """Get the hvac_modes"""
|
# """Get the hvac_modes"""
|
||||||
|
|||||||
@@ -26,23 +26,21 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||||
|
|
||||||
_entity_component_unrecorded_attributes = (
|
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
frozenset(
|
||||||
frozenset(
|
{
|
||||||
{
|
"is_over_switch",
|
||||||
"is_over_switch",
|
"is_inversed",
|
||||||
"is_inversed",
|
"underlying_entities",
|
||||||
"underlying_entities",
|
"on_time_sec",
|
||||||
"on_time_sec",
|
"off_time_sec",
|
||||||
"off_time_sec",
|
"cycle_min",
|
||||||
"cycle_min",
|
"function",
|
||||||
"function",
|
"tpi_coef_int",
|
||||||
"tpi_coef_int",
|
"tpi_coef_ext",
|
||||||
"tpi_coef_ext",
|
"power_percent",
|
||||||
"power_percent",
|
"calculated_on_percent",
|
||||||
"calculated_on_percent",
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,14 +59,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
|||||||
"""True if the switch is inversed (for pilot wire and diode)"""
|
"""True if the switch is inversed (for pilot wire and diode)"""
|
||||||
return self._is_inversed is True
|
return self._is_inversed is True
|
||||||
|
|
||||||
@property
|
|
||||||
def power_percent(self) -> float | None:
|
|
||||||
"""Get the current on_percent value"""
|
|
||||||
if self._prop_algorithm:
|
|
||||||
return round(self._prop_algorithm.on_percent * 100, 0)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, config_entry: ConfigData):
|
def post_init(self, config_entry: ConfigData):
|
||||||
"""Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ class UnderlyingEntity:
|
|||||||
"""capping of the value send to the underlying eqt"""
|
"""capping of the value send to the underlying eqt"""
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
async def turn_off_and_cancel_cycle(self):
|
||||||
|
"""Turn off and cancel eventual running cycle"""
|
||||||
|
self._cancel_cycle()
|
||||||
|
await self.turn_off()
|
||||||
|
|
||||||
|
|
||||||
class UnderlyingSwitch(UnderlyingEntity):
|
class UnderlyingSwitch(UnderlyingEntity):
|
||||||
"""Represent a underlying switch"""
|
"""Represent a underlying switch"""
|
||||||
@@ -409,9 +414,10 @@ class UnderlyingSwitch(UnderlyingEntity):
|
|||||||
await self.turn_off()
|
await self.turn_off()
|
||||||
return
|
return
|
||||||
|
|
||||||
if await self._thermostat.power_manager.check_overpowering():
|
# if await self._thermostat.power_manager.check_overpowering():
|
||||||
_LOGGER.debug("%s - End of cycle (3)", self)
|
# _LOGGER.debug("%s - End of cycle (3)", self)
|
||||||
return
|
# return
|
||||||
|
|
||||||
# safety mode could have change the on_time percent
|
# safety mode could have change the on_time percent
|
||||||
await self._thermostat.safety_manager.refresh_state()
|
await self._thermostat.safety_manager.refresh_state()
|
||||||
time = self._on_time_sec
|
time = self._on_time_sec
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
""" The API of Versatile Thermostat"""
|
""" The API of Versatile Thermostat"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
||||||
@@ -16,8 +17,11 @@ from .const import (
|
|||||||
CONF_THERMOSTAT_TYPE,
|
CONF_THERMOSTAT_TYPE,
|
||||||
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
CONF_THERMOSTAT_CENTRAL_CONFIG,
|
||||||
CONF_MAX_ON_PERCENT,
|
CONF_MAX_ON_PERCENT,
|
||||||
|
NowClass,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .central_feature_power_manager import CentralFeaturePowerManager
|
||||||
|
|
||||||
VTHERM_API_NAME = "vtherm_api"
|
VTHERM_API_NAME = "vtherm_api"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -62,6 +66,12 @@ class VersatileThermostatAPI(dict):
|
|||||||
# A dict that will store all Number entities which holds the temperature
|
# A dict that will store all Number entities which holds the temperature
|
||||||
self._number_temperatures = dict()
|
self._number_temperatures = dict()
|
||||||
self._max_on_percent = None
|
self._max_on_percent = None
|
||||||
|
self._central_power_manager = CentralFeaturePowerManager(
|
||||||
|
VersatileThermostatAPI._hass, self
|
||||||
|
)
|
||||||
|
|
||||||
|
# the current time (for testing purpose)
|
||||||
|
self._now = None
|
||||||
|
|
||||||
def find_central_configuration(self):
|
def find_central_configuration(self):
|
||||||
"""Search for a central configuration"""
|
"""Search for a central configuration"""
|
||||||
@@ -176,6 +186,10 @@ class VersatileThermostatAPI(dict):
|
|||||||
if entry_id is None or entry_id == entity.unique_id:
|
if entry_id is None or entry_id == entity.unique_id:
|
||||||
await entity.async_startup(self.find_central_configuration())
|
await entity.async_startup(self.find_central_configuration())
|
||||||
|
|
||||||
|
# start listening for the central power manager if not only one vtherm reload
|
||||||
|
if not entry_id:
|
||||||
|
await self.central_power_manager.start_listening()
|
||||||
|
|
||||||
async def init_vtherm_preset_with_central(self):
|
async def init_vtherm_preset_with_central(self):
|
||||||
"""Init all VTherm presets when the VTherm uses central temperature"""
|
"""Init all VTherm presets when the VTherm uses central temperature"""
|
||||||
# Initialization of all preset for all VTherm
|
# Initialization of all preset for all VTherm
|
||||||
@@ -289,3 +303,18 @@ class VersatileThermostatAPI(dict):
|
|||||||
def hass(self):
|
def hass(self):
|
||||||
"""Get the HomeAssistant object"""
|
"""Get the HomeAssistant object"""
|
||||||
return VersatileThermostatAPI._hass
|
return VersatileThermostatAPI._hass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def central_power_manager(self) -> any:
|
||||||
|
"""Returns the central power manager"""
|
||||||
|
return self._central_power_manager
|
||||||
|
|
||||||
|
# For testing purpose
|
||||||
|
def _set_now(self, now: datetime):
|
||||||
|
"""Set the now timestamp. This is only for tests purpose"""
|
||||||
|
self._now = now
|
||||||
|
|
||||||
|
@property
|
||||||
|
def now(self) -> datetime:
|
||||||
|
"""Get now. The local datetime or the overloaded _set_now date"""
|
||||||
|
return self._now if self._now is not None else NowClass.get_now(self._hass)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ When your device is controlled by a `climate` entity in Home Assistant and you o
|
|||||||
|
|
||||||
This type also includes advanced self-regulation features to adjust the setpoint sent to the underlying device, helping to achieve the target temperature faster and mitigating poor internal regulation. For example, if the device's internal thermometer is too close to the heating element, it may incorrectly assume the room is warm while the setpoint is far from being achieved in other areas.
|
This type also includes advanced self-regulation features to adjust the setpoint sent to the underlying device, helping to achieve the target temperature faster and mitigating poor internal regulation. For example, if the device's internal thermometer is too close to the heating element, it may incorrectly assume the room is warm while the setpoint is far from being achieved in other areas.
|
||||||
|
|
||||||
Since version 6.8, this VTherm type can also regulate directly by controlling the valve. Ideal for controllable TRVs, this type is recommended if you have such devices.
|
Since version 6.8, this VTherm type can also regulate directly by controlling the valve. Ideal for controllable TRVs, as Sonoff TRVZB, this type is recommended if you have such devices.
|
||||||
|
|
||||||
The underlying entities for this VTherm type are exclusively `climate`.
|
The underlying entities for this VTherm type are exclusively `climate`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,50 @@
|
|||||||
# Power Management - Load Shedding
|
# Power Management - Load Shedding
|
||||||
|
|
||||||
- [Power Management - Load Shedding](#power-management---load-shedding)
|
- [Power Management - Load Shedding](#power-management---load-shedding)
|
||||||
- [Configure Power Management](#configure-power-management)
|
- [Example Use Case:](#example-use-case)
|
||||||
|
- [Configuring Power Management](#configuring-power-management)
|
||||||
|
|
||||||
This feature allows you to regulate the electricity consumption of your heaters. Known as load shedding, this feature enables you to limit the electrical consumption of your heating device if overcapacity conditions are detected.
|
This feature allows you to regulate the electrical consumption of your heaters. Known as load shedding, it lets you limit the electrical consumption of your heating equipment if overconsumption conditions are detected.
|
||||||
You will need a **sensor for the total instantaneous power consumption** of your home, as well as a **sensor for the maximum allowed power**.
|
You will need a **sensor for the total instantaneous power consumption** of your home and a **sensor for the maximum allowed power**.
|
||||||
|
|
||||||
The behavior of this feature is basic:
|
The behavior of this feature is as follows:
|
||||||
1. when the _VTherm_ is about to turn on a device,
|
1. When a new measurement of the home's power consumption or the maximum allowed power is received,
|
||||||
2. it compares the last known value of the power consumption sensor with the last value of the maximum allowed power. If there is a remaining margin greater than or equal to the declared power of the _VTherm_'s devices, then the _VTherm_ and its devices will be turned on. Otherwise, they will remain off until the next cycle.
|
2. If the maximum power is exceeded, the central command will shed the load of all active devices starting with those closest to the setpoint. This continues until enough _VTherms_ are shed,
|
||||||
|
3. If there is available power reserve and some _VTherms_ are shed, the central command will re-enable as many devices as possible, starting with those furthest from the setpoint (at the time they were shed).
|
||||||
|
|
||||||
WARNING: This very basic operation **is not a safety function** but more of an optimization feature to manage consumption at the cost of heating performance. Overloads may occur depending on the frequency of updates from your consumption sensors, and the actual power used by your devices. Therefore, you must always maintain a safety margin.
|
**WARNING:** This is **not a safety feature** but an optimization function to manage consumption at the expense of some heating degradation. Overconsumption is still possible depending on the frequency of your consumption sensor updates and the actual power used by your equipment. Always maintain a safety margin.
|
||||||
|
|
||||||
Typical use case:
|
### Example Use Case:
|
||||||
1. you have an electricity meter limited to 11 kW,
|
1. You have an electric meter limited to 11 kW,
|
||||||
2. you occasionally charge an electric vehicle at 5 kW,
|
2. You occasionally charge an electric vehicle at 5 kW,
|
||||||
3. that leaves 6 kW for everything else, including heating,
|
3. This leaves 6 kW for everything else, including heating,
|
||||||
4. you have 1 kW of other equipment running,
|
4. You have 1 kW of other active devices,
|
||||||
5. you have declared a sensor (`input_number`) for the maximum allowed power at 9 kW (= 11 kW - the reserve for other devices - margin)
|
5. You declare a sensor (`input_number`) for the maximum allowed power at 9 kW (= 11 kW - reserved power for other devices - safety margin).
|
||||||
|
|
||||||
If the vehicle is charging, the total power consumed is 6 kW (5+1), and a _VTherm_ will only turn on if its declared power is 3 kW max (9 kW - 6 kW).
|
If the vehicle is charging, the total consumed power is 6 kW (5 + 1), and a _VTherm_ will only turn on if its declared power is a maximum of 3 kW (9 kW - 6 kW).
|
||||||
If the vehicle is charging and another _VTherm_ of 2 kW is running, the total power consumed is 8 kW (5+1+2), and a _VTherm_ will only turn on if its declared power is 1 kW max (9 kW - 8 kW). Otherwise, it will wait until the next cycle.
|
If the vehicle is charging and another _VTherm_ of 2 kW is on, the total consumed power is 8 kW (5 + 1 + 2), and a _VTherm_ will only turn on if its declared power is a maximum of 1 kW (9 kW - 8 kW). Otherwise, it will skip its turn (cycle).
|
||||||
|
If the vehicle is not charging, the total consumed power is 1 kW, and a _VTherm_ will only turn on if its declared power is a maximum of 8 kW (9 kW - 1 kW).
|
||||||
|
|
||||||
If the vehicle is not charging, the total power consumed is 1 kW, and a _VTherm_ will only turn on if its declared power is 8 kW max (9 kW - 1 kW).
|
## Configuring Power Management
|
||||||
|
|
||||||
## Configure Power Management
|
In the centralized configuration, if you have selected the `With power detection` feature, configure it as follows:
|
||||||
|
|
||||||
If you have chosen the `With power detection` feature, configure it as follows:
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
1. the entity ID of the **instantaneous power consumption sensor** for your home,
|
1. The entity ID of the **sensor for total instantaneous power consumption** of your home,
|
||||||
2. the entity ID of the **maximum allowed power sensor**,
|
2. The entity ID of the **sensor for maximum allowed power**,
|
||||||
3. the temperature to apply if load shedding is activated.
|
3. The temperature to apply if load shedding is activated.
|
||||||
|
|
||||||
Note that all power values must have the same units (kW or W, for example).
|
Ensure that all power values use the same units (e.g., kW or W).
|
||||||
Having a **maximum allowed power sensor** allows you to adjust the maximum power over time using a scheduler or automation.
|
Having a **sensor for maximum allowed power** allows you to modify the maximum power dynamically using a scheduler or automation.
|
||||||
|
|
||||||
|
Note that due to centralized load-shedding, it is not possible to override the consumption and maximum consumption sensors on individual _VTherms_. This configuration must be done in the centralized settings. See [Centralized Configuration](./creation.md#centralized-configuration).
|
||||||
|
|
||||||
>  _*Notes*_
|
>  _*Notes*_
|
||||||
>
|
>
|
||||||
> 1. In case of load shedding, the radiator is set to the preset named `power`. This is a hidden preset, and you cannot select it manually.
|
> 1. During load shedding, the heater is set to the preset named `power`. This is a hidden preset that cannot be manually selected.
|
||||||
> 2. Always keep a margin, as the maximum power may briefly be exceeded while waiting for the next cycle calculation, or due to unregulated equipment.
|
> 2. Always maintain a margin, as the maximum power can briefly be exceeded while waiting for the next cycle's calculation or due to uncontrolled devices.
|
||||||
> 3. If you don't want to use this feature, uncheck it in the 'Functions' menu.
|
> 3. If you do not wish to use this feature, uncheck it in the 'Features' menu.
|
||||||
> 4. If a _VTherm_ controls multiple devices, the **electrical consumption of your heating** must match the sum of the powers.
|
> 4. If a single _VTherm_ controls multiple devices, the **declared heating power consumption** should correspond to the total power of all devices.
|
||||||
> 5. If you are using the Versatile Thermostat UI card (see [here](additions.md#much-better-with-the-versatile-thermostat-ui-card)), load shedding is represented as follows: .
|
> 5. If you use the Versatile Thermostat UI card (see [here](additions.md#better-with-the-versatile-thermostat-ui-card)), load shedding is represented as follows: .
|
||||||
|
> 6. There may be a delay of up to 20 seconds between receiving a new value from the power consumption sensor and triggering load shedding for _VTherms_. This delay prevents overloading Home Assistant if your consumption updates are very frequent.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
## Configure Pre-configured Temperatures
|
## Configure Pre-configured Temperatures
|
||||||
|
|
||||||
The preset mode allows you to pre-configure the target temperature. Used in conjunction with Scheduler (see [scheduler](additions#the-scheduler-component)), you'll have a powerful and simple way to optimize the temperature relative to the electricity consumption in your home. The managed presets are as follows:
|
The preset mode allows you to pre-configure the target temperature. Used in conjunction with Scheduler (see [scheduler](additions.md#the-scheduler-component)), you'll have a powerful and simple way to optimize the temperature relative to the electricity consumption in your home. The managed presets are as follows:
|
||||||
- **Eco**: the device is in energy-saving mode
|
- **Eco**: the device is in energy-saving mode
|
||||||
- **Comfort**: the device is in comfort mode
|
- **Comfort**: the device is in comfort mode
|
||||||
- **Boost**: the device fully opens all valves
|
- **Boost**: the device fully opens all valves
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
> * **Release 7.1**:
|
||||||
|
> - Redesign of the load-shedding function (power management). Load-shedding is now handled centrally (previously, each _VTherm_ was autonomous). This allows for much more efficient management and prioritization of load-shedding on devices that are close to the setpoint. Note that you must have a centralized configuration with power management enabled for this to work. More info [here](./feature-power.md).
|
||||||
|
|
||||||
> * **Release 6.8**:
|
> * **Release 6.8**:
|
||||||
> - Added a new regulation method for `over_climate` type Versatile Thermostats. This method, called 'Direct Valve Control', allows direct control of a TRV valve and possibly an offset to calibrate the internal thermometer of your TRV. This new method has been tested with Sonoff TRVZB and extended to other TRV types where the valve can be directly controlled via `number` entities. More information [here](over-climate.md#lauto-régulation) and [here](self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne).
|
> - Added a new regulation method for `over_climate` type Versatile Thermostats. This method, called 'Direct Valve Control', allows direct control of a TRV valve and possibly an offset to calibrate the internal thermometer of your TRV. This new method has been tested with Sonoff TRVZB and extended to other TRV types where the valve can be directly controlled via `number` entities. More information [here](over-climate.md#lauto-régulation) and [here](self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne).
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ The opening rate calculation algorithm is based on the _TPI_ algorithm described
|
|||||||
|
|
||||||
If a valve closure rate entity is configured, it will be set to 100 minus the opening rate to force the valve into a particular state.
|
If a valve closure rate entity is configured, it will be set to 100 minus the opening rate to force the valve into a particular state.
|
||||||
|
|
||||||
|
Note: for Sonoff TRVZB you should not configure the "closing degree" parameter. This leads to a bug in the TRV and the `hvac_action` is no more working.
|
||||||
|
|
||||||
### Other self-regulation
|
### Other self-regulation
|
||||||
|
|
||||||
In the second case, Versatile Thermostat calculates an offset based on the following information:
|
In the second case, Versatile Thermostat calculates an offset based on the following information:
|
||||||
|
|||||||
@@ -6,11 +6,12 @@
|
|||||||
Cette fonction vous permet de réguler la consommation électrique de vos radiateurs. Connue sous le nom de délestage, cette fonction vous permet de limiter la consommation électrique de votre appareil de chauffage si des conditions de surpuissance sont détectées.
|
Cette fonction vous permet de réguler la consommation électrique de vos radiateurs. Connue sous le nom de délestage, cette fonction vous permet de limiter la consommation électrique de votre appareil de chauffage si des conditions de surpuissance sont détectées.
|
||||||
Vous aurez besoin d'un **capteur de la puissance totale instantanée consommée** de votre logement ainsi que d'un **capteur donnant la puissance maximale autorisée**.
|
Vous aurez besoin d'un **capteur de la puissance totale instantanée consommée** de votre logement ainsi que d'un **capteur donnant la puissance maximale autorisée**.
|
||||||
|
|
||||||
Le comportement de cette fonction est basique :
|
Le comportement de cette fonction est le suivant :
|
||||||
1. lorsque le _VTherm_ va allumer un équipement,
|
1. lorsqu'une nouvelle mesure de la puissance consommée du logement ou de la puissance maximale autorisée est reçue,
|
||||||
2. il compare la dernière valeur connue du capteur de puissance consommée avec la dernière valeur de la puissance maximale autorisée. Si il reste une marge supérieure égale à la puissance déclarée des équipements du _VTherm_ alors le VTherm et ses équipements seront allumés. Sinon ils resteront éteints jusqu'au prochain cycle.
|
2. si la puissance max est dépassée, la commande centrale va mettre en délestage tous les équipements actifs en commençant par ceux qui sont le plus près de la consigne. Il fait ça jusqu'à ce que suffisament de _VTherm_ soient délestés,
|
||||||
|
3. si une réserve de puissance est disponible et que des _VTherms_ sont délestés, alors la commande centrale va délester autant d'équipements que possible en commençant par les plus loin de la consigne (au moment où il a été mis en délestage),
|
||||||
|
|
||||||
ATTENTION: ce fonctionnement très basique **n'est pas une fonction de sécurité** mais plus une fonction permettant une optimisation de la consommation au prix d'une dégradation du chauffage. Des dépassements sont possibles selon la fréquence de remontée de vos capteurs de consommation, la puissance réellement utilisée par votre équipements. Vous devez donc toujours garder une marge de sécurité.
|
ATTENTION: ce fonctionnement **n'est pas une fonction de sécurité** mais plus une fonction permettant une optimisation de la consommation au prix d'une dégradation du chauffage. Des dépassements sont possibles selon la fréquence de remontée de vos capteurs de consommation, la puissance réellement utilisée par votre équipements. Vous devez donc toujours garder une marge de sécurité.
|
||||||
|
|
||||||
Cas d'usage type:
|
Cas d'usage type:
|
||||||
1. vous avez un compteur électrique limité à 11 kW,
|
1. vous avez un compteur électrique limité à 11 kW,
|
||||||
@@ -26,7 +27,7 @@ Si le vehicle n'est pas en charge, la puissance totale consommé est de 1 kW, un
|
|||||||
|
|
||||||
## Configurer la gestion de la puissance
|
## Configurer la gestion de la puissance
|
||||||
|
|
||||||
Si vous avez choisi la fonctionnalité `Avec détection de la puissance`, vous la configurez de la façon suivante :
|
Dans la configuration centralisée, si vous avez choisi la fonctionnalité `Avec détection de la puissance`, vous la configurez de la façon suivante :
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -37,10 +38,13 @@ Si vous avez choisi la fonctionnalité `Avec détection de la puissance`, vous l
|
|||||||
Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW ou W par exemple).
|
Notez que toutes les valeurs de puissance doivent avoir les mêmes unités (kW ou W par exemple).
|
||||||
Le fait d'avoir un **capteur de puissance maximale autorisée**, vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou d'une automatisation.
|
Le fait d'avoir un **capteur de puissance maximale autorisée**, vous permet de modifier la puissance maximale au fil du temps à l'aide d'un planificateur ou d'une automatisation.
|
||||||
|
|
||||||
|
A noter, dû à la centralisation du délestage, il n'est pas possible de sur-charger les capteurs de consommation et de consommation maximale sur les _VTherms_. Cette configuration se fait forcément dans la configuration centralisée. Cf. [Configuration centralisée](./creation.md#configuration-centralisée)
|
||||||
|
|
||||||
>  _*Notes*_
|
>  _*Notes*_
|
||||||
>
|
>
|
||||||
> 1. En cas de délestage, le radiateur est réglé sur le préréglage nommé `power`. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement.
|
> 1. En cas de délestage, le radiateur est réglé sur le préréglage nommé `power`. Il s'agit d'un préréglage caché, vous ne pouvez pas le sélectionner manuellement.
|
||||||
> 2. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
|
> 2. Gardez toujours une marge, car la puissance max peut être brièvement dépassée en attendant le calcul du prochain cycle typiquement ou par des équipements non régulés.
|
||||||
> 3. Si vous ne souhaitez pas utiliser cette fonctionnalité, décochez la dans le menu 'Fonctions'.
|
> 3. Si vous ne souhaitez pas utiliser cette fonctionnalité, décochez la dans le menu 'Fonctions'.
|
||||||
> 4. Si une _VTherm_ controlez plusieurs équipements, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
|
> 4. Si une _VTherm_ controlez plusieurs équipements, la **consommation électrique de votre chauffage** renseigné doit correspondre à la somme des puissances.
|
||||||
> 5. Si vous utilisez la carte Verstatile Thermostat UI (cf. [ici](additions.md#bien-mieux-avec-le-versatile-thermostat-ui-card)), le délestage est représenté comme suit : .
|
> 5. Si vous utilisez la carte Verstatile Thermostat UI (cf. [ici](additions.md#bien-mieux-avec-le-versatile-thermostat-ui-card)), le délestage est représenté comme suit : ,
|
||||||
|
> 6. Un délai pouvant aller jusqu'à 20 sec est possible entre la réception d'une nouvelle valeur du capteur de puissance consommée et la mise en délestage de _VTherm_. Ce délai évite de trop solliciter Home Assistant si vous avez des remontées rapides de votre puissance consommée.
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Configurer les températures préréglées
|
## Configurer les températures préréglées
|
||||||
|
|
||||||
Le mode préréglé (preset) vous permet de préconfigurer la température ciblée. Utilisé en conjonction avec Scheduler (voir [scheduler](additions#composant-scheduler-)) vous aurez un moyen puissant et simple d'optimiser la température par rapport à la consommation électrique de votre maison. Les préréglages gérés sont les suivants :
|
Le mode préréglé (preset) vous permet de préconfigurer la température ciblée. Utilisé en conjonction avec Scheduler (voir [scheduler](additions.md#composant-scheduler-)) vous aurez un moyen puissant et simple d'optimiser la température par rapport à la consommation électrique de votre maison. Les préréglages gérés sont les suivants :
|
||||||
- **Eco** : l'appareil est en mode d'économie d'énergie
|
- **Eco** : l'appareil est en mode d'économie d'énergie
|
||||||
- **Confort** : l'appareil est en mode confort
|
- **Confort** : l'appareil est en mode confort
|
||||||
- **Boost** : l'appareil tourne toutes les vannes à fond
|
- **Boost** : l'appareil tourne toutes les vannes à fond
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ Ce composant nommé __Versatile thermostat__ gère les cas d'utilisation suivant
|
|||||||
- Configuration via l'interface graphique d'intégration standard (à l'aide du flux Config Entry),
|
- Configuration via l'interface graphique d'intégration standard (à l'aide du flux Config Entry),
|
||||||
- Utilisations complètes du **mode préréglages**,
|
- Utilisations complètes du **mode préréglages**,
|
||||||
- Désactiver le mode préréglé lorsque la température est **définie manuellement** sur un thermostat,
|
- Désactiver le mode préréglé lorsque la température est **définie manuellement** sur un thermostat,
|
||||||
- Éteindre/allumer un thermostat ou chager de preset lorsqu'une **porte ou des fenêtres sont ouvertes/fermées** après un certain délai,
|
- Éteindre/allumer un thermostat ou changer de preset lorsqu'une **porte ou des fenêtres sont ouvertes/fermées** après un certain délai,
|
||||||
- Changer de preset lorsqu'une **activité est détectée** ou non dans une pièce pendant un temps défini,
|
- Changer de preset lorsqu'une **activité est détectée** ou non dans une pièce pendant un temps défini,
|
||||||
- Utiliser un algorithme **TPI (Time Proportional Interval)** grâce à l'algorithme [[Argonaute](https://forum.hacf.fr/u/argonaute/summary)] ,
|
- Utiliser un algorithme **TPI (Time Proportional Interval)** grâce à l'algorithme [[Argonaute](https://forum.hacf.fr/u/argonaute/summary)] ,
|
||||||
- Ajouter une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré.
|
- Ajouter une **gestion de délestage** ou une régulation pour ne pas dépasser une puissance totale définie. Lorsque la puissance maximale est dépassée, un préréglage caché de « puissance » est défini sur l'entité climatique. Lorsque la puissance passe en dessous du maximum, le préréglage précédent est restauré.
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
> * **Release 7.1**:
|
||||||
|
> - Refonte de la fonction de délestage (gestion de la puissance). Le délestage est maintenant géré de façon centralisé (auparavent chaque _VTherm_ était autonome). Cela permet une gestion bien plus efficace et de prioriser le délestage sur les équipements qui sont proches de la consigne. Attention, vous devez impérativement avoir une configuration centralisée avec gestion de la puissance pour que cela fonctionne. Plus d'infos [ici](./feature-power.md)
|
||||||
|
|
||||||
> * **Release 6.8**:
|
> * **Release 6.8**:
|
||||||
> - Ajout d'une nouvelle méthode de régulation pour les Versatile Thermostat de type `over_climate`. Cette méthode nommée 'Contrôle direct de la vanne' permet de contrôler directement la vanne d'un TRV et éventuellement un décalage pour calibrer le thermomètre interne de votre TRV. Cette nouvelle méthode a été testée avec des Sonoff TRVZB et généralisée pour d'autre type de TRV pour lesquels la vanne est directement commandable via des entités de type `number`. Plus d'informations [ici](over-climate.md#lauto-régulation) et [ici](self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne).
|
> - Ajout d'une nouvelle méthode de régulation pour les Versatile Thermostat de type `over_climate`. Cette méthode nommée 'Contrôle direct de la vanne' permet de contrôler directement la vanne d'un TRV et éventuellement un décalage pour calibrer le thermomètre interne de votre TRV. Cette nouvelle méthode a été testée avec des Sonoff TRVZB et généralisée pour d'autre type de TRV pour lesquels la vanne est directement commandable via des entités de type `number`. Plus d'informations [ici](over-climate.md#lauto-régulation) et [ici](self-regulation.md#auto-régulation-par-contrôle-direct-de-la-vanne).
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ L'algorithme de calcul du taux d'ouverture est basé sur le _TPI_ qui est décri
|
|||||||
|
|
||||||
Si une entité de type taux de fermeture de la vanne est configurée, il sera positionné avec la valeur 100 - taux d'ouverture pour forcer la vanne dans un état.
|
Si une entité de type taux de fermeture de la vanne est configurée, il sera positionné avec la valeur 100 - taux d'ouverture pour forcer la vanne dans un état.
|
||||||
|
|
||||||
|
Note: pour les Sonoff TRVZB, vous ne devez pas configurer les "closing degree". Cela rend inopérant le `hvac_action` qui est utilisé par _VTherm_ et qui indique que l'équipement est en chauffe.
|
||||||
|
|
||||||
### autres auto-régulation
|
### autres auto-régulation
|
||||||
|
|
||||||
Dans ce deuxième cas, le Versatile Thermostat calcule un décalage basé sur les informations suivantes :
|
Dans ce deuxième cas, le Versatile Thermostat calcule un décalage basé sur les informations suivantes :
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[tool.black]
|
||||||
|
# don't work. Options are in the devcontainer.yaml
|
||||||
|
line-length = 180
|
||||||
@@ -59,8 +59,6 @@ from .const import ( # pylint: disable=unused-import
|
|||||||
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
|
MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG,
|
||||||
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
|
MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG,
|
||||||
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
MOCK_TH_OVER_SWITCH_TPI_CONFIG,
|
||||||
MOCK_PRESETS_CONFIG,
|
|
||||||
MOCK_PRESETS_AC_CONFIG,
|
|
||||||
MOCK_WINDOW_CONFIG,
|
MOCK_WINDOW_CONFIG,
|
||||||
MOCK_MOTION_CONFIG,
|
MOCK_MOTION_CONFIG,
|
||||||
MOCK_POWER_CONFIG,
|
MOCK_POWER_CONFIG,
|
||||||
@@ -89,7 +87,7 @@ FULL_SWITCH_CONFIG = (
|
|||||||
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
|
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
| MOCK_TH_OVER_SWITCH_TYPE_CONFIG
|
||||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||||
| MOCK_PRESETS_CONFIG
|
# | MOCK_PRESETS_CONFIG
|
||||||
| MOCK_FULL_FEATURES
|
| MOCK_FULL_FEATURES
|
||||||
| MOCK_WINDOW_CONFIG
|
| MOCK_WINDOW_CONFIG
|
||||||
| MOCK_MOTION_CONFIG
|
| MOCK_MOTION_CONFIG
|
||||||
@@ -104,7 +102,6 @@ FULL_SWITCH_AC_CONFIG = (
|
|||||||
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
|
| MOCK_TH_OVER_SWITCH_CENTRAL_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
|
| MOCK_TH_OVER_SWITCH_AC_TYPE_CONFIG
|
||||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||||
| MOCK_PRESETS_AC_CONFIG
|
|
||||||
| MOCK_FULL_FEATURES
|
| MOCK_FULL_FEATURES
|
||||||
| MOCK_WINDOW_CONFIG
|
| MOCK_WINDOW_CONFIG
|
||||||
| MOCK_MOTION_CONFIG
|
| MOCK_MOTION_CONFIG
|
||||||
@@ -118,7 +115,7 @@ PARTIAL_CLIMATE_CONFIG = (
|
|||||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
| MOCK_TH_OVER_CLIMATE_TYPE_CONFIG
|
||||||
| MOCK_PRESETS_CONFIG
|
# | MOCK_PRESETS_CONFIG
|
||||||
| MOCK_ADVANCED_CONFIG
|
| MOCK_ADVANCED_CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,7 +124,7 @@ PARTIAL_CLIMATE_CONFIG_USE_DEVICE_TEMP = (
|
|||||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
|
| MOCK_TH_OVER_CLIMATE_TYPE_USE_DEVICE_TEMP_CONFIG
|
||||||
| MOCK_PRESETS_CONFIG
|
# | MOCK_PRESETS_CONFIG
|
||||||
| MOCK_ADVANCED_CONFIG
|
| MOCK_ADVANCED_CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -136,7 +133,7 @@ PARTIAL_CLIMATE_NOT_REGULATED_CONFIG = (
|
|||||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
|
| MOCK_TH_OVER_CLIMATE_TYPE_NOT_REGULATED_CONFIG
|
||||||
| MOCK_PRESETS_CONFIG
|
# | MOCK_PRESETS_CONFIG
|
||||||
| MOCK_ADVANCED_CONFIG
|
| MOCK_ADVANCED_CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -145,7 +142,7 @@ PARTIAL_CLIMATE_AC_CONFIG = (
|
|||||||
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
|
| MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
| MOCK_TH_OVER_CLIMATE_MAIN_CONFIG
|
||||||
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
| MOCK_TH_OVER_CLIMATE_CENTRAL_MAIN_CONFIG
|
||||||
| MOCK_PRESETS_CONFIG
|
# | MOCK_PRESETS_CONFIG
|
||||||
| MOCK_ADVANCED_CONFIG
|
| MOCK_ADVANCED_CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,7 +150,7 @@ FULL_4SWITCH_CONFIG = (
|
|||||||
MOCK_TH_OVER_4SWITCH_USER_CONFIG
|
MOCK_TH_OVER_4SWITCH_USER_CONFIG
|
||||||
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
|
| MOCK_TH_OVER_4SWITCH_TYPE_CONFIG
|
||||||
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
| MOCK_TH_OVER_SWITCH_TPI_CONFIG
|
||||||
| MOCK_PRESETS_CONFIG
|
# | MOCK_PRESETS_CONFIG
|
||||||
| MOCK_WINDOW_CONFIG
|
| MOCK_WINDOW_CONFIG
|
||||||
| MOCK_MOTION_CONFIG
|
| MOCK_MOTION_CONFIG
|
||||||
| MOCK_POWER_CONFIG
|
| MOCK_POWER_CONFIG
|
||||||
@@ -592,7 +589,10 @@ class MockNumber(NumberEntity):
|
|||||||
|
|
||||||
|
|
||||||
async def create_thermostat(
|
async def create_thermostat(
|
||||||
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
|
hass: HomeAssistant,
|
||||||
|
entry: MockConfigEntry,
|
||||||
|
entity_id: str,
|
||||||
|
temps: dict | None = None,
|
||||||
) -> BaseThermostat:
|
) -> BaseThermostat:
|
||||||
"""Creates and return a TPI Thermostat"""
|
"""Creates and return a TPI Thermostat"""
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
@@ -601,6 +601,11 @@ async def create_thermostat(
|
|||||||
|
|
||||||
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||||
|
|
||||||
|
if entity and temps:
|
||||||
|
await set_all_climate_preset_temp(
|
||||||
|
hass, entity, temps, entity.entity_id.replace("climate.", "")
|
||||||
|
)
|
||||||
|
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
|
|
||||||
@@ -741,9 +746,11 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await entity.power_manager._async_power_sensor_changed(power_event)
|
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||||
|
await vtherm_api.central_power_manager._power_sensor_changed(power_event)
|
||||||
|
await vtherm_api.central_power_manager._do_immediate_shedding()
|
||||||
if sleep:
|
if sleep:
|
||||||
await asyncio.sleep(0.1)
|
await entity.hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def send_max_power_change_event(
|
async def send_max_power_change_event(
|
||||||
@@ -767,9 +774,11 @@ async def send_max_power_change_event(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await entity.power_manager._async_max_power_sensor_changed(power_event)
|
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||||
|
await vtherm_api.central_power_manager._max_power_sensor_changed(power_event)
|
||||||
|
await vtherm_api.central_power_manager._do_immediate_shedding()
|
||||||
if sleep:
|
if sleep:
|
||||||
await asyncio.sleep(0.1)
|
await entity.hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def send_window_change_event(
|
async def send_window_change_event(
|
||||||
@@ -1101,3 +1110,9 @@ class SideEffects:
|
|||||||
def add_or_update_side_effect(self, key: str, new_value: Any):
|
def add_or_update_side_effect(self, key: str, new_value: Any):
|
||||||
"""Update the value of a side effect"""
|
"""Update the value of a side effect"""
|
||||||
self._current_side_effects[key] = new_value
|
self._current_side_effects[key] = new_value
|
||||||
|
|
||||||
|
|
||||||
|
async def do_central_power_refresh(hass):
|
||||||
|
"""Do a central power refresh"""
|
||||||
|
await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
|
||||||
|
return hass.async_block_till_done()
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
# https://github.com/miketheman/pytest-socket/pull/275
|
||||||
|
from pytest_socket import socket_allow_hosts
|
||||||
|
|
||||||
from homeassistant.core import StateMachine
|
from homeassistant.core import StateMachine
|
||||||
|
|
||||||
@@ -26,6 +28,12 @@ from custom_components.versatile_thermostat.config_flow import (
|
|||||||
VersatileThermostatBaseConfigFlow,
|
VersatileThermostatBaseConfigFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.const import (
|
||||||
|
CONF_POWER_SENSOR,
|
||||||
|
CONF_MAX_POWER_SENSOR,
|
||||||
|
CONF_USE_POWER_FEATURE,
|
||||||
|
CONF_PRESET_POWER,
|
||||||
|
)
|
||||||
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
|
||||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||||
|
|
||||||
@@ -35,12 +43,6 @@ from .commons import (
|
|||||||
FULL_CENTRAL_CONFIG_WITH_BOILER,
|
FULL_CENTRAL_CONFIG_WITH_BOILER,
|
||||||
)
|
)
|
||||||
|
|
||||||
# https://github.com/miketheman/pytest-socket/pull/275
|
|
||||||
from pytest_socket import socket_allow_hosts
|
|
||||||
|
|
||||||
# ...
|
|
||||||
|
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
def pytest_runtest_setup():
|
def pytest_runtest_setup():
|
||||||
"""setup tests"""
|
"""setup tests"""
|
||||||
@@ -51,16 +53,6 @@ def pytest_runtest_setup():
|
|||||||
|
|
||||||
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
|
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
|
||||||
|
|
||||||
# Permet d'exclure certains test en mode d'ex
|
|
||||||
# sequential = pytest.mark.sequential
|
|
||||||
|
|
||||||
|
|
||||||
# This fixture allow to execute some tests first and not in //
|
|
||||||
# @pytest.fixture
|
|
||||||
# def order():
|
|
||||||
# return 1
|
|
||||||
#
|
|
||||||
|
|
||||||
# This fixture enables loading custom integrations in all tests.
|
# This fixture enables loading custom integrations in all tests.
|
||||||
# Remove to enable selective use of this fixture
|
# Remove to enable selective use of this fixture
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@@ -167,3 +159,24 @@ async def init_central_config_with_boiler_fixture(
|
|||||||
await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER)
|
await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER)
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="init_central_power_manager")
|
||||||
|
async def init_central_power_manager_fixture(
|
||||||
|
hass, init_central_config
|
||||||
|
): # pylint: disable=unused-argument
|
||||||
|
"""Initialize the central power_manager"""
|
||||||
|
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||||
|
|
||||||
|
# 1. creation / init
|
||||||
|
vtherm_api.central_power_manager.post_init(
|
||||||
|
{
|
||||||
|
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_PRESET_POWER: 13,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert vtherm_api.central_power_manager.is_configured
|
||||||
|
|
||||||
|
yield
|
||||||
|
|||||||
@@ -140,25 +140,6 @@ MOCK_TH_OVER_CLIMATE_TYPE_AC_CONFIG = {
|
|||||||
CONF_AUTO_REGULATION_PERIOD_MIN: 1,
|
CONF_AUTO_REGULATION_PERIOD_MIN: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO remove this later
|
|
||||||
MOCK_PRESETS_CONFIG = {
|
|
||||||
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
|
|
||||||
PRESET_ECO + PRESET_TEMP_SUFFIX: 16,
|
|
||||||
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 17,
|
|
||||||
PRESET_BOOST + PRESET_TEMP_SUFFIX: 18,
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO remove this later
|
|
||||||
MOCK_PRESETS_AC_CONFIG = {
|
|
||||||
PRESET_FROST_PROTECTION + PRESET_TEMP_SUFFIX: 7,
|
|
||||||
PRESET_ECO + PRESET_TEMP_SUFFIX: 17,
|
|
||||||
PRESET_COMFORT + PRESET_TEMP_SUFFIX: 19,
|
|
||||||
PRESET_BOOST + PRESET_TEMP_SUFFIX: 20,
|
|
||||||
PRESET_ECO + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 25,
|
|
||||||
PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 23,
|
|
||||||
PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_TEMP_SUFFIX: 21,
|
|
||||||
}
|
|
||||||
|
|
||||||
MOCK_WINDOW_CONFIG = {
|
MOCK_WINDOW_CONFIG = {
|
||||||
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
CONF_WINDOW_SENSOR: "binary_sensor.window_sensor",
|
||||||
# Not used normally only for tests to avoid rewrite all tests
|
# Not used normally only for tests to avoid rewrite all tests
|
||||||
@@ -184,12 +165,16 @@ MOCK_MOTION_CONFIG = {
|
|||||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||||
}
|
}
|
||||||
|
|
||||||
MOCK_POWER_CONFIG = {
|
MOCK_CENTRAL_POWER_CONFIG = {
|
||||||
CONF_POWER_SENSOR: "sensor.power_sensor",
|
CONF_POWER_SENSOR: "sensor.power_sensor",
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
||||||
CONF_PRESET_POWER: 10,
|
CONF_PRESET_POWER: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MOCK_POWER_CONFIG = {
|
||||||
|
CONF_PRESET_POWER: 10,
|
||||||
|
}
|
||||||
|
|
||||||
MOCK_PRESENCE_CONFIG = {
|
MOCK_PRESENCE_CONFIG = {
|
||||||
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long
|
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access
|
||||||
|
|
||||||
""" Test the normal start of a Thermostat """
|
""" Test the normal start of a Thermostat """
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
@@ -107,9 +107,16 @@ async def test_overpowering_binary_sensors(
|
|||||||
skip_hass_states_is_state,
|
skip_hass_states_is_state,
|
||||||
skip_turn_on_off_heater,
|
skip_turn_on_off_heater,
|
||||||
skip_send_event,
|
skip_send_event,
|
||||||
|
init_central_power_manager,
|
||||||
):
|
):
|
||||||
"""Test the overpowering binary sensors in thermostat type"""
|
"""Test the overpowering binary sensors in thermostat type"""
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
"eco": 17,
|
||||||
|
"comfort": 18,
|
||||||
|
"boost": 19,
|
||||||
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverSwitchMockName",
|
title="TheOverSwitchMockName",
|
||||||
@@ -122,9 +129,6 @@ async def test_overpowering_binary_sensors(
|
|||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 19,
|
|
||||||
CONF_USE_WINDOW_FEATURE: False,
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
@@ -136,15 +140,13 @@ async def test_overpowering_binary_sensors(
|
|||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SAFETY_DELAY_MIN: 5,
|
CONF_SAFETY_DELAY_MIN: 5,
|
||||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
||||||
CONF_DEVICE_POWER: 100,
|
CONF_DEVICE_POWER: 100,
|
||||||
CONF_PRESET_POWER: 12,
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: BaseThermostat = await create_thermostat(
|
entity: BaseThermostat = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname", temps
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
@@ -153,35 +155,55 @@ async def test_overpowering_binary_sensors(
|
|||||||
)
|
)
|
||||||
assert overpowering_binary_sensor
|
assert overpowering_binary_sensor
|
||||||
|
|
||||||
now: datetime = datetime.now(tz=get_tz(hass))
|
now: datetime = NowClass.get_now(hass)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
# Overpowering should be not set because poer have not been received
|
# Overpowering should be not set because poer have not been received
|
||||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
await send_temperature_change_event(entity, 15, now)
|
await send_temperature_change_event(entity, 15, now)
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||||
|
|
||||||
await overpowering_binary_sensor.async_my_climate_changed()
|
await overpowering_binary_sensor.async_my_climate_changed()
|
||||||
assert overpowering_binary_sensor.state is STATE_OFF
|
assert overpowering_binary_sensor.state is STATE_OFF
|
||||||
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
|
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
|
||||||
|
|
||||||
await send_power_change_event(entity, 100, now)
|
# Send power mesurement
|
||||||
await send_max_power_change_event(entity, 150, now)
|
side_effects = SideEffects(
|
||||||
assert await entity.power_manager.check_overpowering() is True
|
{
|
||||||
assert entity.power_manager.overpowering_state is STATE_ON
|
"sensor.the_power_sensor": State("sensor.the_power_sensor", 150),
|
||||||
|
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 100),
|
||||||
|
},
|
||||||
|
State("unknown.entity_id", "unknown"),
|
||||||
|
)
|
||||||
|
# fmt:off
|
||||||
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||||
|
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
|
||||||
|
# fmt: on
|
||||||
|
await send_power_change_event(entity, 150, now)
|
||||||
|
await send_max_power_change_event(entity, 100, now)
|
||||||
|
|
||||||
# Simulate the event reception
|
assert entity.power_manager.is_overpowering_detected is True
|
||||||
await overpowering_binary_sensor.async_my_climate_changed()
|
assert entity.power_manager.overpowering_state is STATE_ON
|
||||||
assert overpowering_binary_sensor.state == STATE_ON
|
|
||||||
|
# Simulate the event reception
|
||||||
|
await overpowering_binary_sensor.async_my_climate_changed()
|
||||||
|
assert overpowering_binary_sensor.state == STATE_ON
|
||||||
|
|
||||||
# set max power to a low value
|
# set max power to a low value
|
||||||
await send_max_power_change_event(entity, 201, now)
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 251))
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
# fmt:off
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||||
# Simulate the event reception
|
# fmt: on
|
||||||
await overpowering_binary_sensor.async_my_climate_changed()
|
now = now + timedelta(seconds=30)
|
||||||
assert overpowering_binary_sensor.state == STATE_OFF
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
await send_max_power_change_event(entity, 251, now)
|
||||||
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
|
# Simulate the event reception
|
||||||
|
await overpowering_binary_sensor.async_my_climate_changed()
|
||||||
|
assert overpowering_binary_sensor.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
|
|||||||
@@ -266,7 +266,9 @@ async def test_bug_272(
|
|||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
async def test_bug_407(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||||
|
):
|
||||||
"""Test the followin case in power management:
|
"""Test the followin case in power management:
|
||||||
1. a heater is active (heating). So the power consumption takes the heater power into account. We suppose the power consumption is near the threshold,
|
1. a heater is active (heating). So the power consumption takes the heater power into account. We suppose the power consumption is near the threshold,
|
||||||
2. the user switch preset let's say from Comfort to Boost,
|
2. the user switch preset let's say from Comfort to Boost,
|
||||||
@@ -275,6 +277,12 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
"eco": 17,
|
||||||
|
"comfort": 18,
|
||||||
|
"boost": 19,
|
||||||
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverSwitchMockName",
|
title="TheOverSwitchMockName",
|
||||||
@@ -287,9 +295,6 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 19,
|
|
||||||
CONF_USE_WINDOW_FEATURE: False,
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
@@ -301,34 +306,43 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SAFETY_DELAY_MIN: 5,
|
CONF_SAFETY_DELAY_MIN: 5,
|
||||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
||||||
CONF_DEVICE_POWER: 100,
|
CONF_DEVICE_POWER: 100,
|
||||||
CONF_PRESET_POWER: 12,
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: ThermostatOverSwitch = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname", temps
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
tpi_algo = entity._prop_algorithm
|
tpi_algo = entity._prop_algorithm
|
||||||
assert tpi_algo
|
assert tpi_algo
|
||||||
|
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
now: datetime = NowClass.get_now(hass)
|
||||||
now: datetime = datetime.now(tz=tz)
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
await send_temperature_change_event(entity, 16, now)
|
await send_temperature_change_event(entity, 16, now)
|
||||||
await send_ext_temperature_change_event(entity, 10, now)
|
await send_ext_temperature_change_event(entity, 10, now)
|
||||||
|
|
||||||
# 1. An already active heater will not switch to overpowering
|
# 1. An already active heater will not switch to overpowering
|
||||||
|
side_effects = SideEffects(
|
||||||
|
{
|
||||||
|
"sensor.the_power_sensor": State("sensor.the_power_sensor", 100),
|
||||||
|
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 110),
|
||||||
|
},
|
||||||
|
State("unknown.entity_id", "unknown"),
|
||||||
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.core.ServiceRegistry.async_call"
|
"homeassistant.core.ServiceRegistry.async_call"
|
||||||
) as mock_service_call, patch(
|
) as mock_service_call, patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||||
new_callable=PropertyMock,
|
new_callable=PropertyMock,
|
||||||
return_value=True,
|
return_value=True,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.core.StateMachine.get",
|
||||||
|
side_effect=side_effects.get_side_effects(),
|
||||||
):
|
):
|
||||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
@@ -337,16 +351,17 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||||
assert entity.target_temperature == 18
|
assert entity.target_temperature == 18
|
||||||
# waits that the heater starts
|
# waits that the heater starts
|
||||||
await asyncio.sleep(0.1)
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert mock_service_call.call_count >= 1
|
assert mock_service_call.call_count >= 1
|
||||||
assert entity.is_device_active is True
|
assert entity.is_device_active is True
|
||||||
|
|
||||||
# Send power max mesurement
|
# Send power max mesurement
|
||||||
await send_max_power_change_event(entity, 110, datetime.now())
|
await send_max_power_change_event(entity, 110, now)
|
||||||
# Send power mesurement (theheater is already in the power measurement)
|
# Send power mesurement (theheater is already in the power measurement)
|
||||||
await send_power_change_event(entity, 100, datetime.now())
|
await send_power_change_event(entity, 100, now)
|
||||||
# No overpowering yet
|
# No overpowering yet
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
# All configuration is complete and power is < power_max
|
# All configuration is complete and power is < power_max
|
||||||
assert entity.preset_mode is PRESET_COMFORT
|
assert entity.preset_mode is PRESET_COMFORT
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
@@ -359,36 +374,57 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
|
|||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||||
new_callable=PropertyMock,
|
new_callable=PropertyMock,
|
||||||
return_value=True,
|
return_value=True,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.core.StateMachine.get",
|
||||||
|
side_effect=side_effects.get_side_effects(),
|
||||||
):
|
):
|
||||||
|
now = now + timedelta(seconds=30)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
# change preset to Boost
|
# change preset to Boost
|
||||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
# waits that the heater starts
|
# waits that the heater starts
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
# doesn't work for call_later
|
||||||
|
# await hass.async_block_till_done()
|
||||||
|
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
# simulate a refresh for central power (not necessary)
|
||||||
|
await do_central_power_refresh(hass)
|
||||||
|
|
||||||
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
assert entity.hvac_mode is HVACMode.HEAT
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
assert entity.target_temperature == 19
|
assert entity.target_temperature == 19
|
||||||
assert mock_service_call.call_count >= 1
|
assert mock_service_call.call_count >= 1
|
||||||
|
|
||||||
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
|
# 3. if heater is stopped (is_device_active==False) and power is over max, then overpowering should be started
|
||||||
|
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 150))
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.core.ServiceRegistry.async_call"
|
"homeassistant.core.ServiceRegistry.async_call"
|
||||||
) as mock_service_call, patch(
|
) as mock_service_call, patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||||
new_callable=PropertyMock,
|
new_callable=PropertyMock,
|
||||||
return_value=False,
|
return_value=False,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.core.StateMachine.get",
|
||||||
|
side_effect=side_effects.get_side_effects(),
|
||||||
):
|
):
|
||||||
|
now = now + timedelta(seconds=30)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
# change preset to Boost
|
# change preset to Boost
|
||||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
# waits that the heater starts
|
# waits that the heater starts
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
assert await entity.power_manager.check_overpowering() is True
|
# simulate a refresh for central power (not necessary)
|
||||||
|
await do_central_power_refresh(hass)
|
||||||
|
|
||||||
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
assert entity.hvac_mode is HVACMode.HEAT
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.preset_mode is PRESET_POWER
|
assert entity.preset_mode is PRESET_COMFORT
|
||||||
assert entity.power_manager.overpowering_state is STATE_ON
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@@ -445,8 +481,6 @@ async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
|
|||||||
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
CONF_USE_WINDOW_CENTRAL_CONFIG: False,
|
||||||
CONF_WINDOW_SENSOR: "sensor.theWindowSensor",
|
CONF_WINDOW_SENSOR: "sensor.theWindowSensor",
|
||||||
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
CONF_USE_POWER_CENTRAL_CONFIG: False,
|
||||||
CONF_POWER_SENSOR: "sensor.thePowerSensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.theMaxPowerSensor",
|
|
||||||
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
CONF_USE_PRESENCE_CENTRAL_CONFIG: False,
|
||||||
CONF_PRESENCE_SENSOR: "sensor.thePresenceSensor",
|
CONF_PRESENCE_SENSOR: "sensor.thePresenceSensor",
|
||||||
CONF_USE_MOTION_FEATURE: True, # motion sensor need to be checked AND a motion sensor set
|
CONF_USE_MOTION_FEATURE: True, # motion sensor need to be checked AND a motion sensor set
|
||||||
@@ -456,7 +490,7 @@ async def test_bug_500_3(hass: HomeAssistant, init_vtherm_api) -> None:
|
|||||||
flow = VersatileThermostatBaseConfigFlow(config)
|
flow = VersatileThermostatBaseConfigFlow(config)
|
||||||
|
|
||||||
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
|
assert flow._infos[CONF_USE_WINDOW_FEATURE] is True
|
||||||
assert flow._infos[CONF_USE_POWER_FEATURE] is True
|
assert flow._infos[CONF_USE_POWER_FEATURE] is False
|
||||||
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
|
assert flow._infos[CONF_USE_PRESENCE_FEATURE] is True
|
||||||
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
|
assert flow._infos[CONF_USE_MOTION_FEATURE] is True
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,18 @@ async def test_full_over_switch_wo_central_config(
|
|||||||
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
|
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
|
||||||
):
|
):
|
||||||
"""Tests that a VTherm without any central_configuration is working with its own attributes"""
|
"""Tests that a VTherm without any central_configuration is working with its own attributes"""
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
"frost": 10,
|
||||||
|
"eco": 17,
|
||||||
|
"comfort": 18,
|
||||||
|
"boost": 21,
|
||||||
|
"frost_away": 13,
|
||||||
|
"eco_away": 13,
|
||||||
|
"comfort_away": 13,
|
||||||
|
"boost_away": 13,
|
||||||
|
}
|
||||||
|
|
||||||
# Add a Switch VTherm
|
# Add a Switch VTherm
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
@@ -202,19 +214,11 @@ async def test_full_over_switch_wo_central_config(
|
|||||||
CONF_TEMP_MIN: 8,
|
CONF_TEMP_MIN: 8,
|
||||||
CONF_TEMP_MAX: 18,
|
CONF_TEMP_MAX: 18,
|
||||||
CONF_STEP_TEMPERATURE: 0.3,
|
CONF_STEP_TEMPERATURE: 0.3,
|
||||||
"frost_temp": 10,
|
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 21,
|
|
||||||
"frost_away_temp": 13,
|
|
||||||
"eco_away_temp": 13,
|
|
||||||
"comfort_away_temp": 13,
|
|
||||||
"boost_away_temp": 13,
|
|
||||||
CONF_USE_WINDOW_FEATURE: True,
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
CONF_USE_MOTION_FEATURE: True,
|
CONF_USE_MOTION_FEATURE: True,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
CONF_USE_PRESENCE_FEATURE: True,
|
CONF_USE_PRESENCE_FEATURE: True,
|
||||||
CONF_HEATER: "switch.mock_switch",
|
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
CONF_INVERSE_SWITCH: False,
|
CONF_INVERSE_SWITCH: False,
|
||||||
CONF_TPI_COEF_INT: 0.3,
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
@@ -233,8 +237,6 @@ async def test_full_over_switch_wo_central_config(
|
|||||||
CONF_MOTION_PRESET: "comfort",
|
CONF_MOTION_PRESET: "comfort",
|
||||||
CONF_NO_MOTION_PRESET: "eco",
|
CONF_NO_MOTION_PRESET: "eco",
|
||||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
|
|
||||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||||
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
CONF_USE_MAIN_CENTRAL_CONFIG: False,
|
||||||
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
CONF_USE_TPI_CENTRAL_CONFIG: False,
|
||||||
@@ -249,7 +251,7 @@ async def test_full_over_switch_wo_central_config(
|
|||||||
|
|
||||||
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
with patch("homeassistant.core.ServiceRegistry.async_call"):
|
||||||
entity: ThermostatOverSwitch = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname", temps
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
assert entity.name == "TheOverSwitchMockName"
|
assert entity.name == "TheOverSwitchMockName"
|
||||||
@@ -300,10 +302,13 @@ async def test_full_over_switch_wo_central_config(
|
|||||||
assert entity.motion_manager.motion_preset == "comfort"
|
assert entity.motion_manager.motion_preset == "comfort"
|
||||||
assert entity.motion_manager.no_motion_preset == "eco"
|
assert entity.motion_manager.no_motion_preset == "eco"
|
||||||
|
|
||||||
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
|
|
||||||
assert (
|
assert (
|
||||||
entity.power_manager.max_power_sensor_entity_id
|
VersatileThermostatAPI.get_vtherm_api().central_power_manager.power_sensor_entity_id
|
||||||
== "sensor.mock_max_power_sensor"
|
is None
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
VersatileThermostatAPI.get_vtherm_api().central_power_manager.max_power_sensor_entity_id
|
||||||
|
is None
|
||||||
)
|
)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
@@ -317,7 +322,7 @@ async def test_full_over_switch_wo_central_config(
|
|||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_full_over_switch_with_central_config(
|
async def test_full_over_switch_with_central_config(
|
||||||
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||||
):
|
):
|
||||||
"""Tests that a VTherm with central_configuration is working with the central_config attributes"""
|
"""Tests that a VTherm with central_configuration is working with the central_config attributes"""
|
||||||
# Add a Switch VTherm
|
# Add a Switch VTherm
|
||||||
@@ -334,15 +339,11 @@ async def test_full_over_switch_with_central_config(
|
|||||||
CONF_TEMP_MIN: 8,
|
CONF_TEMP_MIN: 8,
|
||||||
CONF_TEMP_MAX: 18,
|
CONF_TEMP_MAX: 18,
|
||||||
CONF_STEP_TEMPERATURE: 0.3,
|
CONF_STEP_TEMPERATURE: 0.3,
|
||||||
"frost_temp": 10,
|
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 21,
|
|
||||||
CONF_USE_WINDOW_FEATURE: True,
|
CONF_USE_WINDOW_FEATURE: True,
|
||||||
CONF_USE_MOTION_FEATURE: True,
|
CONF_USE_MOTION_FEATURE: True,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
CONF_USE_PRESENCE_FEATURE: True,
|
CONF_USE_PRESENCE_FEATURE: True,
|
||||||
CONF_HEATER: "switch.mock_switch",
|
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
CONF_INVERSE_SWITCH: False,
|
CONF_INVERSE_SWITCH: False,
|
||||||
CONF_TPI_COEF_INT: 0.3,
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
@@ -361,8 +362,6 @@ async def test_full_over_switch_with_central_config(
|
|||||||
CONF_MOTION_PRESET: "comfort",
|
CONF_MOTION_PRESET: "comfort",
|
||||||
CONF_NO_MOTION_PRESET: "eco",
|
CONF_NO_MOTION_PRESET: "eco",
|
||||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_max_power_sensor",
|
|
||||||
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||||
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
CONF_USE_MAIN_CENTRAL_CONFIG: True,
|
||||||
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
CONF_USE_TPI_CENTRAL_CONFIG: True,
|
||||||
@@ -426,10 +425,13 @@ async def test_full_over_switch_with_central_config(
|
|||||||
assert entity.motion_manager.motion_preset == "boost"
|
assert entity.motion_manager.motion_preset == "boost"
|
||||||
assert entity.motion_manager.no_motion_preset == "frost"
|
assert entity.motion_manager.no_motion_preset == "frost"
|
||||||
|
|
||||||
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
|
|
||||||
assert (
|
assert (
|
||||||
entity.power_manager.max_power_sensor_entity_id
|
VersatileThermostatAPI.get_vtherm_api().central_power_manager.power_sensor_entity_id
|
||||||
== "sensor.mock_max_power_sensor"
|
== "sensor.the_power_sensor"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
VersatileThermostatAPI.get_vtherm_api().central_power_manager.max_power_sensor_entity_id
|
||||||
|
== "sensor.the_max_power_sensor"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
|
|||||||
702
tests/test_central_power_manager.py
Normal file
702
tests/test_central_power_manager.py
Normal file
@@ -0,0 +1,702 @@
|
|||||||
|
# pylint: disable=protected-access, unused-argument, line-too-long
|
||||||
|
""" Test the Central Power management """
|
||||||
|
from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from custom_components.versatile_thermostat.feature_power_manager import (
|
||||||
|
FeaturePowerManager,
|
||||||
|
)
|
||||||
|
from custom_components.versatile_thermostat.central_feature_power_manager import (
|
||||||
|
CentralFeaturePowerManager,
|
||||||
|
)
|
||||||
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"use_power_feature, power_entity_id, max_power_entity_id, power_temp, is_configured",
|
||||||
|
[
|
||||||
|
(True, "sensor.power_id", "sensor.max_power_id", 13, True),
|
||||||
|
(True, None, "sensor.max_power_id", 13, False),
|
||||||
|
(True, "sensor.power_id", None, 13, False),
|
||||||
|
(True, "sensor.power_id", "sensor.max_power_id", None, False),
|
||||||
|
(False, "sensor.power_id", "sensor.max_power_id", 13, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_central_power_manager_init(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
use_power_feature,
|
||||||
|
power_entity_id,
|
||||||
|
max_power_entity_id,
|
||||||
|
power_temp,
|
||||||
|
is_configured,
|
||||||
|
):
|
||||||
|
"""Test creation and post_init of the Central Power Manager"""
|
||||||
|
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||||
|
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||||
|
|
||||||
|
assert central_power_manager.is_configured is False
|
||||||
|
assert central_power_manager.current_max_power is None
|
||||||
|
assert central_power_manager.current_power is None
|
||||||
|
assert central_power_manager.power_temperature is None
|
||||||
|
assert central_power_manager.name == "centralPowerManager"
|
||||||
|
|
||||||
|
# 2. post_init
|
||||||
|
central_power_manager.post_init(
|
||||||
|
{
|
||||||
|
CONF_POWER_SENSOR: power_entity_id,
|
||||||
|
CONF_MAX_POWER_SENSOR: max_power_entity_id,
|
||||||
|
CONF_USE_POWER_FEATURE: use_power_feature,
|
||||||
|
CONF_PRESET_POWER: power_temp,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert central_power_manager.is_configured == is_configured
|
||||||
|
assert central_power_manager.current_max_power is None
|
||||||
|
assert central_power_manager.current_power is None
|
||||||
|
assert central_power_manager.power_temperature == power_temp
|
||||||
|
|
||||||
|
# 3. start listening
|
||||||
|
await central_power_manager.start_listening()
|
||||||
|
assert len(central_power_manager._active_listener) == (2 if is_configured else 0)
|
||||||
|
|
||||||
|
# 4. stop listening
|
||||||
|
central_power_manager.stop_listening()
|
||||||
|
assert len(central_power_manager._active_listener) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"vtherm_configs, results",
|
||||||
|
[
|
||||||
|
# simple sort
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "vtherm1",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 13,
|
||||||
|
"target_temperature": 12,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm2",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 18,
|
||||||
|
"target_temperature": 12,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm3",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 12,
|
||||||
|
"target_temperature": 18,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["vtherm2", "vtherm1", "vtherm3"],
|
||||||
|
),
|
||||||
|
# Ignore power not configured and not on
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "vtherm1",
|
||||||
|
"is_configured": False,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 13,
|
||||||
|
"target_temperature": 12,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm2",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": False,
|
||||||
|
"current_temperature": 18,
|
||||||
|
"target_temperature": 12,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm3",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 12,
|
||||||
|
"target_temperature": 18,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["vtherm3"],
|
||||||
|
),
|
||||||
|
# None current_temperature are in last
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "vtherm1",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 13,
|
||||||
|
"target_temperature": 12,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm2",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": None,
|
||||||
|
"target_temperature": 12,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm3",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 12,
|
||||||
|
"target_temperature": 18,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["vtherm1", "vtherm3", "vtherm2"],
|
||||||
|
),
|
||||||
|
# None target_temperature are in last
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "vtherm1",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 13,
|
||||||
|
"target_temperature": 12,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm2",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 18,
|
||||||
|
"target_temperature": None,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm3",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 12,
|
||||||
|
"target_temperature": 18,
|
||||||
|
"saved_target_temp": 18,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["vtherm1", "vtherm3", "vtherm2"],
|
||||||
|
),
|
||||||
|
# simple sort with overpowering detected
|
||||||
|
(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "vtherm1",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 13,
|
||||||
|
# "target_temperature": 12,
|
||||||
|
"saved_target_temp": 21,
|
||||||
|
"is_overpowering_detected": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm2",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 18,
|
||||||
|
# "target_temperature": 12,
|
||||||
|
"saved_target_temp": 17,
|
||||||
|
"is_overpowering_detected": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm3",
|
||||||
|
"is_configured": True,
|
||||||
|
"is_on": True,
|
||||||
|
"current_temperature": 12,
|
||||||
|
# "target_temperature": 18,
|
||||||
|
"saved_target_temp": 16,
|
||||||
|
"is_overpowering_detected": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["vtherm2", "vtherm3", "vtherm1"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_central_power_manageer_find_vtherms(
|
||||||
|
hass: HomeAssistant, vtherm_configs, results
|
||||||
|
):
|
||||||
|
"""Test the find_all_vtherm_with_power_management_sorted_by_dtemp"""
|
||||||
|
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||||
|
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||||
|
|
||||||
|
vtherms = []
|
||||||
|
for vtherm_config in vtherm_configs:
|
||||||
|
vtherm = MagicMock(spec=BaseThermostat)
|
||||||
|
vtherm.name = vtherm_config.get("name")
|
||||||
|
vtherm.is_on = vtherm_config.get("is_on")
|
||||||
|
vtherm.current_temperature = vtherm_config.get("current_temperature")
|
||||||
|
vtherm.target_temperature = vtherm_config.get("target_temperature")
|
||||||
|
vtherm.saved_target_temp = vtherm_config.get("saved_target_temp")
|
||||||
|
vtherm.power_manager.is_configured = vtherm_config.get("is_configured")
|
||||||
|
vtherm.power_manager.is_overpowering_detected = vtherm_config.get("is_overpowering_detected")
|
||||||
|
vtherms.append(vtherm)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.get_climate_components_entities",
|
||||||
|
return_value=vtherms,
|
||||||
|
):
|
||||||
|
vtherm_sorted = (
|
||||||
|
central_power_manager.find_all_vtherm_with_power_management_sorted_by_dtemp()
|
||||||
|
)
|
||||||
|
|
||||||
|
# extract results
|
||||||
|
vtherm_results = [vtherm.name for vtherm in vtherm_sorted]
|
||||||
|
|
||||||
|
assert vtherm_results == results
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"current_power, current_max_power, vtherm_configs, expected_results",
|
||||||
|
[
|
||||||
|
# simple nominal test (initialize overpowering state in VTherm)
|
||||||
|
(
|
||||||
|
1000,
|
||||||
|
5000,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "vtherm1",
|
||||||
|
"device_power": 100,
|
||||||
|
"is_device_active": False,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 1,
|
||||||
|
"on_percent": 0,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm2",
|
||||||
|
"device_power": 10000,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 4,
|
||||||
|
"on_percent": 100,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vtherm3",
|
||||||
|
"device_power": 5000,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": True,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
|
},
|
||||||
|
{"name": "vtherm4", "device_power": 1000, "is_device_active": True, "is_over_climate": True, "is_overpowering_detected": False, "overpowering_state": STATE_OFF},
|
||||||
|
],
|
||||||
|
# init vtherm1 to False
|
||||||
|
{"vtherm3": False, "vtherm2": False, "vtherm1": False},
|
||||||
|
),
|
||||||
|
# Un-shedding only (will be taken in reverse order)
|
||||||
|
(
|
||||||
|
1000,
|
||||||
|
2000,
|
||||||
|
[
|
||||||
|
# should be not unshedded (too much power will be added)
|
||||||
|
{
|
||||||
|
"name": "vtherm1",
|
||||||
|
"device_power": 800,
|
||||||
|
"is_device_active": False,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 1,
|
||||||
|
"on_percent": 1,
|
||||||
|
"is_overpowering_detected": True,
|
||||||
|
"overpowering_state": STATE_ON,
|
||||||
|
},
|
||||||
|
# already stay unshedded cause already unshedded
|
||||||
|
{
|
||||||
|
"name": "vtherm2",
|
||||||
|
"device_power": 100,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": True,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_OFF,
|
||||||
|
},
|
||||||
|
# should be unshedded
|
||||||
|
{
|
||||||
|
"name": "vtherm3",
|
||||||
|
"device_power": 200,
|
||||||
|
"is_device_active": False,
|
||||||
|
"is_over_climate": True,
|
||||||
|
"is_overpowering_detected": True,
|
||||||
|
"overpowering_state": STATE_ON,
|
||||||
|
},
|
||||||
|
# should be unshedded
|
||||||
|
{
|
||||||
|
"name": "vtherm4",
|
||||||
|
"device_power": 300,
|
||||||
|
"is_device_active": False,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 1,
|
||||||
|
"on_percent": 1,
|
||||||
|
"is_overpowering_detected": True,
|
||||||
|
"overpowering_state": STATE_ON,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{"vtherm4": False, "vtherm3": False},
|
||||||
|
),
|
||||||
|
# Shedding
|
||||||
|
(
|
||||||
|
2000,
|
||||||
|
1000,
|
||||||
|
[
|
||||||
|
# should be overpowering
|
||||||
|
{
|
||||||
|
"name": "vtherm1",
|
||||||
|
"device_power": 300,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 1,
|
||||||
|
"on_percent": 1,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_OFF,
|
||||||
|
},
|
||||||
|
# should be overpowering with many underlmying entities
|
||||||
|
{
|
||||||
|
"name": "vtherm2",
|
||||||
|
"device_power": 400,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 4,
|
||||||
|
"on_percent": 0.1,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
|
},
|
||||||
|
# over_climate should be overpowering
|
||||||
|
{
|
||||||
|
"name": "vtherm3",
|
||||||
|
"device_power": 100,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": True,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_OFF,
|
||||||
|
},
|
||||||
|
# should pass cause not active
|
||||||
|
{
|
||||||
|
"name": "vtherm4",
|
||||||
|
"device_power": 800,
|
||||||
|
"is_device_active": False,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 1,
|
||||||
|
"on_percent": 1,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
},
|
||||||
|
# should be not overpowering (already overpowering)
|
||||||
|
{
|
||||||
|
"name": "vtherm5",
|
||||||
|
"device_power": 400,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 4,
|
||||||
|
"on_percent": 0.1,
|
||||||
|
"is_overpowering_detected": True,
|
||||||
|
"overpowering_state": STATE_ON,
|
||||||
|
},
|
||||||
|
# should be overpowering with many underluying entities
|
||||||
|
{
|
||||||
|
"name": "vtherm6",
|
||||||
|
"device_power": 400,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": False,
|
||||||
|
"nb_underlying_entities": 4,
|
||||||
|
"on_percent": 0.1,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
|
},
|
||||||
|
# should not be overpowering (we have enough)
|
||||||
|
{
|
||||||
|
"name": "vtherm7",
|
||||||
|
"device_power": 1000,
|
||||||
|
"is_device_active": True,
|
||||||
|
"is_over_climate": True,
|
||||||
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm6": True},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
# @pytest.mark.skip
|
||||||
|
async def test_central_power_manageer_calculate_shedding(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
current_power,
|
||||||
|
current_max_power,
|
||||||
|
vtherm_configs,
|
||||||
|
expected_results,
|
||||||
|
):
|
||||||
|
"""Test the calculate_shedding of the CentralPowerManager"""
|
||||||
|
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||||
|
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||||
|
|
||||||
|
registered_calls = {}
|
||||||
|
|
||||||
|
def register_call(vtherm, overpowering):
|
||||||
|
"""Register a call to set_overpowering"""
|
||||||
|
registered_calls.update({vtherm.name: overpowering})
|
||||||
|
|
||||||
|
vtherms = []
|
||||||
|
for vtherm_config in vtherm_configs:
|
||||||
|
vtherm = MagicMock(spec=BaseThermostat)
|
||||||
|
vtherm.name = vtherm_config.get("name")
|
||||||
|
vtherm.is_device_active = vtherm_config.get("is_device_active")
|
||||||
|
vtherm.is_over_climate = vtherm_config.get("is_over_climate")
|
||||||
|
vtherm.nb_underlying_entities = vtherm_config.get("nb_underlying_entities")
|
||||||
|
if not vtherm_config.get("is_over_climate"):
|
||||||
|
vtherm.proportional_algorithm = MagicMock()
|
||||||
|
vtherm.on_percent = vtherm.proportional_algorithm.on_percent = vtherm_config.get("on_percent")
|
||||||
|
else:
|
||||||
|
vtherm.on_percent = None
|
||||||
|
vtherm.proportional_algorithm = None
|
||||||
|
|
||||||
|
vtherm.power_manager = MagicMock(spec=FeaturePowerManager)
|
||||||
|
vtherm.power_manager._vtherm = vtherm
|
||||||
|
|
||||||
|
vtherm.power_manager.is_overpowering_detected = vtherm_config.get(
|
||||||
|
"is_overpowering_detected"
|
||||||
|
)
|
||||||
|
vtherm.power_manager.device_power = vtherm_config.get("device_power")
|
||||||
|
vtherm.power_manager.overpowering_state = vtherm_config.get("overpowering_state")
|
||||||
|
|
||||||
|
async def mock_set_overpowering(
|
||||||
|
overpowering, power_consumption_max=0, v=vtherm
|
||||||
|
):
|
||||||
|
register_call(v, overpowering)
|
||||||
|
|
||||||
|
vtherm.power_manager.set_overpowering = mock_set_overpowering
|
||||||
|
|
||||||
|
vtherms.append(vtherm)
|
||||||
|
|
||||||
|
# fmt:off
|
||||||
|
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.find_all_vtherm_with_power_management_sorted_by_dtemp", return_value=vtherms), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=current_max_power), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=current_power), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.is_configured", new_callable=PropertyMock, return_value=True):
|
||||||
|
# fmt:on
|
||||||
|
|
||||||
|
await central_power_manager.calculate_shedding()
|
||||||
|
|
||||||
|
# Check registered calls
|
||||||
|
assert registered_calls == expected_results
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"dsecs, power, nb_call",
|
||||||
|
[
|
||||||
|
(0, 1000, 1),
|
||||||
|
(0, None, 0),
|
||||||
|
(0, STATE_UNAVAILABLE, 0),
|
||||||
|
(0, STATE_UNKNOWN, 0),
|
||||||
|
(21, 1000, 1),
|
||||||
|
(19, 1000, 1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_central_power_manager_power_event(
|
||||||
|
hass: HomeAssistant, dsecs, power, nb_call
|
||||||
|
):
|
||||||
|
"""Tests the Power sensor event"""
|
||||||
|
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||||
|
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||||
|
|
||||||
|
assert central_power_manager.current_power is None
|
||||||
|
assert central_power_manager.power_temperature is None
|
||||||
|
assert central_power_manager.name == "centralPowerManager"
|
||||||
|
|
||||||
|
# 2. post_init
|
||||||
|
central_power_manager.post_init(
|
||||||
|
{
|
||||||
|
CONF_POWER_SENSOR: "sensor.power_entity_id",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.max_power_entity_id",
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_PRESET_POWER: 13,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert central_power_manager.is_configured is True
|
||||||
|
assert central_power_manager.current_max_power is None
|
||||||
|
assert central_power_manager.current_power is None
|
||||||
|
assert central_power_manager.power_temperature == 13
|
||||||
|
|
||||||
|
# 3. start listening (not really useful but don't eat bread)
|
||||||
|
await central_power_manager.start_listening()
|
||||||
|
assert len(central_power_manager._active_listener) == 2
|
||||||
|
|
||||||
|
now: datetime = NowClass.get_now(hass)
|
||||||
|
# vtherm_api._set_now(now) vtherm_api is a MagicMock
|
||||||
|
vtherm_api.now = now
|
||||||
|
|
||||||
|
# 4. Call the _power_sensor_changed
|
||||||
|
side_effects = SideEffects(
|
||||||
|
{
|
||||||
|
"sensor.power_entity_id": State("sensor.power_entity_id", power),
|
||||||
|
"sensor.max_power_entity_id": State("sensor.max_power_entity_id", power),
|
||||||
|
},
|
||||||
|
State("unknown.entity_id", "unknown"),
|
||||||
|
)
|
||||||
|
# fmt:off
|
||||||
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
|
||||||
|
# fmt:on
|
||||||
|
# set a default value to see if it has been replaced
|
||||||
|
central_power_manager._current_power = -999
|
||||||
|
await central_power_manager._power_sensor_changed(event=Event(
|
||||||
|
event_type=EVENT_STATE_CHANGED,
|
||||||
|
data={
|
||||||
|
"entity_id": "sensor.power_entity_id",
|
||||||
|
"new_state": State("sensor.power_entity_id", power),
|
||||||
|
"old_state": State("sensor.power_entity_id", STATE_UNAVAILABLE),
|
||||||
|
}))
|
||||||
|
|
||||||
|
if nb_call > 0:
|
||||||
|
await central_power_manager._do_immediate_shedding()
|
||||||
|
|
||||||
|
expected_power = power if isinstance(power, (int, float)) else -999
|
||||||
|
assert central_power_manager.current_power == expected_power
|
||||||
|
assert mock_calculate_shedding.call_count == nb_call
|
||||||
|
|
||||||
|
# Do another call x seconds later
|
||||||
|
now = now + timedelta(seconds=dsecs)
|
||||||
|
vtherm_api.now = now
|
||||||
|
# fmt:off
|
||||||
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
|
||||||
|
# fmt:on
|
||||||
|
central_power_manager._current_power = -999
|
||||||
|
|
||||||
|
await central_power_manager._power_sensor_changed(event=Event(
|
||||||
|
event_type=EVENT_STATE_CHANGED,
|
||||||
|
data={
|
||||||
|
"entity_id": "sensor.power_entity_id",
|
||||||
|
"new_state": State("sensor.power_entity_id", power),
|
||||||
|
"old_state": State("sensor.power_entity_id", STATE_UNAVAILABLE),
|
||||||
|
}))
|
||||||
|
|
||||||
|
if nb_call > 0:
|
||||||
|
await central_power_manager._do_immediate_shedding()
|
||||||
|
|
||||||
|
assert central_power_manager.current_power == expected_power
|
||||||
|
assert mock_calculate_shedding.call_count == nb_call
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"dsecs, max_power, nb_call",
|
||||||
|
[
|
||||||
|
(0, 1000, 1),
|
||||||
|
(0, None, 0),
|
||||||
|
(0, STATE_UNAVAILABLE, 0),
|
||||||
|
(0, STATE_UNKNOWN, 0),
|
||||||
|
(21, 1000, 1),
|
||||||
|
(19, 1000, 1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_central_power_manager_max_power_event(
|
||||||
|
hass: HomeAssistant, dsecs, max_power, nb_call
|
||||||
|
):
|
||||||
|
"""Tests the Power sensor event"""
|
||||||
|
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
|
||||||
|
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
|
||||||
|
|
||||||
|
assert central_power_manager.current_power is None
|
||||||
|
assert central_power_manager.power_temperature is None
|
||||||
|
assert central_power_manager.name == "centralPowerManager"
|
||||||
|
|
||||||
|
# 2. post_init
|
||||||
|
central_power_manager.post_init(
|
||||||
|
{
|
||||||
|
CONF_POWER_SENSOR: "sensor.power_entity_id",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.max_power_entity_id",
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_PRESET_POWER: 13,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert central_power_manager.is_configured is True
|
||||||
|
assert central_power_manager.current_max_power is None
|
||||||
|
assert central_power_manager.current_power is None
|
||||||
|
assert central_power_manager.power_temperature == 13
|
||||||
|
|
||||||
|
# 3. start listening (not really useful but don't eat bread)
|
||||||
|
await central_power_manager.start_listening()
|
||||||
|
assert len(central_power_manager._active_listener) == 2
|
||||||
|
|
||||||
|
now: datetime = NowClass.get_now(hass)
|
||||||
|
# vtherm_api._set_now(now) vtherm_api is a MagicMock
|
||||||
|
vtherm_api.now = now
|
||||||
|
|
||||||
|
# 4. Call the _power_sensor_changed
|
||||||
|
side_effects = SideEffects(
|
||||||
|
{
|
||||||
|
"sensor.power_entity_id": State("sensor.power_entity_id", max_power),
|
||||||
|
"sensor.max_power_entity_id": State(
|
||||||
|
"sensor.max_power_entity_id", max_power
|
||||||
|
),
|
||||||
|
},
|
||||||
|
State("unknown.entity_id", "unknown"),
|
||||||
|
)
|
||||||
|
# fmt:off
|
||||||
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
|
||||||
|
# fmt:on
|
||||||
|
# set a default value to see if it has been replaced
|
||||||
|
central_power_manager._current_max_power = -999
|
||||||
|
await central_power_manager._power_sensor_changed(event=Event(
|
||||||
|
event_type=EVENT_STATE_CHANGED,
|
||||||
|
data={
|
||||||
|
"entity_id": "sensor.max_power_entity_id",
|
||||||
|
"new_state": State("sensor.max_power_entity_id", max_power),
|
||||||
|
"old_state": State("sensor.max_power_entity_id", STATE_UNAVAILABLE),
|
||||||
|
}))
|
||||||
|
|
||||||
|
if nb_call > 0:
|
||||||
|
await central_power_manager._do_immediate_shedding()
|
||||||
|
|
||||||
|
expected_power = max_power if isinstance(max_power, (int, float)) else -999
|
||||||
|
assert central_power_manager.current_max_power == expected_power
|
||||||
|
assert mock_calculate_shedding.call_count == nb_call
|
||||||
|
|
||||||
|
# Do another call x seconds later
|
||||||
|
now = now + timedelta(seconds=dsecs)
|
||||||
|
vtherm_api.now = now
|
||||||
|
# fmt:off
|
||||||
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
|
||||||
|
# fmt:on
|
||||||
|
central_power_manager._current_max_power = -999
|
||||||
|
|
||||||
|
await central_power_manager._power_sensor_changed(event=Event(
|
||||||
|
event_type=EVENT_STATE_CHANGED,
|
||||||
|
data={
|
||||||
|
"entity_id": "sensor.max_power_entity_id",
|
||||||
|
"new_state": State("sensor.max_power_entity_id", max_power),
|
||||||
|
"old_state": State("sensor.max_power_entity_id", STATE_UNAVAILABLE),
|
||||||
|
}))
|
||||||
|
|
||||||
|
if nb_call > 0:
|
||||||
|
await central_power_manager._do_immediate_shedding()
|
||||||
|
|
||||||
|
assert central_power_manager.current_max_power == expected_power
|
||||||
|
assert mock_calculate_shedding.call_count == nb_call
|
||||||
@@ -90,7 +90,7 @@ async def test_motion_feature_manager_refresh(
|
|||||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
motion_manager.start_listening()
|
await motion_manager.start_listening()
|
||||||
assert motion_manager.is_configured is True
|
assert motion_manager.is_configured is True
|
||||||
assert motion_manager.motion_state == STATE_UNKNOWN
|
assert motion_manager.motion_state == STATE_UNKNOWN
|
||||||
assert motion_manager.is_motion_detected is False
|
assert motion_manager.is_motion_detected is False
|
||||||
@@ -198,7 +198,7 @@ async def test_motion_feature_manager_event(
|
|||||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
motion_manager.start_listening()
|
await motion_manager.start_listening()
|
||||||
|
|
||||||
# 2. test _motion_sensor_changed with the parametrized
|
# 2. test _motion_sensor_changed with the parametrized
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
|||||||
@@ -721,10 +721,14 @@ async def test_multiple_climates_underlying_changes_not_aligned(
|
|||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_multiple_switch_power_management(
|
async def test_multiple_switch_power_management(
|
||||||
hass: HomeAssistant, skip_hass_states_is_state
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||||
):
|
):
|
||||||
"""Test the Power management"""
|
"""Test the Power management"""
|
||||||
|
temps = {
|
||||||
|
"eco": 17,
|
||||||
|
"comfort": 18,
|
||||||
|
"boost": 19,
|
||||||
|
}
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverSwitchMockName",
|
title="TheOverSwitchMockName",
|
||||||
@@ -737,17 +741,16 @@ async def test_multiple_switch_power_management(
|
|||||||
CONF_CYCLE_MIN: 8,
|
CONF_CYCLE_MIN: 8,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 19,
|
|
||||||
CONF_USE_WINDOW_FEATURE: False,
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
CONF_USE_PRESENCE_FEATURE: False,
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
CONF_HEATER: "switch.mock_switch1",
|
CONF_UNDERLYING_LIST: [
|
||||||
CONF_HEATER_2: "switch.mock_switch2",
|
"switch.mock_switch1",
|
||||||
CONF_HEATER_3: "switch.mock_switch3",
|
"switch.mock_switch2",
|
||||||
CONF_HEATER_4: "switch.mock_switch4",
|
"switch.mock_switch3",
|
||||||
|
"switch.mock_switch4",
|
||||||
|
],
|
||||||
CONF_HEATER_KEEP_ALIVE: 0,
|
CONF_HEATER_KEEP_ALIVE: 0,
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SAFETY_DELAY_MIN: 5,
|
CONF_SAFETY_DELAY_MIN: 5,
|
||||||
@@ -755,15 +758,13 @@ async def test_multiple_switch_power_management(
|
|||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
CONF_TPI_COEF_INT: 0.3,
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
CONF_TPI_COEF_EXT: 0.01,
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
||||||
CONF_DEVICE_POWER: 100,
|
CONF_DEVICE_POWER: 100,
|
||||||
CONF_PRESET_POWER: 12,
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: BaseThermostat = await create_thermostat(
|
entity: BaseThermostat = await create_thermostat(
|
||||||
hass, entry, "climate.theover4switchmockname"
|
hass, entry, "climate.theover4switchmockname", temps
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
assert entity.is_over_climate is False
|
assert entity.is_over_climate is False
|
||||||
@@ -772,6 +773,9 @@ async def test_multiple_switch_power_management(
|
|||||||
tpi_algo = entity._prop_algorithm
|
tpi_algo = entity._prop_algorithm
|
||||||
assert tpi_algo
|
assert tpi_algo
|
||||||
|
|
||||||
|
now: datetime = NowClass.get_now(hass)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
assert entity.hvac_mode is HVACMode.HEAT
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
@@ -779,77 +783,109 @@ async def test_multiple_switch_power_management(
|
|||||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||||
assert entity.target_temperature == 19
|
assert entity.target_temperature == 19
|
||||||
|
|
||||||
|
# make the heater heats
|
||||||
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
await send_ext_temperature_change_event(entity, 1, now)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# 1. Send power mesurement
|
# 1. Send power mesurement
|
||||||
await send_power_change_event(entity, 50, datetime.now())
|
side_effects = SideEffects(
|
||||||
|
{
|
||||||
|
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
||||||
|
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
|
||||||
|
},
|
||||||
|
State("unknown.entity_id", "unknown"),
|
||||||
|
)
|
||||||
|
|
||||||
# Send power max mesurement
|
# Send power max mesurement
|
||||||
await send_max_power_change_event(entity, 300, datetime.now())
|
# fmt:off
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||||
# All configuration is complete and power is < power_max
|
# fmt: on
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
now = now + timedelta(seconds=30)
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
|
await send_power_change_event(entity, 50, datetime.now())
|
||||||
|
await send_max_power_change_event(entity, 300, datetime.now())
|
||||||
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
|
# All configuration is complete and power is < power_max
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
|
|
||||||
# 2. Send power max mesurement too low and HVACMode is on
|
# 2. Send power max mesurement too low and HVACMode is on
|
||||||
with patch(
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
|
||||||
"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:
|
|
||||||
# 100 of the device / 4 -> 25, current power 50 so max is 75
|
|
||||||
await send_max_power_change_event(entity, 74, datetime.now())
|
|
||||||
assert await entity.power_manager.check_overpowering() is True
|
|
||||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
|
||||||
assert entity.preset_mode is PRESET_POWER
|
|
||||||
assert entity.power_manager.overpowering_state is STATE_ON
|
|
||||||
assert entity.target_temperature == 12
|
|
||||||
|
|
||||||
assert mock_send_event.call_count == 2
|
#fmt: off
|
||||||
mock_send_event.assert_has_calls(
|
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, \
|
||||||
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
|
||||||
call.send_event(
|
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
|
||||||
EventType.POWER_EVENT,
|
#fmt: on
|
||||||
{
|
now = now + timedelta(seconds=30)
|
||||||
"type": "start",
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
"current_power": 50,
|
|
||||||
"device_power": 100,
|
assert entity.power_percent > 0
|
||||||
"current_max_power": 74,
|
# 100 of the device / 4 -> 25, current power 50 so max is 75
|
||||||
"current_power_consumption": 25.0,
|
await send_max_power_change_event(entity, 49, datetime.now())
|
||||||
},
|
assert entity.power_manager.is_overpowering_detected is True
|
||||||
),
|
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||||
],
|
assert entity.preset_mode is PRESET_POWER
|
||||||
any_order=True,
|
assert entity.power_manager.overpowering_state is STATE_ON
|
||||||
)
|
assert entity.target_temperature == 12
|
||||||
assert mock_heater_on.call_count == 0
|
|
||||||
assert mock_heater_off.call_count == 4 # The fourth are shutdown
|
assert mock_send_event.call_count == 2
|
||||||
|
mock_send_event.assert_has_calls(
|
||||||
|
[
|
||||||
|
call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_POWER}),
|
||||||
|
call.send_event(
|
||||||
|
EventType.POWER_EVENT,
|
||||||
|
{
|
||||||
|
"type": "start",
|
||||||
|
"current_power": 50,
|
||||||
|
"device_power": 100,
|
||||||
|
"current_max_power": 49,
|
||||||
|
"current_power_consumption": 100,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
assert mock_heater_on.call_count == 0
|
||||||
|
assert mock_heater_off.call_count == 4 # The fourth are shutdown
|
||||||
|
|
||||||
# 3. change PRESET
|
# 3. change PRESET
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
) as mock_send_event:
|
) as mock_send_event:
|
||||||
await entity.async_set_preset_mode(PRESET_ECO)
|
now = now + timedelta(seconds=30)
|
||||||
assert entity.preset_mode is PRESET_ECO
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
# No change
|
|
||||||
assert entity.power_manager.overpowering_state is STATE_ON
|
await entity.async_set_preset_mode(PRESET_ECO)
|
||||||
|
assert entity.preset_mode is PRESET_ECO
|
||||||
|
# No change cause temperature is very low
|
||||||
|
assert entity.power_manager.overpowering_state is STATE_ON
|
||||||
|
|
||||||
# 4. Send hugh power max mesurement to release overpowering
|
# 4. Send hugh power max mesurement to release overpowering
|
||||||
with patch(
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 150))
|
||||||
"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:
|
|
||||||
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
|
|
||||||
await send_max_power_change_event(entity, 150, datetime.now())
|
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
|
||||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
|
||||||
assert entity.preset_mode is PRESET_ECO
|
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
|
||||||
assert entity.target_temperature == 17
|
|
||||||
|
|
||||||
assert (
|
with patch(
|
||||||
mock_heater_on.call_count == 0
|
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||||
) # The fourth are not restarted because temperature is enought
|
) as mock_send_event, patch(
|
||||||
assert mock_heater_off.call_count == 0
|
"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:
|
||||||
|
now = now + timedelta(seconds=30)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
|
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating
|
||||||
|
await send_max_power_change_event(entity, 150, datetime.now())
|
||||||
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
|
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||||
|
assert entity.preset_mode is PRESET_ECO
|
||||||
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
|
assert entity.target_temperature == 17
|
||||||
|
|
||||||
|
assert (
|
||||||
|
mock_heater_on.call_count == 0
|
||||||
|
) # The fourth are not restarted because temperature is enought
|
||||||
|
assert mock_heater_off.call_count == 0
|
||||||
|
|||||||
@@ -212,6 +212,13 @@ async def test_underlying_change_follow(
|
|||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
now: datetime = datetime.now(tz=tz)
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
PRESET_FROST_PROTECTION: 7,
|
||||||
|
PRESET_ECO: 16,
|
||||||
|
PRESET_COMFORT: 17,
|
||||||
|
PRESET_BOOST: 18,
|
||||||
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverClimateMockName",
|
title="TheOverClimateMockName",
|
||||||
@@ -232,7 +239,7 @@ async def test_underlying_change_follow(
|
|||||||
) as mock_find_climate, patch(
|
) as mock_find_climate, patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||||
) as mock_underlying_set_hvac_mode:
|
) as mock_underlying_set_hvac_mode:
|
||||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
|
||||||
|
|
||||||
assert entity
|
assert entity
|
||||||
assert entity.name == "TheOverClimateMockName"
|
assert entity.name == "TheOverClimateMockName"
|
||||||
@@ -354,6 +361,13 @@ async def test_underlying_change_not_follow(
|
|||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
now: datetime = datetime.now(tz=tz)
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
PRESET_FROST_PROTECTION: 7,
|
||||||
|
PRESET_ECO: 16,
|
||||||
|
PRESET_COMFORT: 17,
|
||||||
|
PRESET_BOOST: 18,
|
||||||
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverClimateMockName",
|
title="TheOverClimateMockName",
|
||||||
@@ -374,7 +388,7 @@ async def test_underlying_change_not_follow(
|
|||||||
) as mock_find_climate, patch(
|
) as mock_find_climate, patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||||
) as mock_underlying_set_hvac_mode:
|
) as mock_underlying_set_hvac_mode:
|
||||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
|
||||||
|
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
@@ -726,6 +740,13 @@ async def test_ignore_temp_outside_minmax_range(
|
|||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
now: datetime = datetime.now(tz=tz)
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
PRESET_FROST_PROTECTION: 7,
|
||||||
|
PRESET_ECO: 16,
|
||||||
|
PRESET_COMFORT: 17,
|
||||||
|
PRESET_BOOST: 18,
|
||||||
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverClimateMockName",
|
title="TheOverClimateMockName",
|
||||||
@@ -746,7 +767,7 @@ async def test_ignore_temp_outside_minmax_range(
|
|||||||
) as mock_find_climate, patch(
|
) as mock_find_climate, patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode"
|
||||||
) as mock_underlying_set_hvac_mode:
|
) as mock_underlying_set_hvac_mode:
|
||||||
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname")
|
entity = await create_thermostat(hass, entry, "climate.theoverclimatemockname", temps)
|
||||||
|
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from custom_components.versatile_thermostat.thermostat_switch import (
|
|||||||
from custom_components.versatile_thermostat.feature_power_manager import (
|
from custom_components.versatile_thermostat.feature_power_manager import (
|
||||||
FeaturePowerManager,
|
FeaturePowerManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
|
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
|
||||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
@@ -17,28 +18,28 @@ logging.getLogger().setLevel(logging.DEBUG)
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_over_climate, is_device_active, power, max_power, current_overpowering_state, overpowering_state, nb_call, changed, check_overpowering_ret",
|
"is_over_climate, is_device_active, power, max_power, check_power_available",
|
||||||
[
|
[
|
||||||
# don't switch to overpower (power is enough)
|
# don't switch to overpower (power is enough)
|
||||||
(False, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
|
(False, False, 1000, 3000, True),
|
||||||
# switch to overpower (power is not enough)
|
# switch to overpower (power is not enough)
|
||||||
(False, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
|
(False, False, 2000, 3000, False),
|
||||||
# don't switch to overpower (power is not enough but device is already on)
|
# don't switch to overpower (power is not enough but device is already on)
|
||||||
(False, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
|
(False, True, 2000, 3000, True),
|
||||||
# Same with a over_climate
|
# Same with a over_climate
|
||||||
# don't switch to overpower (power is enough)
|
# don't switch to overpower (power is enough)
|
||||||
(True, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
|
(True, False, 1000, 3000, True),
|
||||||
# switch to overpower (power is not enough)
|
# switch to overpower (power is not enough)
|
||||||
(True, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
|
(True, False, 2000, 3000, False),
|
||||||
# don't switch to overpower (power is not enough but device is already on)
|
# don't switch to overpower (power is not enough but device is already on)
|
||||||
(True, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
|
(True, True, 2000, 3000, True),
|
||||||
# Leave overpowering state
|
# Leave overpowering state
|
||||||
# switch to not overpower (power is enough)
|
# switch to not overpower (power is enough)
|
||||||
(False, False, 1000, 3000, STATE_ON, STATE_OFF, 1, True, False),
|
(False, False, 1000, 3000, True),
|
||||||
# don't switch to overpower (power is still not enough)
|
# don't switch to overpower (power is still not enough)
|
||||||
(False, False, 2000, 3000, STATE_ON, STATE_ON, 0, True, True),
|
(False, False, 2000, 3000, False),
|
||||||
# keep overpower (power is not enough but device is already on)
|
# keep overpower (power is not enough but device is already on)
|
||||||
(False, True, 3000, 3000, STATE_ON, STATE_ON, 0, True, True),
|
(False, True, 3000, 3000, False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_power_feature_manager(
|
async def test_power_feature_manager(
|
||||||
@@ -47,17 +48,15 @@ async def test_power_feature_manager(
|
|||||||
is_device_active,
|
is_device_active,
|
||||||
power,
|
power,
|
||||||
max_power,
|
max_power,
|
||||||
current_overpowering_state,
|
check_power_available,
|
||||||
overpowering_state,
|
|
||||||
nb_call,
|
|
||||||
changed,
|
|
||||||
check_overpowering_ret,
|
|
||||||
):
|
):
|
||||||
"""Test the FeaturePresenceManager class direclty"""
|
"""Test the FeaturePresenceManager class direclty"""
|
||||||
|
|
||||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||||
|
|
||||||
|
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||||
|
|
||||||
# 1. creation
|
# 1. creation
|
||||||
power_manager = FeaturePowerManager(fake_vtherm, hass)
|
power_manager = FeaturePowerManager(fake_vtherm, hass)
|
||||||
|
|
||||||
@@ -80,16 +79,27 @@ async def test_power_feature_manager(
|
|||||||
assert custom_attributes["current_max_power"] is None
|
assert custom_attributes["current_max_power"] is None
|
||||||
|
|
||||||
# 2. post_init
|
# 2. post_init
|
||||||
power_manager.post_init(
|
vtherm_api.find_central_configuration = MagicMock()
|
||||||
|
vtherm_api.central_power_manager.post_init(
|
||||||
{
|
{
|
||||||
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_PRESET_POWER: 13,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert vtherm_api.central_power_manager.is_configured
|
||||||
|
|
||||||
|
power_manager.post_init(
|
||||||
|
{
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
CONF_PRESET_POWER: 10,
|
CONF_PRESET_POWER: 10,
|
||||||
CONF_DEVICE_POWER: 1234,
|
CONF_DEVICE_POWER: 1234,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await power_manager.start_listening()
|
||||||
|
|
||||||
assert power_manager.is_configured is True
|
assert power_manager.is_configured is True
|
||||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||||
|
|
||||||
@@ -107,25 +117,18 @@ async def test_power_feature_manager(
|
|||||||
assert custom_attributes["current_max_power"] is None
|
assert custom_attributes["current_max_power"] is None
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
power_manager.start_listening()
|
await power_manager.start_listening()
|
||||||
assert power_manager.is_configured is True
|
assert power_manager.is_configured is True
|
||||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||||
|
|
||||||
assert len(power_manager._active_listener) == 2
|
assert len(power_manager._active_listener) == 0 # no more listening
|
||||||
|
|
||||||
# 4. test refresh and check_overpowering with the parametrized
|
# 4. test refresh and check_overpowering with the parametrized
|
||||||
side_effects = SideEffects(
|
|
||||||
{
|
|
||||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", power),
|
|
||||||
"sensor.the_max_power_sensor": State(
|
|
||||||
"sensor.the_max_power_sensor", max_power
|
|
||||||
),
|
|
||||||
},
|
|
||||||
State("unknown.entity_id", "unknown"),
|
|
||||||
)
|
|
||||||
# fmt:off
|
# fmt:off
|
||||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()) as mock_get_state:
|
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=max_power), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=power):
|
||||||
# fmt:on
|
# fmt:on
|
||||||
|
|
||||||
# Finish the mock configuration
|
# Finish the mock configuration
|
||||||
tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, "climate.vtherm")
|
tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, "climate.vtherm")
|
||||||
tpi_algo._on_percent = 1 # pylint: disable="protected-access"
|
tpi_algo._on_percent = 1 # pylint: disable="protected-access"
|
||||||
@@ -134,8 +137,84 @@ async def test_power_feature_manager(
|
|||||||
type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate)
|
type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate)
|
||||||
type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo)
|
type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo)
|
||||||
type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1)
|
type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1)
|
||||||
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
|
|
||||||
type(fake_vtherm)._saved_preset_mode = PropertyMock(return_value=PRESET_ECO)
|
ret = await power_manager.check_power_available()
|
||||||
|
assert ret == check_power_available
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"is_over_climate, current_overpowering_state, is_overpowering, new_overpowering_state, msg_sent",
|
||||||
|
[
|
||||||
|
# false -> false
|
||||||
|
(False, STATE_OFF, False, STATE_OFF, False),
|
||||||
|
# false -> true
|
||||||
|
(False, STATE_OFF, True, STATE_ON, True),
|
||||||
|
# true -> true
|
||||||
|
(False, STATE_ON, True, STATE_ON, False),
|
||||||
|
# true -> False
|
||||||
|
(False, STATE_ON, False, STATE_OFF, True),
|
||||||
|
# Same with over_climate
|
||||||
|
# false -> false
|
||||||
|
(True, STATE_OFF, False, STATE_OFF, False),
|
||||||
|
# false -> true
|
||||||
|
(True, STATE_OFF, True, STATE_ON, True),
|
||||||
|
# true -> true
|
||||||
|
(True, STATE_ON, True, STATE_ON, False),
|
||||||
|
# true -> False
|
||||||
|
(True, STATE_ON, False, STATE_OFF, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_power_feature_manager_set_overpowering(
|
||||||
|
hass,
|
||||||
|
is_over_climate,
|
||||||
|
current_overpowering_state,
|
||||||
|
is_overpowering,
|
||||||
|
new_overpowering_state,
|
||||||
|
msg_sent,
|
||||||
|
):
|
||||||
|
"""Test the set_overpowering method of FeaturePowerManager"""
|
||||||
|
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||||
|
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||||
|
|
||||||
|
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
|
||||||
|
|
||||||
|
# 1. creation / init
|
||||||
|
power_manager = FeaturePowerManager(fake_vtherm, hass)
|
||||||
|
vtherm_api.find_central_configuration = MagicMock()
|
||||||
|
vtherm_api.central_power_manager.post_init(
|
||||||
|
{
|
||||||
|
CONF_POWER_SENSOR: "sensor.the_power_sensor",
|
||||||
|
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_PRESET_POWER: 13,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert vtherm_api.central_power_manager.is_configured
|
||||||
|
|
||||||
|
power_manager.post_init(
|
||||||
|
{
|
||||||
|
CONF_USE_POWER_FEATURE: True,
|
||||||
|
CONF_PRESET_POWER: 10,
|
||||||
|
CONF_DEVICE_POWER: 1234,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await power_manager.start_listening()
|
||||||
|
|
||||||
|
assert power_manager.is_configured is True
|
||||||
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
# check overpowering
|
||||||
|
power_manager._overpowering_state = current_overpowering_state
|
||||||
|
|
||||||
|
# fmt:off
|
||||||
|
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=2000), \
|
||||||
|
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=1000):
|
||||||
|
# fmt:on
|
||||||
|
# Finish mocking
|
||||||
|
fake_vtherm.is_over_climate = is_over_climate
|
||||||
|
fake_vtherm.preset_mode = MagicMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
|
||||||
|
fake_vtherm._saved_preset_mode = PRESET_ECO
|
||||||
|
|
||||||
fake_vtherm.save_hvac_mode = MagicMock()
|
fake_vtherm.save_hvac_mode = MagicMock()
|
||||||
fake_vtherm.restore_hvac_mode = AsyncMock()
|
fake_vtherm.restore_hvac_mode = AsyncMock()
|
||||||
@@ -147,26 +226,17 @@ async def test_power_feature_manager(
|
|||||||
fake_vtherm.update_custom_attributes = MagicMock()
|
fake_vtherm.update_custom_attributes = MagicMock()
|
||||||
|
|
||||||
|
|
||||||
ret = await power_manager.refresh_state()
|
# Call set_overpowering
|
||||||
assert ret == changed
|
await power_manager.set_overpowering(is_overpowering, 1234)
|
||||||
assert power_manager.is_configured is True
|
|
||||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
|
||||||
assert power_manager.current_power == power
|
|
||||||
assert power_manager.current_max_power == max_power
|
|
||||||
|
|
||||||
# check overpowering
|
assert power_manager.overpowering_state == new_overpowering_state
|
||||||
power_manager._overpowering_state = current_overpowering_state
|
|
||||||
ret2 = await power_manager.check_overpowering()
|
|
||||||
assert ret2 == check_overpowering_ret
|
|
||||||
assert power_manager.overpowering_state == overpowering_state
|
|
||||||
assert mock_get_state.call_count == 2
|
|
||||||
|
|
||||||
if power_manager.overpowering_state == STATE_OFF:
|
if not is_overpowering:
|
||||||
|
assert power_manager.overpowering_state == STATE_OFF
|
||||||
assert fake_vtherm.save_hvac_mode.call_count == 0
|
assert fake_vtherm.save_hvac_mode.call_count == 0
|
||||||
assert fake_vtherm.save_preset_mode.call_count == 0
|
assert fake_vtherm.save_preset_mode.call_count == 0
|
||||||
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0
|
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0
|
||||||
assert fake_vtherm.async_set_preset_mode_internal.call_count == 0
|
assert fake_vtherm.async_set_preset_mode_internal.call_count == 0
|
||||||
assert fake_vtherm.send_event.call_count == nb_call
|
|
||||||
|
|
||||||
if current_overpowering_state == STATE_ON:
|
if current_overpowering_state == STATE_ON:
|
||||||
assert fake_vtherm.update_custom_attributes.call_count == 1
|
assert fake_vtherm.update_custom_attributes.call_count == 1
|
||||||
@@ -178,18 +248,24 @@ async def test_power_feature_manager(
|
|||||||
else:
|
else:
|
||||||
assert fake_vtherm.update_custom_attributes.call_count == 0
|
assert fake_vtherm.update_custom_attributes.call_count == 0
|
||||||
|
|
||||||
if nb_call == 1:
|
if msg_sent:
|
||||||
fake_vtherm.send_event.assert_has_calls(
|
fake_vtherm.send_event.assert_has_calls(
|
||||||
[
|
[
|
||||||
call.fake_vtherm.send_event(
|
call.fake_vtherm.send_event(
|
||||||
EventType.POWER_EVENT,
|
EventType.POWER_EVENT,
|
||||||
{'type': 'end', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power}),
|
{
|
||||||
|
"type": "end",
|
||||||
|
"current_power": 1000,
|
||||||
|
"device_power": 1234,
|
||||||
|
"current_max_power": 2000,
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
# is_overpowering is True
|
||||||
|
else:
|
||||||
elif power_manager.overpowering_state == STATE_ON:
|
assert power_manager.overpowering_state == STATE_ON
|
||||||
if is_over_climate:
|
if is_over_climate and current_overpowering_state == STATE_OFF:
|
||||||
assert fake_vtherm.save_hvac_mode.call_count == 1
|
assert fake_vtherm.save_hvac_mode.call_count == 1
|
||||||
else:
|
else:
|
||||||
assert fake_vtherm.save_hvac_mode.call_count == 0
|
assert fake_vtherm.save_hvac_mode.call_count == 0
|
||||||
@@ -209,30 +285,37 @@ async def test_power_feature_manager(
|
|||||||
assert fake_vtherm.restore_hvac_mode.call_count == 0
|
assert fake_vtherm.restore_hvac_mode.call_count == 0
|
||||||
assert fake_vtherm.restore_preset_mode.call_count == 0
|
assert fake_vtherm.restore_preset_mode.call_count == 0
|
||||||
|
|
||||||
if nb_call == 1:
|
if msg_sent:
|
||||||
fake_vtherm.send_event.assert_has_calls(
|
fake_vtherm.send_event.assert_has_calls(
|
||||||
[
|
[
|
||||||
call.fake_vtherm.send_event(
|
call.fake_vtherm.send_event(
|
||||||
EventType.POWER_EVENT,
|
EventType.POWER_EVENT,
|
||||||
{'type': 'start', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power, 'current_power_consumption': 1234.0}),
|
{
|
||||||
|
"type": "start",
|
||||||
|
"current_power": 1000,
|
||||||
|
"device_power": 1234,
|
||||||
|
"current_max_power": 2000,
|
||||||
|
"current_power_consumption": 1234.0,
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
fake_vtherm.reset_mock()
|
fake_vtherm.reset_mock()
|
||||||
|
|
||||||
# 5. Check custom_attributes
|
# 5. Check custom_attributes
|
||||||
custom_attributes = {}
|
custom_attributes = {}
|
||||||
power_manager.add_custom_attributes(custom_attributes)
|
power_manager.add_custom_attributes(custom_attributes)
|
||||||
assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor"
|
assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor"
|
||||||
assert (
|
assert (
|
||||||
custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
|
custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
|
||||||
)
|
)
|
||||||
assert custom_attributes["overpowering_state"] == overpowering_state
|
assert custom_attributes["overpowering_state"] == new_overpowering_state
|
||||||
assert custom_attributes["is_power_configured"] is True
|
assert custom_attributes["is_power_configured"] is True
|
||||||
assert custom_attributes["device_power"] == 1234
|
assert custom_attributes["device_power"] == 1234
|
||||||
assert custom_attributes["power_temp"] == 10
|
assert custom_attributes["power_temp"] == 10
|
||||||
assert custom_attributes["current_power"] == power
|
assert custom_attributes["current_power"] == 1000
|
||||||
assert custom_attributes["current_max_power"] == max_power
|
assert custom_attributes["current_max_power"] == 2000
|
||||||
|
|
||||||
power_manager.stop_listening()
|
power_manager.stop_listening()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@@ -241,10 +324,15 @@ async def test_power_feature_manager(
|
|||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_power_management_hvac_off(
|
async def test_power_management_hvac_off(
|
||||||
hass: HomeAssistant, skip_hass_states_is_state
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||||
):
|
):
|
||||||
"""Test the Power management"""
|
"""Test the Power management"""
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
"eco": 17,
|
||||||
|
"comfort": 18,
|
||||||
|
"boost": 19,
|
||||||
|
}
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverSwitchMockName",
|
title="TheOverSwitchMockName",
|
||||||
@@ -257,29 +345,24 @@ async def test_power_management_hvac_off(
|
|||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 19,
|
|
||||||
CONF_USE_WINDOW_FEATURE: False,
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
CONF_USE_PRESENCE_FEATURE: False,
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
CONF_HEATER: "switch.mock_switch",
|
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
CONF_TPI_COEF_INT: 0.3,
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
CONF_TPI_COEF_EXT: 0.01,
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SAFETY_DELAY_MIN: 5,
|
CONF_SAFETY_DELAY_MIN: 5,
|
||||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
||||||
CONF_DEVICE_POWER: 100,
|
CONF_DEVICE_POWER: 100,
|
||||||
CONF_PRESET_POWER: 12,
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: ThermostatOverSwitch = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname", temps
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
@@ -292,34 +375,53 @@ async def test_power_management_hvac_off(
|
|||||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||||
assert entity.hvac_mode == HVACMode.OFF
|
assert entity.hvac_mode == HVACMode.OFF
|
||||||
|
|
||||||
|
now: datetime = NowClass.get_now(hass)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
# Send power mesurement
|
# Send power mesurement
|
||||||
await send_power_change_event(entity, 50, datetime.now())
|
# fmt:off
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
side_effects = SideEffects(
|
||||||
|
{
|
||||||
|
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
||||||
|
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
|
||||||
|
},
|
||||||
|
State("unknown.entity_id", "unknown"),
|
||||||
|
)
|
||||||
|
# fmt:off
|
||||||
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||||
|
# fmt: on
|
||||||
|
await send_power_change_event(entity, 50, now)
|
||||||
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
|
|
||||||
# All configuration is not complete
|
# All configuration is not complete
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN # due to hvac_off
|
||||||
|
|
||||||
# Send power max mesurement
|
# Send power max mesurement
|
||||||
await send_max_power_change_event(entity, 300, datetime.now())
|
now = now + timedelta(seconds=30)
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
# All configuration is complete and power is < power_max
|
await send_max_power_change_event(entity, 300, now)
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
# All configuration is complete and power is < power_max
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN # # due to hvac_off
|
||||||
|
|
||||||
# Send power max mesurement too low but HVACMode is off
|
# Send power max mesurement too low but HVACMode is off
|
||||||
with patch(
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
# fmt:off
|
||||||
) as mock_send_event, patch(
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||||
) as mock_heater_on, patch(
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
|
||||||
) as mock_heater_off:
|
# fmt: on
|
||||||
|
now = now + timedelta(seconds=30)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
await send_max_power_change_event(entity, 149, datetime.now())
|
await send_max_power_change_event(entity, 149, datetime.now())
|
||||||
assert await entity.power_manager.check_overpowering() is True
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
|
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
assert entity.power_manager.overpowering_state is STATE_ON
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||||
|
|
||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
assert mock_heater_on.call_count == 0
|
assert mock_heater_on.call_count == 0
|
||||||
@@ -328,9 +430,17 @@ async def test_power_management_hvac_off(
|
|||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
|
async def test_power_management_hvac_on(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||||
|
):
|
||||||
"""Test the Power management"""
|
"""Test the Power management"""
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
"eco": 17,
|
||||||
|
"comfort": 18,
|
||||||
|
"boost": 19,
|
||||||
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverSwitchMockName",
|
title="TheOverSwitchMockName",
|
||||||
@@ -343,32 +453,30 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
|||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 19,
|
|
||||||
CONF_USE_WINDOW_FEATURE: False,
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
CONF_USE_PRESENCE_FEATURE: False,
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
CONF_HEATER: "switch.mock_switch",
|
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
CONF_TPI_COEF_INT: 0.3,
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
CONF_TPI_COEF_EXT: 0.01,
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SAFETY_DELAY_MIN: 5,
|
CONF_SAFETY_DELAY_MIN: 5,
|
||||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
||||||
CONF_DEVICE_POWER: 100,
|
CONF_DEVICE_POWER: 100,
|
||||||
CONF_PRESET_POWER: 12,
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: ThermostatOverSwitch = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname", temps
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
|
now: datetime = NowClass.get_now(hass)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
tpi_algo = entity._prop_algorithm
|
tpi_algo = entity._prop_algorithm
|
||||||
assert tpi_algo
|
assert tpi_algo
|
||||||
|
|
||||||
@@ -379,25 +487,49 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
|||||||
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||||
assert entity.target_temperature == 19
|
assert entity.target_temperature == 19
|
||||||
|
|
||||||
|
# make the heater heats
|
||||||
|
await send_temperature_change_event(entity, 15, now)
|
||||||
|
await send_ext_temperature_change_event(entity, 1, now)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity.power_percent > 0
|
||||||
|
|
||||||
# Send power mesurement
|
# Send power mesurement
|
||||||
await send_power_change_event(entity, 50, datetime.now())
|
side_effects = SideEffects(
|
||||||
# Send power max mesurement
|
{
|
||||||
await send_max_power_change_event(entity, 300, datetime.now())
|
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
|
||||||
# All configuration is complete and power is < power_max
|
},
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
State("unknown.entity_id", "unknown"),
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
)
|
||||||
|
# fmt:off
|
||||||
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||||
|
# fmt: on
|
||||||
|
await send_power_change_event(entity, 50, datetime.now())
|
||||||
|
# Send power max mesurement
|
||||||
|
now = now + timedelta(seconds=30)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
await send_max_power_change_event(entity, 300, datetime.now())
|
||||||
|
|
||||||
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
|
# All configuration is complete and power is < power_max
|
||||||
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
|
|
||||||
# Send power max mesurement too low and HVACMode is on
|
# Send power max mesurement too low and HVACMode is on
|
||||||
with patch(
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
# fmt:off
|
||||||
) as mock_send_event, patch(
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||||
) as mock_heater_on, patch(
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
|
||||||
) as mock_heater_off:
|
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
|
||||||
await send_max_power_change_event(entity, 149, datetime.now())
|
# fmt: on
|
||||||
assert await entity.power_manager.check_overpowering() is True
|
now = now + timedelta(seconds=30)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
|
await send_max_power_change_event(entity, 49, now)
|
||||||
|
assert entity.power_manager.is_overpowering_detected is True
|
||||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||||
assert entity.preset_mode is PRESET_POWER
|
assert entity.preset_mode is PRESET_POWER
|
||||||
assert entity.power_manager.overpowering_state is STATE_ON
|
assert entity.power_manager.overpowering_state is STATE_ON
|
||||||
@@ -413,7 +545,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
|||||||
"type": "start",
|
"type": "start",
|
||||||
"current_power": 50,
|
"current_power": 50,
|
||||||
"device_power": 100,
|
"device_power": 100,
|
||||||
"current_max_power": 149,
|
"current_max_power": 49,
|
||||||
"current_power_consumption": 100.0,
|
"current_power_consumption": 100.0,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -423,16 +555,20 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
|||||||
assert mock_heater_on.call_count == 0
|
assert mock_heater_on.call_count == 0
|
||||||
assert mock_heater_off.call_count == 1
|
assert mock_heater_off.call_count == 1
|
||||||
|
|
||||||
# Send power mesurement low to unseet power preset
|
# Send power mesurement low to unset power preset
|
||||||
with patch(
|
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 48))
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
|
||||||
) as mock_send_event, patch(
|
# fmt:off
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
||||||
) as mock_heater_on, patch(
|
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
|
||||||
) as mock_heater_off:
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
|
||||||
await send_power_change_event(entity, 48, datetime.now())
|
# fmt: on
|
||||||
assert await entity.power_manager.check_overpowering() is False
|
now = now + timedelta(seconds=30)
|
||||||
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
|
await send_power_change_event(entity, 48, now)
|
||||||
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
# All configuration is complete and power is < power_max, we restore previous preset
|
# All configuration is complete and power is < power_max, we restore previous preset
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
@@ -462,10 +598,16 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
|||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
async def test_power_management_energy_over_switch(
|
async def test_power_management_energy_over_switch(
|
||||||
hass: HomeAssistant, skip_hass_states_is_state
|
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
|
||||||
):
|
):
|
||||||
"""Test the Power management energy mesurement"""
|
"""Test the Power management energy mesurement"""
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
"eco": 17,
|
||||||
|
"comfort": 18,
|
||||||
|
"boost": 19,
|
||||||
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverSwitchMockName",
|
title="TheOverSwitchMockName",
|
||||||
@@ -478,30 +620,24 @@ async def test_power_management_energy_over_switch(
|
|||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 19,
|
|
||||||
CONF_USE_WINDOW_FEATURE: False,
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
CONF_USE_PRESENCE_FEATURE: False,
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
CONF_HEATER: "switch.mock_switch",
|
CONF_UNDERLYING_LIST: ["switch.mock_switch", "switch.mock_switch2"],
|
||||||
CONF_HEATER_2: "switch.mock_switch2",
|
|
||||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
CONF_TPI_COEF_INT: 0.3,
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
CONF_TPI_COEF_EXT: 0.01,
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SAFETY_DELAY_MIN: 5,
|
CONF_SAFETY_DELAY_MIN: 5,
|
||||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
||||||
CONF_DEVICE_POWER: 100,
|
CONF_DEVICE_POWER: 100,
|
||||||
CONF_PRESET_POWER: 12,
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: ThermostatOverSwitch = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverswitchmockname"
|
hass, entry, "climate.theoverswitchmockname", temps
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
|
|
||||||
@@ -523,6 +659,8 @@ async def test_power_management_energy_over_switch(
|
|||||||
await entity.async_set_preset_mode(PRESET_BOOST)
|
await entity.async_set_preset_mode(PRESET_BOOST)
|
||||||
await send_temperature_change_event(entity, 15, datetime.now())
|
await send_temperature_change_event(entity, 15, datetime.now())
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert entity.hvac_mode is HVACMode.HEAT
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
assert entity.target_temperature == 19
|
assert entity.target_temperature == 19
|
||||||
@@ -594,6 +732,12 @@ async def test_power_management_energy_over_climate(
|
|||||||
):
|
):
|
||||||
"""Test the Power management for a over_climate thermostat"""
|
"""Test the Power management for a over_climate thermostat"""
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
"eco": 17,
|
||||||
|
"comfort": 18,
|
||||||
|
"boost": 19,
|
||||||
|
}
|
||||||
|
|
||||||
the_mock_underlying = MagicMockClimate()
|
the_mock_underlying = MagicMockClimate()
|
||||||
with patch(
|
with patch(
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
|
||||||
@@ -611,26 +755,21 @@ async def test_power_management_energy_over_climate(
|
|||||||
CONF_CYCLE_MIN: 5,
|
CONF_CYCLE_MIN: 5,
|
||||||
CONF_TEMP_MIN: 15,
|
CONF_TEMP_MIN: 15,
|
||||||
CONF_TEMP_MAX: 30,
|
CONF_TEMP_MAX: 30,
|
||||||
"eco_temp": 17,
|
|
||||||
"comfort_temp": 18,
|
|
||||||
"boost_temp": 19,
|
|
||||||
CONF_USE_WINDOW_FEATURE: False,
|
CONF_USE_WINDOW_FEATURE: False,
|
||||||
CONF_USE_MOTION_FEATURE: False,
|
CONF_USE_MOTION_FEATURE: False,
|
||||||
CONF_USE_POWER_FEATURE: True,
|
CONF_USE_POWER_FEATURE: True,
|
||||||
CONF_USE_PRESENCE_FEATURE: False,
|
CONF_USE_PRESENCE_FEATURE: False,
|
||||||
CONF_CLIMATE: "climate.mock_climate",
|
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
|
||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SAFETY_DELAY_MIN: 5,
|
CONF_SAFETY_DELAY_MIN: 5,
|
||||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
||||||
CONF_DEVICE_POWER: 100,
|
CONF_DEVICE_POWER: 100,
|
||||||
CONF_PRESET_POWER: 12,
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
entity: ThermostatOverSwitch = await create_thermostat(
|
entity: ThermostatOverSwitch = await create_thermostat(
|
||||||
hass, entry, "climate.theoverclimatemockname"
|
hass, entry, "climate.theoverclimatemockname", temps
|
||||||
)
|
)
|
||||||
assert entity
|
assert entity
|
||||||
assert entity.is_over_climate
|
assert entity.is_over_climate
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ async def test_presence_feature_manager(
|
|||||||
assert custom_attributes["is_presence_configured"] is True
|
assert custom_attributes["is_presence_configured"] is True
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
presence_manager.start_listening()
|
await presence_manager.start_listening()
|
||||||
assert presence_manager.is_configured is True
|
assert presence_manager.is_configured is True
|
||||||
assert presence_manager.presence_state == STATE_UNKNOWN
|
assert presence_manager.presence_state == STATE_UNKNOWN
|
||||||
assert presence_manager.is_absence_detected is False
|
assert presence_manager.is_absence_detected is False
|
||||||
|
|||||||
@@ -224,8 +224,6 @@ async def test_sensors_over_climate(
|
|||||||
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
CONF_SAFETY_DELAY_MIN: 5,
|
CONF_SAFETY_DELAY_MIN: 5,
|
||||||
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
|
||||||
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
|
|
||||||
CONF_DEVICE_POWER: 1.5,
|
CONF_DEVICE_POWER: 1.5,
|
||||||
CONF_PRESET_POWER: 12,
|
CONF_PRESET_POWER: 12,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,23 @@ async def test_over_switch_ac_full_start(
|
|||||||
): # pylint: disable=unused-argument
|
): # pylint: disable=unused-argument
|
||||||
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
|
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
|
||||||
|
|
||||||
|
temps = {
|
||||||
|
PRESET_FROST_PROTECTION: 7,
|
||||||
|
PRESET_ECO: 17,
|
||||||
|
PRESET_COMFORT: 19,
|
||||||
|
PRESET_BOOST: 20,
|
||||||
|
PRESET_ECO + PRESET_AC_SUFFIX: 25,
|
||||||
|
PRESET_COMFORT + PRESET_AC_SUFFIX: 23,
|
||||||
|
PRESET_BOOST + PRESET_AC_SUFFIX: 21,
|
||||||
|
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX: 7,
|
||||||
|
PRESET_ECO + PRESET_AWAY_SUFFIX: 16,
|
||||||
|
PRESET_COMFORT + PRESET_AWAY_SUFFIX: 17,
|
||||||
|
PRESET_BOOST + PRESET_AWAY_SUFFIX: 18,
|
||||||
|
PRESET_ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX: 27,
|
||||||
|
PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX: 26,
|
||||||
|
PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX: 25,
|
||||||
|
}
|
||||||
|
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
title="TheOverSwitchACMockName",
|
title="TheOverSwitchACMockName",
|
||||||
@@ -57,21 +74,7 @@ async def test_over_switch_ac_full_start(
|
|||||||
assert isinstance(entity, ThermostatOverSwitch)
|
assert isinstance(entity, ThermostatOverSwitch)
|
||||||
|
|
||||||
# Initialise the preset temp
|
# Initialise the preset temp
|
||||||
await set_climate_preset_temp(
|
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
|
||||||
entity, PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX, 7
|
|
||||||
)
|
|
||||||
await set_climate_preset_temp(entity, PRESET_ECO + PRESET_AWAY_SUFFIX, 16)
|
|
||||||
await set_climate_preset_temp(entity, PRESET_COMFORT + PRESET_AWAY_SUFFIX, 17)
|
|
||||||
await set_climate_preset_temp(entity, PRESET_BOOST + PRESET_AWAY_SUFFIX, 18)
|
|
||||||
await set_climate_preset_temp(
|
|
||||||
entity, PRESET_ECO + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 27
|
|
||||||
)
|
|
||||||
await set_climate_preset_temp(
|
|
||||||
entity, PRESET_COMFORT + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 26
|
|
||||||
)
|
|
||||||
await set_climate_preset_temp(
|
|
||||||
entity, PRESET_BOOST + PRESET_AC_SUFFIX + PRESET_AWAY_SUFFIX, 25
|
|
||||||
)
|
|
||||||
|
|
||||||
assert entity.name == "TheOverSwitchMockName"
|
assert entity.name == "TheOverSwitchMockName"
|
||||||
assert entity.is_over_climate is False # pylint: disable=protected-access
|
assert entity.is_over_climate is False # pylint: disable=protected-access
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ async def test_over_valve_full_start(
|
|||||||
CONF_MOTION_OFF_DELAY: 30,
|
CONF_MOTION_OFF_DELAY: 30,
|
||||||
CONF_MOTION_PRESET: PRESET_COMFORT,
|
CONF_MOTION_PRESET: PRESET_COMFORT,
|
||||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||||
CONF_POWER_SENSOR: "sensor.power_sensor",
|
|
||||||
CONF_MAX_POWER_SENSOR: "sensor.power_max_sensor",
|
|
||||||
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
CONF_PRESENCE_SENSOR: "person.presence_sensor",
|
||||||
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 7,
|
PRESET_FROST_PROTECTION + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 7,
|
||||||
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.1,
|
PRESET_ECO + PRESET_AWAY_SUFFIX + PRESET_TEMP_SUFFIX: 17.1,
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ async def test_window_feature_manager_refresh_sensor_action_turn_off(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
assert window_manager.is_configured is True
|
assert window_manager.is_configured is True
|
||||||
assert window_manager.window_state == STATE_UNKNOWN
|
assert window_manager.window_state == STATE_UNKNOWN
|
||||||
assert window_manager.window_auto_state == STATE_UNAVAILABLE
|
assert window_manager.window_auto_state == STATE_UNAVAILABLE
|
||||||
@@ -288,7 +288,7 @@ async def test_window_feature_manager_refresh_sensor_action_frost_only(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
assert window_manager.is_configured is True
|
assert window_manager.is_configured is True
|
||||||
assert window_manager.window_state == STATE_UNKNOWN
|
assert window_manager.window_state == STATE_UNKNOWN
|
||||||
assert window_manager.window_auto_state == STATE_UNAVAILABLE
|
assert window_manager.window_auto_state == STATE_UNAVAILABLE
|
||||||
@@ -408,7 +408,7 @@ async def test_window_feature_manager_sensor_event_action_turn_off(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
assert len(window_manager._active_listener) == 1
|
assert len(window_manager._active_listener) == 1
|
||||||
|
|
||||||
# 4. test refresh with the parametrized
|
# 4. test refresh with the parametrized
|
||||||
@@ -535,7 +535,7 @@ async def test_window_feature_manager_event_sensor_action_frost_only(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
|
|
||||||
# 4. test refresh with the parametrized
|
# 4. test refresh with the parametrized
|
||||||
# fmt:off
|
# fmt:off
|
||||||
@@ -660,7 +660,7 @@ async def test_window_feature_manager_window_auto(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert window_manager.is_window_auto_configured is True
|
assert window_manager.is_window_auto_configured is True
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
|
|
||||||
# 2. Call manage window auto
|
# 2. Call manage window auto
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
|||||||
Reference in New Issue
Block a user