Compare commits

..

15 Commits

Author SHA1 Message Date
Jean-Marc Collin
09e1b49af8 Typo 2024-12-31 18:15:15 +00:00
Jean-Marc Collin
e9cc5691af Add safety manager direct tests 2024-12-31 16:22:17 +00:00
Jean-Marc Collin
1c39ad670d Documentation and release 2024-12-31 15:50:06 +00:00
Jean-Marc Collin
6c91c197a1 Add safety feature_safety_manager
Rename config attribute from security_ to safety_
2024-12-31 15:42:33 +00:00
Jean-Marc Collin
7ec7d3a26a Add test_auto_start_stop feature manager. All tests ok 2024-12-27 17:49:09 +00:00
Jean-Marc Collin
9fc8f9c909 Fix all testus with feature_window_manager ok 2024-12-27 15:41:28 +00:00
Jean-Marc Collin
329598b95a All windows tests ok 2024-12-27 12:47:19 +00:00
Jean-Marc Collin
c83c772901 All tests Window Feature Manager ok. 2024-12-27 11:53:58 +00:00
Jean-Marc Collin
18248aee9e Tests ok. But tests are not complete 2024-12-26 20:00:51 +00:00
Jean-Marc Collin
a11eaef9f7 Add Motion manager. All tests ok 2024-12-26 15:42:29 +00:00
Jean-Marc Collin
887d59a08f Refactor power feature 2024-12-23 19:07:44 +00:00
Jean-Marc Collin
eb503c0a02 Fix presence test 2024-12-23 14:10:23 +00:00
Jean-Marc Collin
37ffacc5fd Python 3.13 2024-12-23 14:55:51 +01:00
Jean-Marc Collin
c99956f50c Add PresenceFeatureManager ok 2024-12-23 13:53:55 +00:00
Jean-Marc Collin
b41d0f34dc Refactor Presence Feature 2024-12-23 12:04:22 +00:00
23 changed files with 562 additions and 1828 deletions

View File

@@ -54,7 +54,6 @@
"python.analysis.autoSearchPaths": true,
"pylint.lintOnChange": false,
"python.formatting.provider": "black",
"python.formatting.blackArgs": ["--line-length", "180"],
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,

View File

@@ -1,2 +0,0 @@
[FORMAT]
max-line-length=180

View File

@@ -38,9 +38,8 @@ A big thank you to all my beer sponsors for their donations and encouragements.
The documentation is now divided into several pages for easier reading and searching:
1. [Introduction](documentation/en/presentation.md),
2. [Installation](documentation/en/installation.md),
3. [Choosing a VTherm type](documentation/en/creation.md),
4. [Basic attributes](documentation/en/base-attributes.md)
2. [Choosing a VTherm type](documentation/en/creation.md),
3. [Basic attributes](documentation/en/base-attributes.md)
3. [Configuring a VTherm on a `switch`](documentation/en/over-switch.md)
3. [Configuring a VTherm on a `climate`](documentation/en/over-climate.md)
3. [Configuring a VTherm on a valve](documentation/en/over-valve.md)
@@ -104,4 +103,4 @@ If you wish to contribute, please read the [contribution guidelines](CONTRIBUTIN
[license-shield]: https://img.shields.io/github/license/jmcollin78/versatile_thermostat.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/maintainer-Joakim%20Sørensen%20%40ludeeus-blue.svg?style=for-the-badge
[releases-shield]: https://img.shields.io/github/release/jmcollin78/versatile_thermostat.svg?style=for-the-badge
[releases]: https://github.com/jmcollin78/versatile_thermostat/releases
[releases]: https://github.com/jmcollin78/versatile_thermostat/releases

View File

@@ -3,8 +3,7 @@
import logging
from datetime import timedelta
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.climate import ClimateEntity, DOMAIN as CLIMATE_DOMAIN
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
@@ -114,6 +113,6 @@ class VersatileThermostatBaseEntity(Entity):
self, event: Event
): # pylint: disable=unused-argument
"""Called when my climate have change
This method aims to be overridden to take the status change
This method aims to be overriden to take the status change
"""
return

View File

@@ -16,10 +16,10 @@ _LOGGER = logging.getLogger(__name__)
class BaseFeatureManager:
"""A base class for all feature"""
def __init__(self, vtherm: Any, hass: HomeAssistant, name: str = None):
def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager"""
self._vtherm = vtherm
self._name = vtherm.name if vtherm else name
self._name = vtherm.name
self._active_listener: list[CALLBACK_TYPE] = []
self._hass = hass
@@ -38,10 +38,6 @@ class BaseFeatureManager:
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:
"""Add a listener to the list of active listener"""
self._active_listener.append(func)

View File

@@ -11,6 +11,7 @@ from homeassistant.core import (
callback,
Event,
State,
)
from homeassistant.components.climate import ClimateEntity
@@ -27,7 +28,7 @@ from homeassistant.helpers.event import (
)
from homeassistant.components.climate.const import (
from homeassistant.components.climate import (
ATTR_PRESET_MODE,
# ATTR_FAN_MODE,
HVACMode,
@@ -50,10 +51,11 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
STATE_ON,
)
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .commons import ConfigData, T, deprecated
from .commons import ConfigData, T
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -72,7 +74,6 @@ from .feature_safety_manager import FeatureSafetyManager
_LOGGER = logging.getLogger(__name__)
class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Representation of a base class for all Versatile Thermostat device."""
@@ -174,22 +175,21 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._total_energy = None
_LOGGER.debug("%s - _init_ resetting energy to None", self)
# Because energy of climate is calculated in the thermostat we have to keep
# that here and not in underlying entity
# because energy of climate is calculated in the thermostat we have to keep that here and not in underlying entity
self._underlying_climate_start_hvac_action_date = None
self._underlying_climate_delta_t = 0
self._current_tz = dt_util.get_time_zone(self._hass.config.time_zone)
# Last change time is the datetime of the last change sent by
# VTherm to the device it is used in `over_climate` when a
# state changes from underlying to avoid loops
# Last change time is the datetime of the last change sent by VTherm to the device
# it is used in `over_cliamte` when a state have change from underlying to avoid loops
self._last_change_time_from_vtherm = None
self._underlyings: list[T] = []
self._ema_temp = None
self._ema_algo = None
self._now = None
self._attr_fan_mode = None
@@ -208,7 +208,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_off_reason: HVAC_OFF_REASONS | None = None
# Instantiate all features manager
# Instanciate all features manager
self._managers: list[BaseFeatureManager] = []
self._presence_manager: FeaturePresenceManager = FeaturePresenceManager(
@@ -276,7 +276,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return entry_infos
def post_init(self, config_entry: ConfigData):
"""Finish the initialization of the thermostat"""
"""Finish the initialization of the thermostast"""
_LOGGER.info(
"%s - Updating VersatileThermostat with infos %s",
@@ -364,9 +364,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
and self._ext_temp_sensor_entity_id is None
):
_LOGGER.warning(
"Using TPI function but not external temperature sensor is set. "
"Removing the delta temp ext factor. "
"Thermostat will not be fully operational."
"Using TPI function but not external temperature sensor is set. Removing the delta temp ext factor. Thermostat will not be fully operationnal" # pylint: disable=line-too-long
)
self._tpi_coef_ext = 0
@@ -444,6 +442,10 @@ 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)
# issue 428. Link to others entities will start at link
@@ -469,17 +471,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
under.remove_entity()
async def async_startup(self, central_configuration):
"""Triggered on startup, used to get old state and set internal states
accordingly. This is triggered by VTherm API"""
"""Triggered on startup, used to get old state and set internal states accordingly. This is triggered by
VTherm API"""
_LOGGER.debug("%s - Calling async_startup", self)
_LOGGER.debug("%s - Calling async_startup_internal", self)
need_write_state = False
# start listening for all managers
for manager in self._managers:
manager.start_listening()
await self.get_my_previous_state()
await self.init_presets(central_configuration)
@@ -516,14 +514,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await self._async_update_ext_temp(ext_temperature_state)
else:
_LOGGER.debug(
"%s - external temperature sensor have NOT been retrieved "
"cause unknown or unavailable",
"%s - external temperature sensor have NOT been retrieved cause unknown or unavailable",
self,
)
else:
_LOGGER.debug(
"%s - external temperature sensor have NOT been retrieved "
"cause no external sensor",
"%s - external temperature sensor have NOT been retrieved cause no external sensor",
self,
)
@@ -547,16 +543,16 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.reset_last_change_time_from_vtherm()
def init_underlyings(self):
"""Initialize all underlyings. Should be overridden if necessary"""
"""Initialize all underlyings. Should be overriden if necessary"""
def restore_specific_previous_state(self, old_state: State):
"""Should be overridden in each specific thermostat
"""Should be overriden in each specific thermostat
if a specific previous state or attribute should be
restored
"""
async def get_my_previous_state(self):
"""Try to get my previous state"""
"""Try to get my previou state"""
# Check If we have an old state
old_state = await self.async_get_last_state()
_LOGGER.debug(
@@ -824,7 +820,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property
def total_energy(self) -> float | None:
"""Returns the total energy calculated for this thermostat"""
"""Returns the total energy calculated for this thermostast"""
if self._total_energy is not None:
return round(self._total_energy, 2)
else:
@@ -934,8 +930,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property
def activable_underlying_entities(self) -> list | None:
"""Returns the activable underlying entities for controlling
the central boiler"""
"""Returns the activable underlying entities for controling the central boiler"""
return self.underlying_entities
def find_underlying_by_entity_id(self, entity_id: str) -> Entity | None:
@@ -1020,12 +1015,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.async_write_ha_state()
self.send_event(EventType.HVAC_MODE_EVENT, {"hvac_mode": self._hvac_mode})
# If we already are in OFF, the manual OFF should just
# overwrite the reason and saved_hvac_mode
# If we already are in OFF, the manual OFF should just overwrite the reason and saved_hvac_mode
if self._hvac_mode == HVACMode.OFF and hvac_mode == HVACMode.OFF:
_LOGGER.info(
"%s - already in OFF. Change the reason to MANUAL "
"and erase the saved_havc_mode"
"%s - already in OFF. Change the reason to MANUAL and erase the saved_havc_mode"
)
self._hvac_off_reason = HVAC_OFF_REASON_MANUAL
self._saved_hvac_mode = HVACMode.OFF
@@ -1043,12 +1036,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await under.set_hvac_mode(hvac_mode) or need_control_heating
)
# If AC is on maybe we have to change the temperature in force mode,
# but not in frost mode (there is no Frost protection possible in AC mode)
if (
self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL]
and self.preset_mode != PRESET_NONE
):
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
if self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL] and self.preset_mode != PRESET_NONE:
if self.preset_mode != PRESET_FROST_PROTECTION:
await self.async_set_preset_mode_internal(self.preset_mode, True)
else:
@@ -1074,8 +1063,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# We accept a new preset when:
# 1. last_central_mode is not set,
# 2. or last_central_mode is AUTO,
# 3. or last_central_mode is CENTRAL_MODE_FROST_PROTECTION and preset_mode is
# PRESET_FROST_PROTECTION (to be abel to re-set the preset_mode)
# 3. or last_central_mode is CENTRAL_MODE_FROST_PROTECTION and preset_mode is PRESET_FROST_PROTECTION (to be abel to re-set the preset_mode)
accept = self._last_central_mode in [
None,
CENTRAL_MODE_AUTO,
@@ -1111,18 +1099,15 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
and preset_mode not in HIDDEN_PRESETS
):
raise ValueError(
f"Got unsupported preset_mode {preset_mode}. Must be one of {
self._attr_preset_modes}" # pylint: disable=line-too-long
f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long
)
old_preset_mode = self._attr_preset_mode
if preset_mode == old_preset_mode and not force:
# I don't think we need to call async_write_ha_state
# if we didn't change the state
# I don't think we need to call async_write_ha_state if we didn't change the state
return
# In safety mode don't change preset but memorise
# the new expected preset when safety will be off
# In safety mode don't change preset but memorise the new expected preset when safety will be off
if preset_mode != PRESET_SAFETY and self._safety_manager.is_safety_detected:
_LOGGER.debug(
"%s - is in safety mode. Just memorise the new expected ", self
@@ -1184,8 +1169,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
def find_preset_temp(self, preset_mode: str):
"""Find the right temperature of a preset considering
the presence if configured"""
"""Find the right temperature of a preset considering the presence if configured"""
if preset_mode is None or preset_mode == "none":
return (
self._attr_max_temp
@@ -1196,9 +1180,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if preset_mode == PRESET_SAFETY:
return (
self._target_temp
)
# In safety just keep the current target temperature,
# the thermostat should be off
) # in safety just keep the current target temperature, the thermostat should be off
if preset_mode == PRESET_POWER:
return self._power_manager.power_temperature
if preset_mode == PRESET_ACTIVITY:
@@ -1274,8 +1256,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._saved_target_temp = temperature
async def change_target_temperature(self, temperature: float):
"""Set the target temperature and the target temperature
of underlying climate if any"""
"""Set the target temperature and the target temperature of underlying climate if any"""
if temperature:
self._target_temp = temperature
@@ -1305,7 +1286,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@callback
async def _async_temperature_changed(self, event: Event) -> callable:
"""Handle temperature of the temperature sensor changes.
Return the function to dearm (clear) the window auto check"""
Return the fonction to desarm (clear) the window auto check"""
new_state: State = event.data.get("new_state")
_LOGGER.debug(
"%s - Temperature changed. Event.new_state is %s",
@@ -1334,7 +1315,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# try to extract the datetime (from state)
try:
# Convert ISO 8601 string to datetime object
# Convertir la chaîne au format ISO 8601 en objet datetime
self._last_temperature_measure = self.get_last_updated_date_or_now(
new_state
)
@@ -1360,7 +1341,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
async def _async_ext_temperature_changed(self, event: Event):
"""Handle external temperature of the sensor changes."""
"""Handle external temperature opf the sensor changes."""
new_state: State = event.data.get("new_state")
_LOGGER.debug(
"%s - external Temperature changed. Event.new_state is %s",
@@ -1406,8 +1387,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
_LOGGER.debug(
"%s - After setting _last_temperature_measure %s, "
"state.last_changed.replace=%s",
"%s - After setting _last_temperature_measure %s , state.last_changed.replace=%s",
self,
self._last_temperature_measure,
state.last_changed.astimezone(self._current_tz),
@@ -1434,8 +1414,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._last_ext_temperature_measure = self.get_state_date_or_now(state)
_LOGGER.debug(
"%s - After setting _last_ext_temperature_measure %s, "
"state.last_changed.replace=%s",
"%s - After setting _last_ext_temperature_measure %s , state.last_changed.replace=%s",
self,
self._last_ext_temperature_measure,
state.last_changed.astimezone(self._current_tz),
@@ -1448,11 +1427,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.error("Unable to update external temperature from sensor: %s", ex)
async def async_underlying_entity_turn_off(self):
"""Turn heater toggleable device off. Used by Window, overpowering,
control_heating to turn all off"""
"""Turn heater toggleable device off. Used by Window, overpowering, control_heating to turn all off"""
for under in self._underlyings:
await under.turn_off_and_cancel_cycle()
await under.turn_off()
def save_preset_mode(self):
"""Save the current preset mode to be restored later
@@ -1464,7 +1442,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
):
self._saved_preset_mode = self._attr_preset_mode
async def restore_preset_mode(self, force=False):
async def restore_preset_mode(self):
"""Restore a previous preset mode
We never restore a hidden preset mode. Normally that is not possible
"""
@@ -1472,7 +1450,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._saved_preset_mode not in HIDDEN_PRESETS
and self._saved_preset_mode is not None
):
await self.async_set_preset_mode_internal(self._saved_preset_mode, force=force)
await self.async_set_preset_mode_internal(self._saved_preset_mode)
def save_hvac_mode(self):
"""Save the current hvac-mode to be restored later"""
@@ -1580,10 +1558,19 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await self.async_set_hvac_mode(HVACMode.OFF)
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
def is_initialized(self) -> bool:
"""Check if all underlyings are initialized
This is useful only for over_climate in which we
This is usefull only for over_climate in which we
should have found the underlying climate to be operational"""
return True
@@ -1601,22 +1588,18 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# check auto_window conditions
await self._window_manager.manage_window_auto(in_cycle=True)
# In over_climate mode, if the underlying climate is not initialized,
# try to initialize it
# In over_climate mode, if the underlying climate is not initialized, try to initialize it
if not self.is_initialized:
if not self.init_underlyings():
# still not found, we an stop here
return False
# Check overpowering condition
await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
# TODO remove this
# overpowering is now centralized
# overpowering = await self._power_manager.check_overpowering()
# if overpowering == STATE_ON:
# _LOGGER.debug("%s - End of cycle (overpowering)", self)
# return True
# Not necessary for switch because each switch is checking at startup
overpowering = await self._power_manager.check_overpowering()
if overpowering == STATE_ON:
_LOGGER.debug("%s - End of cycle (overpowering)", self)
return True
safety: bool = await self._safety_manager.refresh_state()
if safety and self.is_over_climate:
@@ -1646,13 +1629,13 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state.
Should be overridden by super class
Should be overriden by super class
"""
raise NotImplementedError()
def incremente_energy(self):
"""increment the energy counter if device is active
Should be overridden by super class
Should be overriden by super class
"""
raise NotImplementedError()
@@ -1692,9 +1675,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"last_temperature_datetime": self._last_temperature_measure.astimezone(
self._current_tz
).isoformat(),
"last_ext_temperature_datetime":
self._last_ext_temperature_measure.astimezone(
self._current_tz
"last_ext_temperature_datetime": self._last_ext_temperature_measure.astimezone(
self._current_tz
).isoformat(),
"minimal_activation_delay_sec": self._minimal_activation_delay,
ATTR_TOTAL_ENERGY: self.total_energy,
@@ -1782,8 +1764,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
entity_id: climate.thermostat_2
"""
_LOGGER.info(
"%s - Calling service_set_preset_temperature, preset: %s, "
"temperature: %s, temperature_away: %s",
"%s - Calling service_set_preset_temperature, preset: %s, temperature: %s, temperature_away: %s",
self,
preset,
temperature,
@@ -1796,8 +1777,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._presets_away[self.get_preset_away_name(preset)] = temperature_away
else:
_LOGGER.warning(
"%s - No preset %s configured for this thermostat. "
"Ignoring set_preset_temperature call",
"%s - No preset %s configured for this thermostat. Ignoring set_preset_temperature call",
self,
preset,
)
@@ -1826,8 +1806,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
entity_id: climate.thermostat_2
"""
_LOGGER.info(
"%s - Calling SERVICE_SET_SAFETY, delay_min: %s, "
"min_on_percent: %s %%, default_on_percent: %s %%",
"%s - Calling SERVICE_SET_SAFETY, delay_min: %s, min_on_percent: %s %%, default_on_percent: %s %%",
self,
delay_min,
min_on_percent * 100,
@@ -1870,8 +1849,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
async def init_presets(self, central_config):
"""Init all presets of the VTherm"""
# If preset central config is used and central config is set,
# take the presets from central config
# If preset central config is used and central config is set , take the presets from central config
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
presets: dict[str, Any] = {}
@@ -1957,17 +1935,3 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
def is_preset_configured(self, preset) -> bool:
"""Returns True if the preset in argument is configured"""
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)
# @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

View File

@@ -1,288 +0,0 @@
""" Implements a central Power Feature Manager for Versatile Thermostat """
import logging
from typing import Any
from functools import cmp_to_key
from homeassistant.core import HomeAssistant, Event, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
EventStateChangedData,
)
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._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")
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"""
ret = False
if self._is_configured:
# try to acquire current power and power max
if (
new_state := get_safe_float(self._hass, self._power_sensor_entity_id)
) is not None:
self._current_power = new_state
_LOGGER.debug("Current power have been retrieved: %.3f", self._current_power)
ret = True
# Try to acquire power max
if (
new_state := get_safe_float(
self._hass, self._max_power_sensor_entity_id
)
) is not None:
self._current_max_power = new_state
_LOGGER.debug("Current power max have been retrieved: %.3f", self._current_max_power)
ret = True
# check if we need to re-calculate shedding
if ret:
now = self._vtherm_api.now
dtimestamp = (
(now - self._last_shedding_date).seconds
if self._last_shedding_date
else 999
)
if dtimestamp >= MIN_DTEMP_SECS:
await self.calculate_shedding()
self._last_shedding_date = now
return ret
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
# 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:
device_power = vtherm.power_manager.device_power
if vtherm.is_device_active and not vtherm.power_manager.is_overpowering_detected:
total_power_gain += device_power
_LOGGER.debug("vtherm %s should be in overpowering state", vtherm.name)
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
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_affected_power = 0
force_overpowering = False
for vtherm in vtherms_sorted:
device_power = vtherm.power_manager.device_power
if vtherm.is_device_active:
power_consumption_max = 0
else:
if vtherm.is_over_climate:
power_consumption_max = device_power
else:
power_consumption_max = max(
device_power / vtherm.nb_underlying_entities,
device_power * vtherm.proportional_algorithm.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)
if force_overpowering or (total_affected_power + power_consumption_max >= available_power):
_LOGGER.debug("vtherm %s should be in overpowering state", vtherm.name)
if not vtherm.power_manager.is_overpowering_detected:
# To force all others vtherms to be in overpowering
force_overpowering = True
await vtherm.power_manager.set_overpowering(True, power_consumption_max)
else:
total_affected_power += power_consumption_max
# Always set to false to init the state
_LOGGER.debug("vtherm %s should not be in overpowering state", vtherm.name)
await vtherm.power_manager.set_overpowering(False)
_LOGGER.debug("after vtherm %s total_affected_power=%s, available_power=%s", vtherm.name, total_affected_power, available_power)
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"

View File

@@ -28,7 +28,6 @@ from .thermostat_switch import ThermostatOverSwitch
from .thermostat_climate import ThermostatOverClimate
from .thermostat_valve import ThermostatOverValve
from .thermostat_climate_valve import ThermostatOverClimateValve
from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__)
@@ -52,9 +51,6 @@ async def async_setup_entry(
)
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
# Instantiate the right base class

View File

@@ -3,7 +3,6 @@
# pylint: disable=line-too-long
import logging
import warnings
from types import MappingProxyType
from typing import Any, TypeVar
@@ -133,20 +132,3 @@ def check_and_extract_service_configuration(service_config) -> dict:
"check_and_extract_service_configuration(%s) gives '%s'", service_config, 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

View File

@@ -339,12 +339,6 @@ 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
{
vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean,

View File

@@ -503,8 +503,6 @@ def get_safe_float(hass, entity_id: str):
if (
entity_id is None
or not (state := hass.states.get(entity_id))
or state.state is None
or state.state == "None"
or state.state == "unknown"
or state.state == "unavailable"
):

View File

@@ -12,15 +12,22 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from homeassistant.core import (
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 .commons import ConfigData
from .base_manager import BaseFeatureManager
from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__)
@@ -43,96 +50,194 @@ class FeaturePowerManager(BaseFeatureManager):
def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager"""
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._overpowering_state = STATE_UNAVAILABLE
self._is_configured: bool = False
self._device_power: float = 0
self._use_power_feature: bool = False
@overrides
def post_init(self, entry_infos: ConfigData):
"""Reinit of the manager"""
# 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._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
@overrides
def start_listening(self):
"""Start listening the underlying entity. There is nothing to listen"""
central_power_configuration = (
VersatileThermostatAPI.get_vtherm_api().central_power_manager.is_configured
)
self._current_power = None
self._current_max_power = None
if (
self._use_power_feature
entry_infos.get(CONF_USE_POWER_FEATURE, False)
and self._max_power_sensor_entity_id
and self._power_sensor_entity_id
and self._device_power
and central_power_configuration
):
self._is_configured = True
self._overpowering_state = STATE_UNKNOWN
else:
if self._use_power_feature:
if not central_power_configuration:
_LOGGER.warning(
"%s - Power management is not fully configured. You have to configure the central configuration power",
self,
)
else:
_LOGGER.warning(
"%s - Power management is not fully configured. You have to configure the power feature of the VTherm",
self,
)
_LOGGER.info("%s - Power management is not fully configured", self)
@overrides
def start_listening(self):
"""Start listening the underlying entity"""
if self._is_configured:
self.stop_listening()
else:
return
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]):
"""Add some custom attributes"""
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
extra_state_attributes.update(
{
"power_sensor_entity_id": vtherm_api.central_power_manager.power_sensor_entity_id,
"max_power_sensor_entity_id": vtherm_api.central_power_manager.max_power_sensor_entity_id,
"power_sensor_entity_id": self._power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
"overpowering_state": self._overpowering_state,
"is_power_configured": self._is_configured,
"device_power": self._device_power,
"power_temp": self._power_temp,
"current_power": vtherm_api.central_power_manager.current_power,
"current_max_power": vtherm_api.central_power_manager.current_max_power,
"current_power": self._current_power,
"current_max_power": self._current_max_power,
"mean_cycle_power": self.mean_cycle_power,
}
)
async def check_power_available(self) -> bool:
"""Check if the Vtherm can be started considering overpowering.
Returns True if no overpowering conditions are found
async def check_overpowering(self) -> bool:
"""Check the overpowering condition
Turn the preset_mode of the heater to 'power' if power conditions are exceeded
Returns True if overpowering is 'on'
"""
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
if (
not self._is_configured
or not vtherm_api.central_power_manager.is_configured
):
return True
if not self._is_configured:
return False
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
self._current_power is None
or self._device_power is None
or self._current_max_power is None
):
_LOGGER.warning(
"%s - power not valued. check_power_available not available", self
"%s - power not valued. check_overpowering not available", self
)
return True
return False
_LOGGER.debug(
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
self,
current_power,
current_max_power,
self._current_power,
self._current_max_power,
self._device_power,
)
@@ -148,78 +253,62 @@ class FeaturePowerManager(BaseFeatureManager):
self._device_power * self._vtherm.proportional_algorithm.on_percent,
)
ret = (current_power + power_consumption_max) < current_max_power
if not ret:
_LOGGER.info(
"%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f",
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:
ret = (self._current_power + power_consumption_max) >= self._current_max_power
if (
self._overpowering_state == STATE_OFF
and ret
and self._vtherm.hvac_mode != HVACMode.OFF
):
_LOGGER.warning(
"%s - overpowering is detected. Heater preset will be set to 'power'",
self,
)
self._overpowering_state = STATE_ON
if self._vtherm.is_over_climate:
self._vtherm.save_hvac_mode()
self._vtherm.save_preset_mode()
await self._vtherm.async_underlying_entity_turn_off()
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER, force=True)
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER)
self._vtherm.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": current_power,
"current_power": self._current_power,
"device_power": self._device_power,
"current_max_power": current_max_power,
"current_max_power": self._current_max_power,
"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(
"%s - end of overpowering is detected. Heater preset will be restored to '%s'",
self,
self._vtherm._saved_preset_mode, # pylint: disable=protected-access
)
self._overpowering_state = STATE_OFF
# restore state
if self._vtherm.is_over_climate:
await self._vtherm.restore_hvac_mode()
await self._vtherm.restore_hvac_mode(False)
await self._vtherm.restore_preset_mode()
# restart cycle
await self._vtherm.async_control_heating(force=True)
self._vtherm.send_event(
EventType.POWER_EVENT,
{
"type": "end",
"current_power": current_power,
"current_power": self._current_power,
"device_power": self._device_power,
"current_max_power": current_max_power,
"current_max_power": self._current_max_power,
},
)
elif not overpowering and self._overpowering_state != STATE_OFF:
# just set to not overpowering the state which was not set
self._overpowering_state = STATE_OFF
else:
# Nothing to do (already in the right state)
return
self._vtherm.update_custom_attributes()
new_overpowering_state = STATE_ON if ret else STATE_OFF
if self._overpowering_state != new_overpowering_state:
self._overpowering_state = new_overpowering_state
self._vtherm.update_custom_attributes()
return self._overpowering_state == STATE_ON
@overrides
@property
@@ -236,9 +325,14 @@ class FeaturePowerManager(BaseFeatureManager):
return self._overpowering_state
@property
def is_overpowering_detected(self) -> str | None:
"""Return True if the Vtherm is in overpowering state"""
return self._overpowering_state == STATE_ON
def max_power_sensor_entity_id(self) -> bool:
"""Return the power max entity id"""
return self._max_power_sensor_entity_id
@property
def power_sensor_entity_id(self) -> bool:
"""Return the power entity id"""
return self._power_sensor_entity_id
@property
def power_temperature(self) -> bool:
@@ -250,6 +344,16 @@ class FeaturePowerManager(BaseFeatureManager):
"""Return the 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
def mean_cycle_power(self) -> float | None:
"""Returns the mean power consumption during the cycle"""

View File

@@ -190,11 +190,6 @@ class UnderlyingEntity:
"""capping of the value send to the underlying eqt"""
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):
"""Represent a underlying switch"""
@@ -414,10 +409,9 @@ class UnderlyingSwitch(UnderlyingEntity):
await self.turn_off()
return
# if await self._thermostat.power_manager.check_overpowering():
# _LOGGER.debug("%s - End of cycle (3)", self)
# return
if await self._thermostat.power_manager.check_overpowering():
_LOGGER.debug("%s - End of cycle (3)", self)
return
# safety mode could have change the on_time percent
await self._thermostat.safety_manager.refresh_state()
time = self._on_time_sec

View File

@@ -1,7 +1,6 @@
""" The API of Versatile Thermostat"""
import logging
from datetime import datetime
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
@@ -17,11 +16,8 @@ from .const import (
CONF_THERMOSTAT_TYPE,
CONF_THERMOSTAT_CENTRAL_CONFIG,
CONF_MAX_ON_PERCENT,
NowClass,
)
from .central_feature_power_manager import CentralFeaturePowerManager
VTHERM_API_NAME = "vtherm_api"
_LOGGER = logging.getLogger(__name__)
@@ -66,12 +62,6 @@ class VersatileThermostatAPI(dict):
# A dict that will store all Number entities which holds the temperature
self._number_temperatures = dict()
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):
"""Search for a central configuration"""
@@ -186,10 +176,6 @@ class VersatileThermostatAPI(dict):
if entry_id is None or entry_id == entity.unique_id:
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:
self.central_power_manager.start_listening()
async def init_vtherm_preset_with_central(self):
"""Init all VTherm presets when the VTherm uses central temperature"""
# Initialization of all preset for all VTherm
@@ -303,18 +289,3 @@ class VersatileThermostatAPI(dict):
def hass(self):
"""Get the HomeAssistant object"""
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)

View File

@@ -1,3 +0,0 @@
[tool.black]
# don't work. Options are in the devcontainer.yaml
line-length = 180

View File

@@ -592,10 +592,7 @@ class MockNumber(NumberEntity):
async def create_thermostat(
hass: HomeAssistant,
entry: MockConfigEntry,
entity_id: str,
temps: dict | None = None,
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
) -> BaseThermostat:
"""Creates and return a TPI Thermostat"""
entry.add_to_hass(hass)
@@ -604,11 +601,6 @@ async def create_thermostat(
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
@@ -749,10 +741,9 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
)
},
)
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
await vtherm_api.central_power_manager._power_sensor_changed(power_event)
await entity.power_manager._async_power_sensor_changed(power_event)
if sleep:
await entity.hass.async_block_till_done()
await asyncio.sleep(0.1)
async def send_max_power_change_event(
@@ -776,10 +767,9 @@ async def send_max_power_change_event(
)
},
)
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
await vtherm_api.central_power_manager._max_power_sensor_changed(power_event)
await entity.power_manager._async_max_power_sensor_changed(power_event)
if sleep:
await entity.hass.async_block_till_done()
await asyncio.sleep(0.1)
async def send_window_change_event(
@@ -1111,9 +1101,3 @@ class SideEffects:
def add_or_update_side_effect(self, key: str, new_value: Any):
"""Update the value of a side effect"""
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()

View File

@@ -19,8 +19,6 @@
from unittest.mock import patch
import pytest
# https://github.com/miketheman/pytest-socket/pull/275
from pytest_socket import socket_allow_hosts
from homeassistant.core import StateMachine
@@ -28,12 +26,6 @@ from custom_components.versatile_thermostat.config_flow import (
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.base_thermostat import BaseThermostat
@@ -43,6 +35,12 @@ from .commons import (
FULL_CENTRAL_CONFIG_WITH_BOILER,
)
# https://github.com/miketheman/pytest-socket/pull/275
from pytest_socket import socket_allow_hosts
# ...
# ...
def pytest_runtest_setup():
"""setup tests"""
@@ -53,6 +51,16 @@ def pytest_runtest_setup():
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.
# Remove to enable selective use of this fixture
@pytest.fixture(autouse=True)
@@ -159,24 +167,3 @@ async def init_central_config_with_boiler_fixture(
await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER)
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

View File

@@ -1,4 +1,4 @@
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long
""" Test the normal start of a Thermostat """
from unittest.mock import patch
@@ -107,16 +107,9 @@ async def test_overpowering_binary_sensors(
skip_hass_states_is_state,
skip_turn_on_off_heater,
skip_send_event,
init_central_power_manager,
):
"""Test the overpowering binary sensors in thermostat type"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -129,6 +122,9 @@ async def test_overpowering_binary_sensors(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
@@ -140,13 +136,15 @@ async def test_overpowering_binary_sensors(
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
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_PRESET_POWER: 12,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theoverswitchmockname", temps
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -155,54 +153,35 @@ async def test_overpowering_binary_sensors(
)
assert overpowering_binary_sensor
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
now: datetime = datetime.now(tz=get_tz(hass))
# Overpowering should be not set because poer have not been received
await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now)
assert entity.power_manager.is_overpowering_detected is False
assert await entity.power_manager.check_overpowering() is False
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state is STATE_OFF
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
# Send power mesurement
side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", 100),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 150),
},
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, 100, now)
await send_max_power_change_event(entity, 150, now)
await send_power_change_event(entity, 100, now)
await send_max_power_change_event(entity, 150, now)
assert await entity.power_manager.check_overpowering() is True
assert entity.power_manager.overpowering_state is STATE_ON
assert entity.power_manager.is_overpowering_detected is True
assert entity.power_manager.overpowering_state is STATE_ON
# Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed()
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
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 201))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 201, 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
await send_max_power_change_event(entity, 201, now)
assert await entity.power_manager.check_overpowering() 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])

View File

@@ -266,9 +266,7 @@ async def test_bug_272(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_bug_407(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
"""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,
2. the user switch preset let's say from Comfort to Boost,
@@ -277,12 +275,6 @@ async def test_bug_407(
"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -295,6 +287,9 @@ async def test_bug_407(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
@@ -306,62 +301,52 @@ async def test_bug_407(
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
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_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname", temps
hass, entry, "climate.theoverswitchmockname"
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
await send_temperature_change_event(entity, 16, now)
await send_ext_temperature_change_event(entity, 10, now)
# 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(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
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_preset_mode(PRESET_COMFORT)
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 18
# waits that the heater starts
await hass.async_block_till_done()
await asyncio.sleep(0.1)
assert mock_service_call.call_count >= 1
assert entity.is_device_active is True
# Send power max mesurement
await send_max_power_change_event(entity, 110, now)
await send_max_power_change_event(entity, 110, datetime.now())
# Send power mesurement (theheater is already in the power measurement)
await send_power_change_event(entity, 100, now)
await send_power_change_event(entity, 100, datetime.now())
# No overpowering yet
assert entity.power_manager.is_overpowering_detected is False
assert await entity.power_manager.check_overpowering() is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_OFF
@@ -374,24 +359,13 @@ async def test_bug_407(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
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
await entity.async_set_preset_mode(PRESET_BOOST)
# waits that the heater starts
await asyncio.sleep(0.1)
# doesn't work for call_later
# await hass.async_block_till_done()
# simulate a refresh for central power (not necessary)
await do_central_power_refresh(hass)
assert entity.power_manager.is_overpowering_detected is False
assert await entity.power_manager.check_overpowering() is False
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_OFF
@@ -405,22 +379,13 @@ async def test_bug_407(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
new_callable=PropertyMock,
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
await entity.async_set_preset_mode(PRESET_COMFORT)
# waits that the heater starts
await asyncio.sleep(0.1)
# simulate a refresh for central power (not necessary)
await do_central_power_refresh(hass)
assert entity.power_manager.is_overpowering_detected is True
assert await entity.power_manager.check_overpowering() is True
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_POWER
assert entity.power_manager.overpowering_state is STATE_ON

View File

@@ -188,18 +188,6 @@ async def test_full_over_switch_wo_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_vtherm_api
):
"""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
entry = MockConfigEntry(
domain=DOMAIN,
@@ -214,11 +202,19 @@ async def test_full_over_switch_wo_central_config(
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
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_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
@@ -237,6 +233,8 @@ async def test_full_over_switch_wo_central_config(
CONF_MOTION_PRESET: "comfort",
CONF_NO_MOTION_PRESET: "eco",
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_USE_MAIN_CENTRAL_CONFIG: False,
CONF_USE_TPI_CENTRAL_CONFIG: False,
@@ -251,7 +249,7 @@ async def test_full_over_switch_wo_central_config(
with patch("homeassistant.core.ServiceRegistry.async_call"):
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname", temps
hass, entry, "climate.theoverswitchmockname"
)
assert entity
assert entity.name == "TheOverSwitchMockName"
@@ -302,13 +300,10 @@ async def test_full_over_switch_wo_central_config(
assert entity.motion_manager.motion_preset == "comfort"
assert entity.motion_manager.no_motion_preset == "eco"
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
assert (
VersatileThermostatAPI.get_vtherm_api().central_power_manager.power_sensor_entity_id
is None
)
assert (
VersatileThermostatAPI.get_vtherm_api().central_power_manager.max_power_sensor_entity_id
is None
entity.power_manager.max_power_sensor_entity_id
== "sensor.mock_max_power_sensor"
)
assert (
@@ -322,7 +317,7 @@ async def test_full_over_switch_wo_central_config(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_full_over_switch_with_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
):
"""Tests that a VTherm with central_configuration is working with the central_config attributes"""
# Add a Switch VTherm
@@ -339,11 +334,15 @@ async def test_full_over_switch_with_central_config(
CONF_TEMP_MIN: 8,
CONF_TEMP_MAX: 18,
CONF_STEP_TEMPERATURE: 0.3,
"frost_temp": 10,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 21,
CONF_USE_WINDOW_FEATURE: True,
CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: True,
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
@@ -362,6 +361,8 @@ async def test_full_over_switch_with_central_config(
CONF_MOTION_PRESET: "comfort",
CONF_NO_MOTION_PRESET: "eco",
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_USE_MAIN_CENTRAL_CONFIG: True,
CONF_USE_TPI_CENTRAL_CONFIG: True,
@@ -425,13 +426,10 @@ async def test_full_over_switch_with_central_config(
assert entity.motion_manager.motion_preset == "boost"
assert entity.motion_manager.no_motion_preset == "frost"
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
assert (
VersatileThermostatAPI.get_vtherm_api().central_power_manager.power_sensor_entity_id
== "sensor.the_power_sensor"
)
assert (
VersatileThermostatAPI.get_vtherm_api().central_power_manager.max_power_sensor_entity_id
== "sensor.the_max_power_sensor"
entity.power_manager.max_power_sensor_entity_id
== "sensor.mock_max_power_sensor"
)
assert (

View File

@@ -1,719 +0,0 @@
# 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
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 (no shedding)
(
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,
},
],
{"vtherm1": False},
),
# Simple trivial shedding
(
1000,
2000,
[
# should be overpowering
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
# should be overpowering with many underlmying entities
{
"name": "vtherm2",
"device_power": 4000,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": False,
},
# over_climate should be overpowering
{
"name": "vtherm3",
"device_power": 1000,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# should pass but because will be also overpowering because previous was overpowering
{
"name": "vtherm4",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
],
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm4": True},
),
# More complex shedding
(
1000,
2000,
[
# already overpowering (non change)
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
# already overpowering and already active (can be un overpowered)
{
"name": "vtherm2",
"device_power": 1100,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": True,
},
# should terminate the overpowering
{
"name": "vtherm3",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
# should terminate the overpowering and active
{
"name": "vtherm4",
"device_power": 3800,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
],
{"vtherm2": False, "vtherm3": False, "vtherm4": False},
),
# More complex shedding
(
1000,
2000,
[
# already overpowering (non change)
{
"name": "vtherm1",
"device_power": 1100,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
},
# should be overpowering
{
"name": "vtherm2",
"device_power": 1800,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# should terminate the overpowering and active but just before is overpowering
{
"name": "vtherm3",
"device_power": 100,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
],
{"vtherm1": False, "vtherm2": True, "vtherm3": True},
),
# Sheeding only current_power > max_power (need to gain 1000 )
(
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,
},
# should be overpowering but is already
{
"name": "vtherm2",
"device_power": 600,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": True,
},
# over_climate should be not overpowering (device not active)
{
"name": "vtherm3",
"device_power": 690,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# over_climate should be overpowering (device active and not already overpowering)
{
"name": "vtherm4",
"device_power": 690,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
},
# should not overpower (keep as is)
{
"name": "vtherm5",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
],
{"vtherm1": True, "vtherm4": 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.proportional_algorithm.on_percent = vtherm_config.get("on_percent")
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")
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)
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),
}))
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),
}))
assert central_power_manager.current_power == expected_power
assert mock_calculate_shedding.call_count == (nb_call if dsecs >= 20 else 0)
@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)
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),
}))
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),
}))
assert central_power_manager.current_max_power == expected_power
assert mock_calculate_shedding.call_count == (nb_call if dsecs >= 20 else 0)

View File

@@ -721,14 +721,10 @@ async def test_multiple_climates_underlying_changes_not_aligned(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switch_power_management(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -741,16 +737,17 @@ async def test_multiple_switch_power_management(
CONF_CYCLE_MIN: 8,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: [
"switch.mock_switch1",
"switch.mock_switch2",
"switch.mock_switch3",
"switch.mock_switch4",
],
CONF_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
@@ -758,13 +755,15 @@ async def test_multiple_switch_power_management(
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
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_PRESET_POWER: 12,
},
)
entity: BaseThermostat = await create_thermostat(
hass, entry, "climate.theover4switchmockname", temps
hass, entry, "climate.theover4switchmockname"
)
assert entity
assert entity.is_over_climate is False
@@ -773,9 +772,6 @@ async def test_multiple_switch_power_management(
tpi_algo = entity._prop_algorithm
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_preset_mode(PRESET_BOOST)
assert entity.hvac_mode is HVACMode.HEAT
@@ -784,103 +780,76 @@ async def test_multiple_switch_power_management(
assert entity.target_temperature == 19
# 1. Send power mesurement
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"),
)
await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
# fmt: on
now = now + timedelta(seconds=30)
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
await send_max_power_change_event(entity, 300, datetime.now())
assert await entity.power_manager.check_overpowering() 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
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 74))
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
# 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
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
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
await send_max_power_change_event(entity, 74, 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
assert entity.power_manager.overpowering_state is STATE_ON
assert entity.target_temperature == 12
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": 74,
"current_power_consumption": 25.0,
},
),
],
any_order=True,
)
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": 74,
"current_power_consumption": 25.0,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 4 # The fourth are shutdown
# 3. change PRESET
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
# No change
assert entity.power_manager.overpowering_state is STATE_ON
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event:
await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO
# No change
assert entity.power_manager.overpowering_state is STATE_ON
# 4. Send hugh power max mesurement to release overpowering
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 150))
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
# 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
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
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
assert (
mock_heater_on.call_count == 0
) # The fourth are not restarted because temperature is enought
assert mock_heater_off.call_count == 0

View File

@@ -10,7 +10,6 @@ from custom_components.versatile_thermostat.thermostat_switch import (
from custom_components.versatile_thermostat.feature_power_manager import (
FeaturePowerManager,
)
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -18,28 +17,28 @@ logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize(
"is_over_climate, is_device_active, power, max_power, check_power_available",
"is_over_climate, is_device_active, power, max_power, current_overpowering_state, overpowering_state, nb_call, changed, check_overpowering_ret",
[
# don't switch to overpower (power is enough)
(False, False, 1000, 3000, True),
(False, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
# switch to overpower (power is not enough)
(False, False, 2000, 3000, False),
(False, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
# don't switch to overpower (power is not enough but device is already on)
(False, True, 2000, 3000, True),
(False, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
# Same with a over_climate
# don't switch to overpower (power is enough)
(True, False, 1000, 3000, True),
(True, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
# switch to overpower (power is not enough)
(True, False, 2000, 3000, False),
(True, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
# don't switch to overpower (power is not enough but device is already on)
(True, True, 2000, 3000, True),
(True, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
# Leave overpowering state
# switch to not overpower (power is enough)
(False, False, 1000, 3000, True),
(False, False, 1000, 3000, STATE_ON, STATE_OFF, 1, True, False),
# don't switch to overpower (power is still not enough)
(False, False, 2000, 3000, False),
(False, False, 2000, 3000, STATE_ON, STATE_ON, 0, True, True),
# keep overpower (power is not enough but device is already on)
(False, True, 3000, 3000, False),
(False, True, 3000, 3000, STATE_ON, STATE_ON, 0, True, True),
],
)
async def test_power_feature_manager(
@@ -48,15 +47,17 @@ async def test_power_feature_manager(
is_device_active,
power,
max_power,
check_power_available,
current_overpowering_state,
overpowering_state,
nb_call,
changed,
check_overpowering_ret,
):
"""Test the FeaturePresenceManager class direclty"""
fake_vtherm = MagicMock(spec=BaseThermostat)
type(fake_vtherm).name = PropertyMock(return_value="the name")
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# 1. creation
power_manager = FeaturePowerManager(fake_vtherm, hass)
@@ -79,27 +80,16 @@ async def test_power_feature_manager(
assert custom_attributes["current_max_power"] is None
# 2. post_init
vtherm_api.find_central_configuration = MagicMock()
vtherm_api.central_power_manager.post_init(
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,
}
)
power_manager.start_listening()
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
@@ -121,14 +111,21 @@ async def test_power_feature_manager(
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
assert len(power_manager._active_listener) == 0 # no more listening
assert len(power_manager._active_listener) == 2
# 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
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):
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()) as mock_get_state:
# fmt:on
# Finish the mock configuration
tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, "climate.vtherm")
tpi_algo._on_percent = 1 # pylint: disable="protected-access"
@@ -137,84 +134,8 @@ async def test_power_feature_manager(
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).nb_underlying_entities = PropertyMock(return_value=1)
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,
}
)
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
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)
fake_vtherm.save_hvac_mode = MagicMock()
fake_vtherm.restore_hvac_mode = AsyncMock()
@@ -226,17 +147,26 @@ async def test_power_feature_manager_set_overpowering(
fake_vtherm.update_custom_attributes = MagicMock()
# Call set_overpowering
await power_manager.set_overpowering(is_overpowering, 1234)
ret = await power_manager.refresh_state()
assert ret == changed
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
assert power_manager.overpowering_state == new_overpowering_state
# check overpowering
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 not is_overpowering:
assert power_manager.overpowering_state == STATE_OFF
if power_manager.overpowering_state == STATE_OFF:
assert fake_vtherm.save_hvac_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_set_preset_mode_internal.call_count == 0
assert fake_vtherm.send_event.call_count == nb_call
if current_overpowering_state == STATE_ON:
assert fake_vtherm.update_custom_attributes.call_count == 1
@@ -248,24 +178,18 @@ async def test_power_feature_manager_set_overpowering(
else:
assert fake_vtherm.update_custom_attributes.call_count == 0
if msg_sent:
if nb_call == 1:
fake_vtherm.send_event.assert_has_calls(
[
call.fake_vtherm.send_event(
EventType.POWER_EVENT,
{
"type": "end",
"current_power": 1000,
"device_power": 1234,
"current_max_power": 2000,
},
),
{'type': 'end', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power}),
]
)
# is_overpowering is True
else:
assert power_manager.overpowering_state == STATE_ON
if is_over_climate and current_overpowering_state == STATE_OFF:
elif power_manager.overpowering_state == STATE_ON:
if is_over_climate:
assert fake_vtherm.save_hvac_mode.call_count == 1
else:
assert fake_vtherm.save_hvac_mode.call_count == 0
@@ -285,37 +209,30 @@ async def test_power_feature_manager_set_overpowering(
assert fake_vtherm.restore_hvac_mode.call_count == 0
assert fake_vtherm.restore_preset_mode.call_count == 0
if msg_sent:
if nb_call == 1:
fake_vtherm.send_event.assert_has_calls(
[
call.fake_vtherm.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": 1000,
"device_power": 1234,
"current_max_power": 2000,
"current_power_consumption": 1234.0,
},
),
{'type': 'start', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power, 'current_power_consumption': 1234.0}),
]
)
fake_vtherm.reset_mock()
# 5. Check custom_attributes
# 5. Check 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["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
)
assert custom_attributes["overpowering_state"] == new_overpowering_state
assert custom_attributes["overpowering_state"] == overpowering_state
assert custom_attributes["is_power_configured"] is True
assert custom_attributes["device_power"] == 1234
assert custom_attributes["power_temp"] == 10
assert custom_attributes["current_power"] == 1000
assert custom_attributes["current_max_power"] == 2000
assert custom_attributes["current_power"] == power
assert custom_attributes["current_max_power"] == max_power
power_manager.stop_listening()
await hass.async_block_till_done()
@@ -324,15 +241,10 @@ async def test_power_feature_manager_set_overpowering(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_off(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -345,24 +257,29 @@ async def test_power_management_hvac_off(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
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_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname", temps
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -375,53 +292,34 @@ async def test_power_management_hvac_off(
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.hvac_mode == HVACMode.OFF
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
# Send power mesurement
# fmt:off
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
await send_power_change_event(entity, 50, datetime.now())
assert await entity.power_manager.check_overpowering() is False
# All configuration is not complete
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_UNKNOWN # due to hvac_off
# All configuration is not complete
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
# Send power max mesurement
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 300, 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_UNKNOWN # # due to hvac_off
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
assert await entity.power_manager.check_overpowering() 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 but HVACMode is off
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
assert entity.power_manager.is_overpowering_detected is False
assert await entity.power_manager.check_overpowering() is True
# 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.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.power_manager.overpowering_state is STATE_ON
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
@@ -430,17 +328,9 @@ async def test_power_management_hvac_off(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_on(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
"""Test the Power management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -453,30 +343,32 @@ async def test_power_management_hvac_on(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_HEATER: "switch.mock_switch",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
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_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname", temps
hass, entry, "climate.theoverswitchmockname"
)
assert entity
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
tpi_algo = entity._prop_algorithm
assert tpi_algo
@@ -488,40 +380,24 @@ async def test_power_management_hvac_on(
assert entity.target_temperature == 19
# Send power mesurement
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, 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
await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now())
assert await entity.power_manager.check_overpowering() 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
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now())
assert entity.power_manager.is_overpowering_detected is True
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
@@ -548,18 +424,15 @@ async def test_power_management_hvac_on(
assert mock_heater_off.call_count == 1
# Send power mesurement low to unseet power preset
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 48))
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
) as mock_heater_on, patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off:
await send_power_change_event(entity, 48, datetime.now())
assert entity.power_manager.is_overpowering_detected is False
assert await entity.power_manager.check_overpowering() is False
# All configuration is complete and power is < power_max, we restore previous preset
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_OFF
@@ -589,16 +462,10 @@ async def test_power_management_hvac_on(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_switch(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
hass: HomeAssistant, skip_hass_states_is_state
):
"""Test the Power management energy mesurement"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -611,24 +478,30 @@ async def test_power_management_energy_over_switch(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["switch.mock_switch", "switch.mock_switch2"],
CONF_HEATER: "switch.mock_switch",
CONF_HEATER_2: "switch.mock_switch2",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
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_PRESET_POWER: 12,
},
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname", temps
hass, entry, "climate.theoverswitchmockname"
)
assert entity
@@ -650,8 +523,6 @@ async def test_power_management_energy_over_switch(
await entity.async_set_preset_mode(PRESET_BOOST)
await send_temperature_change_event(entity, 15, datetime.now())
await hass.async_block_till_done()
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19
@@ -723,12 +594,6 @@ async def test_power_management_energy_over_climate(
):
"""Test the Power management for a over_climate thermostat"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
the_mock_underlying = MagicMockClimate()
with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
@@ -746,11 +611,14 @@ async def test_power_management_energy_over_climate(
CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False,
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_CLIMATE: "climate.mock_climate",
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
@@ -762,7 +630,7 @@ async def test_power_management_energy_over_climate(
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverclimatemockname", temps
hass, entry, "climate.theoverclimatemockname"
)
assert entity
assert entity.is_over_climate