Compare commits

...

15 Commits

Author SHA1 Message Date
Jean-Marc Collin
8b58e69755 Fix restore overpowering_state 2025-01-05 10:53:28 +00:00
Jean-Marc Collin
81231f977c Add temporal filter for calculate_shedding
Add restore overpowering state at startup
2025-01-05 10:30:34 +00:00
Jean-Marc Collin
6bdcecefac Add logs 2025-01-05 08:47:00 +00:00
Jean-Marc Collin
34181b4204 Fix too much shedding in over_climate 2025-01-05 08:44:59 +00:00
Jean-Marc Collin
460281603f Fix overpowering is set even if other heater have on_percent = 0 2025-01-04 19:21:06 +00:00
Jean-Marc Collin
10c8281b32 Integration tests ok 2025-01-04 17:38:17 +00:00
Jean-Marc Collin
c01f96c955 Change shedding calculation delay to 20 sec (vs 60 sec) 2025-01-04 15:50:30 +00:00
Jean-Marc Collin
33c7c710ee enhance the overpowering algo if current_power > max_power 2025-01-04 15:26:29 +00:00
Jean-Marc Collin
6d0ebbaaab integrattion tests - Do startup works 2025-01-04 11:07:41 +00:00
Jean-Marc Collin
0b5d937968 All tests ok 2025-01-04 09:39:42 +00:00
Jean-Marc Collin
24fcb7a161 All tests not ok 2025-01-03 17:43:27 +00:00
Jean-Marc Collin
03fbc5362a All tests ok for central_feature_power_manager 2025-01-03 10:24:09 +00:00
Jean-Marc Collin
9f3199a053 Commit for rebase 2025-01-03 08:10:39 +00:00
Jean-Marc Collin
7a636c0a72 With tests of calculate_shedding ok 2025-01-03 08:10:39 +00:00
Jean-Marc Collin
6237273029 First implem + tests (not finished) 2025-01-03 08:10:39 +00:00
31 changed files with 1818 additions and 572 deletions

View File

@@ -54,6 +54,7 @@
"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,

2
.pylintrc Normal file
View File

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

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):
def __init__(self, vtherm: Any, hass: HomeAssistant, name: str = None):
"""Init of a featureManager"""
self._vtherm = vtherm
self._name = vtherm.name
self._name = vtherm.name if vtherm else name
self._active_listener: list[CALLBACK_TYPE] = []
self._hass = hass
@@ -27,7 +27,7 @@ class BaseFeatureManager:
"""Initialize the attributes of the FeatureManager"""
raise NotImplementedError()
def start_listening(self):
async def start_listening(self):
"""Start listening the underlying entity"""
raise NotImplementedError()
@@ -38,6 +38,10 @@ 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

@@ -50,7 +50,6 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
STATE_ON,
)
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -99,7 +98,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"comfort_away_temp",
"power_temp",
"ac_mode",
"current_max_power",
"saved_preset_mode",
"saved_target_temp",
"saved_hvac_mode",
@@ -191,7 +189,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._ema_temp = None
self._ema_algo = None
self._now = None
self._attr_fan_mode = None
@@ -446,10 +443,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
)
)
# start listening for all managers
for manager in self._managers:
manager.start_listening()
self.async_on_remove(self.remove_thermostat)
# issue 428. Link to others entities will start at link
@@ -482,6 +475,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.debug("%s - Calling async_startup_internal", self)
need_write_state = False
# start listening for all managers
for manager in self._managers:
await manager.start_listening()
await self.get_my_previous_state()
await self.init_presets(central_configuration)
@@ -1454,7 +1451,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
control_heating to turn all off"""
for under in self._underlyings:
await under.turn_off()
await under.turn_off_and_cancel_cycle()
def save_preset_mode(self):
"""Save the current preset mode to be restored later
@@ -1466,7 +1463,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
):
self._saved_preset_mode = self._attr_preset_mode
async def restore_preset_mode(self):
async def restore_preset_mode(self, force=False):
"""Restore a previous preset mode
We never restore a hidden preset mode. Normally that is not possible
"""
@@ -1474,7 +1471,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._saved_preset_mode not in HIDDEN_PRESETS
and self._saved_preset_mode is not None
):
await self.async_set_preset_mode_internal(self._saved_preset_mode)
await self.async_set_preset_mode_internal(self._saved_preset_mode, force=force)
def save_hvac_mode(self):
"""Save the current hvac-mode to be restored later"""
@@ -1582,15 +1579,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await self.async_set_hvac_mode(HVACMode.OFF)
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
@@ -1620,11 +1608,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return False
# Check overpowering condition
# 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
# Not usefull. Will be done at the next power refresh
# await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
safety: bool = await self._safety_manager.refresh_state()
if safety and self.is_over_climate:
@@ -1965,3 +1950,34 @@ 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) # pylint: disable=protected-access
# @deprecated
@property
def now(self) -> datetime:
"""Get now. The local datetime or the overloaded _set_now date
This method should be replaced by the vthermAPI equivalent"""
return VersatileThermostatAPI.get_vtherm_api(self._hass).now
@property
def power_percent(self) -> float | None:
"""Get the current on_percent as a percentage value. valid only for Vtherm with a TPI algo
Get the current on_percent value"""
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
@property
def on_percent(self) -> float | None:
"""Get the current on_percent value. valid only for Vtherm with a TPI algo"""
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
return self._prop_algorithm.on_percent
else:
return None

View File

@@ -0,0 +1,309 @@
""" Implements a central Power Feature Manager for Versatile Thermostat """
import logging
from typing import Any
from functools import cmp_to_key
from datetime import timedelta
from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant, Event, callback
from homeassistant.helpers.event import (
async_track_state_change_event,
EventStateChangedData,
async_call_later,
)
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.climate import (
ClimateEntity,
DOMAIN as CLIMATE_DOMAIN,
)
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .commons import ConfigData
from .base_manager import BaseFeatureManager
# circular dependency
# from .base_thermostat import BaseThermostat
MIN_DTEMP_SECS = 20
_LOGGER = logging.getLogger(__name__)
class CentralFeaturePowerManager(BaseFeatureManager):
"""A central Power feature manager"""
def __init__(self, hass: HomeAssistant, vtherm_api: Any):
"""Init of a featureManager"""
super().__init__(None, hass, "centralPowerManager")
self._hass: HomeAssistant = hass
self._vtherm_api = vtherm_api # no type due to circular reference
self._is_configured: bool = False
self._power_sensor_entity_id: str = None
self._max_power_sensor_entity_id: str = None
self._current_power: float = None
self._current_max_power: float = None
self._power_temp: float = None
self._cancel_calculate_shedding_call = None
# Not used now
self._last_shedding_date = None
def post_init(self, entry_infos: ConfigData):
"""Gets the configuration parameters"""
central_config = self._vtherm_api.find_central_configuration()
if not central_config:
_LOGGER.info("No central configuration is found. Power management will be deactivated")
return
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
self._is_configured = False
self._current_power = None
self._current_max_power = None
if (
entry_infos.get(CONF_USE_POWER_FEATURE, False)
and self._max_power_sensor_entity_id
and self._power_sensor_entity_id
and self._power_temp
):
self._is_configured = True
else:
_LOGGER.info("Power management is not fully configured and will be deactivated")
async def start_listening(self):
"""Start listening the power sensor"""
if not self._is_configured:
return
self.stop_listening()
self.add_listener(
async_track_state_change_event(
self.hass,
[self._power_sensor_entity_id],
self._power_sensor_changed,
)
)
self.add_listener(
async_track_state_change_event(
self.hass,
[self._max_power_sensor_entity_id],
self._max_power_sensor_changed,
)
)
@callback
async def _power_sensor_changed(self, event: Event[EventStateChangedData]):
"""Handle power changes."""
_LOGGER.debug("Receive new Power event")
_LOGGER.debug(event)
await self.refresh_state()
@callback
async def _max_power_sensor_changed(self, event: Event[EventStateChangedData]):
"""Handle power max changes."""
_LOGGER.debug("Receive new Power Max event")
_LOGGER.debug(event)
await self.refresh_state()
@overrides
async def refresh_state(self) -> bool:
"""Tries to get the last state from sensor
Returns True if a change has been made"""
async def _calculate_shedding_internal(_):
_LOGGER.debug("Do the shedding calculation")
await self.calculate_shedding()
if self._cancel_calculate_shedding_call:
self._cancel_calculate_shedding_call()
self._cancel_calculate_shedding_call = None
if not self._is_configured:
return False
# Retrieve current power
new_power = get_safe_float(self._hass, self._power_sensor_entity_id)
power_changed = new_power is not None and self._current_power != new_power
if power_changed:
self._current_power = new_power
_LOGGER.debug("New current power has been retrieved: %.3f", self._current_power)
# Retrieve max power
new_max_power = get_safe_float(self._hass, self._max_power_sensor_entity_id)
max_power_changed = new_max_power is not None and self._current_max_power != new_max_power
if max_power_changed:
self._current_max_power = new_max_power
_LOGGER.debug("New current max power has been retrieved: %.3f", self._current_max_power)
# Schedule shedding calculation if there's any change
if power_changed or max_power_changed:
if not self._cancel_calculate_shedding_call:
self._cancel_calculate_shedding_call = async_call_later(self.hass, timedelta(seconds=MIN_DTEMP_SECS), _calculate_shedding_internal)
return True
return False
# For testing purpose only, do an immediate shedding calculation
async def _do_immediate_shedding(self):
"""Do an immmediate shedding calculation if a timer was programmed.
Else, do nothing"""
if self._cancel_calculate_shedding_call:
self._cancel_calculate_shedding_call()
self._cancel_calculate_shedding_call = None
await self.calculate_shedding()
async def calculate_shedding(self):
"""Do the shedding calculation and set/unset VTherm into overpowering state"""
if not self.is_configured or self.current_max_power is None or self.current_power is None:
return
_LOGGER.debug("-------- Start of calculate_shedding")
# Find all VTherms
available_power = self.current_max_power - self.current_power
vtherms_sorted = self.find_all_vtherm_with_power_management_sorted_by_dtemp()
# shedding only
if available_power < 0:
_LOGGER.debug(
"The available power is is < 0 (%s). Set overpowering only for list: %s",
available_power,
vtherms_sorted,
)
# we will set overpowering for the nearest target temp first
total_power_gain = 0
for vtherm in vtherms_sorted:
if vtherm.is_device_active and not vtherm.power_manager.is_overpowering_detected:
device_power = vtherm.power_manager.device_power
total_power_gain += device_power
_LOGGER.info("vtherm %s should be in overpowering state (device_power=%.2f)", vtherm.name, device_power)
await vtherm.power_manager.set_overpowering(True, device_power)
_LOGGER.debug("after vtherm %s total_power_gain=%s, available_power=%s", vtherm.name, total_power_gain, available_power)
if total_power_gain >= -available_power:
_LOGGER.debug("We have found enough vtherm to set to overpowering")
break
# unshedding only
else:
vtherms_sorted.reverse()
_LOGGER.debug("The available power is is > 0 (%s). Do a complete shedding/un-shedding calculation for list: %s", available_power, vtherms_sorted)
total_power_added = 0
for vtherm in vtherms_sorted:
# We want to do always unshedding in order to initialize the state
# so we cannot use is_overpowering_detected which test also UNKNOWN and UNAVAILABLE
if vtherm.power_manager.overpowering_state == STATE_OFF:
continue
power_consumption_max = device_power = vtherm.power_manager.device_power
# calculate the power_consumption_max
if vtherm.on_percent is not None:
power_consumption_max = max(
device_power / vtherm.nb_underlying_entities,
device_power * vtherm.on_percent,
)
_LOGGER.debug("vtherm %s power_consumption_max is %s (device_power=%s, overclimate=%s)", vtherm.name, power_consumption_max, device_power, vtherm.is_over_climate)
# or not ... is for initializing the overpowering state if not already done
if total_power_added + power_consumption_max < available_power or not vtherm.power_manager.is_overpowering_detected:
# we count the unshedding only if the VTherm was in shedding
if vtherm.power_manager.is_overpowering_detected:
_LOGGER.info("vtherm %s should not be in overpowering state (power_consumption_max=%.2f)", vtherm.name, power_consumption_max)
total_power_added += power_consumption_max
await vtherm.power_manager.set_overpowering(False)
if total_power_added >= available_power:
_LOGGER.debug("We have found enough vtherm to set to non-overpowering")
break
_LOGGER.debug("after vtherm %s total_power_added=%s, available_power=%s", vtherm.name, total_power_added, available_power)
self._last_shedding_date = self._vtherm_api.now
_LOGGER.debug("-------- End of calculate_shedding")
def get_climate_components_entities(self) -> list:
"""Get all VTherms entitites"""
vtherms = []
component: EntityComponent[ClimateEntity] = self._hass.data.get(
CLIMATE_DOMAIN, None
)
if component:
for entity in component.entities:
# A little hack to test if the climate is a VTherm. Cannot use isinstance
# due to circular dependency of BaseThermostat
if (
entity.device_info
and entity.device_info.get("model", None) == DOMAIN
):
vtherms.append(entity)
return vtherms
def find_all_vtherm_with_power_management_sorted_by_dtemp(
self,
) -> list:
"""Returns all the VTherms with power management activated"""
entities = self.get_climate_components_entities()
vtherms = [
vtherm
for vtherm in entities
if vtherm.power_manager.is_configured and vtherm.is_on
]
# sort the result with the min temp difference first. A and B should be BaseThermostat class
def cmp_temps(a, b) -> int:
diff_a = float("inf")
diff_b = float("inf")
a_target = a.target_temperature if not a.power_manager.is_overpowering_detected else a.saved_target_temp
b_target = b.target_temperature if not b.power_manager.is_overpowering_detected else b.saved_target_temp
if a.current_temperature is not None and a_target is not None:
diff_a = a_target - a.current_temperature
if b.current_temperature is not None and b_target is not None:
diff_b = b_target - b.current_temperature
if diff_a == diff_b:
return 0
return 1 if diff_a > diff_b else -1
vtherms.sort(key=cmp_to_key(cmp_temps))
return vtherms
@property
def is_configured(self) -> bool:
"""True if the FeatureManager is fully configured"""
return self._is_configured
@property
def current_power(self) -> float | None:
"""Return the current power from sensor"""
return self._current_power
@property
def current_max_power(self) -> float | None:
"""Return the current power from sensor"""
return self._current_max_power
@property
def power_temperature(self) -> float | None:
"""Return the power temperature"""
return self._power_temp
@property
def power_sensor_entity_id(self) -> float | None:
"""Return the power sensor entity id"""
return self._power_sensor_entity_id
@property
def max_power_sensor_entity_id(self) -> float | None:
"""Return the max power sensor entity id"""
return self._max_power_sensor_entity_id
def __str__(self):
return "CentralPowerManager"

View File

@@ -28,6 +28,7 @@ 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__)
@@ -51,6 +52,9 @@ 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,6 +3,7 @@
# pylint: disable=line-too-long
import logging
import warnings
from types import MappingProxyType
from typing import Any, TypeVar
@@ -132,3 +133,20 @@ def check_and_extract_service_configuration(service_config) -> dict:
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
)
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,6 +339,12 @@ STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
}
)
STEP_NON_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float),
}
)
STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{
vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean,

View File

@@ -503,6 +503,8 @@ 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

@@ -71,7 +71,7 @@ class FeatureAutoStartStopManager(BaseFeatureManager):
)
@overrides
def start_listening(self):
async def start_listening(self):
"""Start listening the underlying entity"""
@overrides

View File

@@ -86,7 +86,7 @@ class FeatureMotionManager(BaseFeatureManager):
self._motion_state = STATE_UNKNOWN
@overrides
def start_listening(self):
async def start_listening(self):
"""Start listening the underlying entity"""
if self._is_configured:
self.stop_listening()

View File

@@ -12,22 +12,15 @@ 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__)
@@ -50,194 +43,94 @@ 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
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._device_power
):
@overrides
async 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
)
if self._use_power_feature and self._device_power and central_power_configuration:
self._is_configured = True
self._overpowering_state = STATE_UNKNOWN
# Try to restore _overpowering_state from previous state
old_state = await self._vtherm.async_get_last_state()
self._overpowering_state = STATE_ON if old_state and old_state.attributes and old_state.attributes.get("overpowering_state") == STATE_ON else STATE_UNKNOWN
else:
_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)
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,
)
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": self._power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
"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,
"overpowering_state": self._overpowering_state,
"is_power_configured": self._is_configured,
"device_power": self._device_power,
"power_temp": self._power_temp,
"current_power": self._current_power,
"current_max_power": self._current_max_power,
"current_power": vtherm_api.central_power_manager.current_power,
"current_max_power": vtherm_api.central_power_manager.current_max_power,
"mean_cycle_power": self.mean_cycle_power,
}
)
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'
async def check_power_available(self) -> bool:
"""Check if the Vtherm can be started considering overpowering.
Returns True if no overpowering conditions are found
"""
if not self._is_configured:
return False
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
if (
self._current_power is None
not self._is_configured
or not vtherm_api.central_power_manager.is_configured
):
return True
current_power = vtherm_api.central_power_manager.current_power
current_max_power = vtherm_api.central_power_manager.current_max_power
if (
current_power is None
or current_max_power is None
or self._device_power is None
or self._current_max_power is None
):
_LOGGER.warning(
"%s - power not valued. check_overpowering not available", self
"%s - power not valued. check_power_available not available", self
)
return False
return True
_LOGGER.debug(
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
self,
self._current_power,
self._current_max_power,
current_power,
current_max_power,
self._device_power,
)
@@ -253,62 +146,78 @@ class FeaturePowerManager(BaseFeatureManager):
self._device_power * self._vtherm.proportional_algorithm.on_percent,
)
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
):
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:
_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)
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER, force=True)
self._vtherm.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": self._current_power,
"current_power": current_power,
"device_power": self._device_power,
"current_max_power": self._current_max_power,
"current_max_power": current_max_power,
"current_power_consumption": power_consumption_max,
},
)
# 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
):
elif not overpowering and self.is_overpowering_detected:
_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(False)
await self._vtherm.restore_hvac_mode()
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": self._current_power,
"current_power": current_power,
"device_power": self._device_power,
"current_max_power": self._current_max_power,
"current_max_power": current_max_power,
},
)
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
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()
@overrides
@property
@@ -325,14 +234,9 @@ class FeaturePowerManager(BaseFeatureManager):
return self._overpowering_state
@property
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
def is_overpowering_detected(self) -> str | None:
"""Return True if the Vtherm is in overpowering state"""
return self._overpowering_state == STATE_ON
@property
def power_temperature(self) -> bool:
@@ -344,16 +248,6 @@ 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

@@ -67,7 +67,7 @@ class FeaturePresenceManager(BaseFeatureManager):
self._presence_state = STATE_UNKNOWN
@overrides
def start_listening(self):
async def start_listening(self):
"""Start listening the underlying entity"""
if self._is_configured:
self.stop_listening()

View File

@@ -70,7 +70,7 @@ class FeatureSafetyManager(BaseFeatureManager):
self._is_configured = True
@overrides
def start_listening(self):
async def start_listening(self):
"""Start listening the underlying entity"""
@overrides

View File

@@ -124,7 +124,7 @@ class FeatureWindowManager(BaseFeatureManager):
self._window_state = STATE_UNKNOWN
@overrides
def start_listening(self):
async def start_listening(self):
"""Start listening the underlying entity"""
if self._is_configured:
self.stop_listening()

View File

@@ -263,14 +263,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
"""True if the Thermostat is regulated by valve"""
return True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
# @property
# def hvac_modes(self) -> list[HVACMode]:
# """Get the hvac_modes"""

View File

@@ -26,23 +26,21 @@ _LOGGER = logging.getLogger(__name__)
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""Representation of a base class for a Versatile Thermostat over a switch."""
_entity_component_unrecorded_attributes = (
BaseThermostat._entity_component_unrecorded_attributes.union(
frozenset(
{
"is_over_switch",
"is_inversed",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
"calculated_on_percent",
}
)
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
frozenset(
{
"is_over_switch",
"is_inversed",
"underlying_entities",
"on_time_sec",
"off_time_sec",
"cycle_min",
"function",
"tpi_coef_int",
"tpi_coef_ext",
"power_percent",
"calculated_on_percent",
}
)
)
@@ -61,14 +59,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
"""True if the switch is inversed (for pilot wire and diode)"""
return self._is_inversed is True
@property
def power_percent(self) -> float | None:
"""Get the current on_percent value"""
if self._prop_algorithm:
return round(self._prop_algorithm.on_percent * 100, 0)
else:
return None
@overrides
def post_init(self, config_entry: ConfigData):
"""Initialize the Thermostat"""

View File

@@ -190,6 +190,11 @@ 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"""
@@ -409,9 +414,10 @@ 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,6 +1,7 @@
""" The API of Versatile Thermostat"""
import logging
from datetime import datetime
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
@@ -16,8 +17,11 @@ 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__)
@@ -62,6 +66,12 @@ 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"""
@@ -176,6 +186,10 @@ 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:
await 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
@@ -289,3 +303,18 @@ 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

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

View File

@@ -592,7 +592,10 @@ class MockNumber(NumberEntity):
async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str
hass: HomeAssistant,
entry: MockConfigEntry,
entity_id: str,
temps: dict | None = None,
) -> BaseThermostat:
"""Creates and return a TPI Thermostat"""
entry.add_to_hass(hass)
@@ -601,6 +604,11 @@ 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
@@ -741,9 +749,11 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
)
},
)
await entity.power_manager._async_power_sensor_changed(power_event)
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
await vtherm_api.central_power_manager._power_sensor_changed(power_event)
await vtherm_api.central_power_manager._do_immediate_shedding()
if sleep:
await asyncio.sleep(0.1)
await entity.hass.async_block_till_done()
async def send_max_power_change_event(
@@ -767,9 +777,11 @@ async def send_max_power_change_event(
)
},
)
await entity.power_manager._async_max_power_sensor_changed(power_event)
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
await vtherm_api.central_power_manager._max_power_sensor_changed(power_event)
await vtherm_api.central_power_manager._do_immediate_shedding()
if sleep:
await asyncio.sleep(0.1)
await entity.hass.async_block_till_done()
async def send_window_change_event(
@@ -1101,3 +1113,9 @@ 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,6 +19,8 @@
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
@@ -26,6 +28,12 @@ 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
@@ -35,12 +43,6 @@ 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"""
@@ -51,16 +53,6 @@ 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)
@@ -167,3 +159,24 @@ 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
# pylint: disable=wildcard-import, unused-wildcard-import, unused-argument, line-too-long, protected-access
""" Test the normal start of a Thermostat """
from unittest.mock import patch
@@ -107,9 +107,16 @@ 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",
@@ -122,9 +129,6 @@ 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,
@@ -136,15 +140,13 @@ 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"
hass, entry, "climate.theoverswitchmockname", temps
)
assert entity
@@ -153,35 +155,55 @@ async def test_overpowering_binary_sensors(
)
assert overpowering_binary_sensor
now: datetime = datetime.now(tz=get_tz(hass))
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
# Overpowering should be not set because poer have not been received
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 await entity.power_manager.check_overpowering() is False
assert entity.power_manager.is_overpowering_detected is False
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state is STATE_OFF
assert overpowering_binary_sensor.device_class == BinarySensorDeviceClass.POWER
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
# Send power mesurement
side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", 150),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 100),
},
State("unknown.entity_id", "unknown"),
)
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
# fmt: on
await send_power_change_event(entity, 150, now)
await send_max_power_change_event(entity, 100, now)
# Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state == 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
# set max power to a low value
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
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 251))
# 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, 251, now)
assert entity.power_manager.is_overpowering_detected is False
assert entity.power_manager.overpowering_state is STATE_OFF
# Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed()
assert overpowering_binary_sensor.state == STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])

View File

@@ -266,7 +266,9 @@ 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):
async def test_bug_407(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
"""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,
@@ -275,6 +277,12 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -287,9 +295,6 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
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,
@@ -301,34 +306,43 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
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"
hass, entry, "climate.theoverswitchmockname", temps
)
assert entity
tpi_algo = entity._prop_algorithm
assert tpi_algo
tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz)
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
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)
@@ -337,16 +351,17 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 18
# waits that the heater starts
await asyncio.sleep(0.1)
await hass.async_block_till_done()
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, datetime.now())
await send_max_power_change_event(entity, 110, now)
# Send power mesurement (theheater is already in the power measurement)
await send_power_change_event(entity, 100, datetime.now())
await send_power_change_event(entity, 100, now)
# No overpowering yet
assert await entity.power_manager.check_overpowering() is False
assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_OFF
@@ -359,36 +374,57 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
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()
assert await entity.power_manager.check_overpowering() is False
# simulate a refresh for central power (not necessary)
await do_central_power_refresh(hass)
assert entity.power_manager.is_overpowering_detected is False
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.target_temperature == 19
assert mock_service_call.call_count >= 1
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
# 3. if heater is stopped (is_device_active==False) and power is over max, then overpowering should be started
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 150))
with patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"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)
assert await entity.power_manager.check_overpowering() is True
# simulate a refresh for central power (not necessary)
await do_central_power_refresh(hass)
assert entity.power_manager.is_overpowering_detected is False
assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_POWER
assert entity.power_manager.overpowering_state is STATE_ON
assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_OFF
@pytest.mark.parametrize("expected_lingering_tasks", [True])

View File

@@ -188,6 +188,18 @@ 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,
@@ -202,19 +214,11 @@ 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_HEATER: "switch.mock_switch",
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
@@ -233,8 +237,6 @@ 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,
@@ -249,7 +251,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"
hass, entry, "climate.theoverswitchmockname", temps
)
assert entity
assert entity.name == "TheOverSwitchMockName"
@@ -300,10 +302,13 @@ async def test_full_over_switch_wo_central_config(
assert entity.motion_manager.motion_preset == "comfort"
assert entity.motion_manager.no_motion_preset == "eco"
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
assert (
entity.power_manager.max_power_sensor_entity_id
== "sensor.mock_max_power_sensor"
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
)
assert (
@@ -317,7 +322,7 @@ async def test_full_over_switch_wo_central_config(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_full_over_switch_with_central_config(
hass: HomeAssistant, skip_hass_states_is_state, init_central_config
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
"""Tests that a VTherm with central_configuration is working with the central_config attributes"""
# Add a Switch VTherm
@@ -334,15 +339,11 @@ 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_HEATER: "switch.mock_switch",
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_INVERSE_SWITCH: False,
CONF_TPI_COEF_INT: 0.3,
@@ -361,8 +362,6 @@ 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,
@@ -426,10 +425,13 @@ 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 (
entity.power_manager.max_power_sensor_entity_id
== "sensor.mock_max_power_sensor"
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"
)
assert (

View File

@@ -0,0 +1,702 @@
# pylint: disable=protected-access, unused-argument, line-too-long
""" Test the Central Power management """
from unittest.mock import patch, AsyncMock, MagicMock, PropertyMock
from datetime import datetime, timedelta
import logging
from custom_components.versatile_thermostat.feature_power_manager import (
FeaturePowerManager,
)
from custom_components.versatile_thermostat.central_feature_power_manager import (
CentralFeaturePowerManager,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize(
"use_power_feature, power_entity_id, max_power_entity_id, power_temp, is_configured",
[
(True, "sensor.power_id", "sensor.max_power_id", 13, True),
(True, None, "sensor.max_power_id", 13, False),
(True, "sensor.power_id", None, 13, False),
(True, "sensor.power_id", "sensor.max_power_id", None, False),
(False, "sensor.power_id", "sensor.max_power_id", 13, False),
],
)
async def test_central_power_manager_init(
hass: HomeAssistant,
use_power_feature,
power_entity_id,
max_power_entity_id,
power_temp,
is_configured,
):
"""Test creation and post_init of the Central Power Manager"""
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
assert central_power_manager.is_configured is False
assert central_power_manager.current_max_power is None
assert central_power_manager.current_power is None
assert central_power_manager.power_temperature is None
assert central_power_manager.name == "centralPowerManager"
# 2. post_init
central_power_manager.post_init(
{
CONF_POWER_SENSOR: power_entity_id,
CONF_MAX_POWER_SENSOR: max_power_entity_id,
CONF_USE_POWER_FEATURE: use_power_feature,
CONF_PRESET_POWER: power_temp,
}
)
assert central_power_manager.is_configured == is_configured
assert central_power_manager.current_max_power is None
assert central_power_manager.current_power is None
assert central_power_manager.power_temperature == power_temp
# 3. start listening
await central_power_manager.start_listening()
assert len(central_power_manager._active_listener) == (2 if is_configured else 0)
# 4. stop listening
central_power_manager.stop_listening()
assert len(central_power_manager._active_listener) == 0
@pytest.mark.parametrize(
"vtherm_configs, results",
[
# simple sort
(
[
{
"name": "vtherm1",
"is_configured": True,
"is_on": True,
"current_temperature": 13,
"target_temperature": 12,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
{
"name": "vtherm2",
"is_configured": True,
"is_on": True,
"current_temperature": 18,
"target_temperature": 12,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
{
"name": "vtherm3",
"is_configured": True,
"is_on": True,
"current_temperature": 12,
"target_temperature": 18,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
],
["vtherm2", "vtherm1", "vtherm3"],
),
# Ignore power not configured and not on
(
[
{
"name": "vtherm1",
"is_configured": False,
"is_on": True,
"current_temperature": 13,
"target_temperature": 12,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
{
"name": "vtherm2",
"is_configured": True,
"is_on": False,
"current_temperature": 18,
"target_temperature": 12,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
{
"name": "vtherm3",
"is_configured": True,
"is_on": True,
"current_temperature": 12,
"target_temperature": 18,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
],
["vtherm3"],
),
# None current_temperature are in last
(
[
{
"name": "vtherm1",
"is_configured": True,
"is_on": True,
"current_temperature": 13,
"target_temperature": 12,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
{
"name": "vtherm2",
"is_configured": True,
"is_on": True,
"current_temperature": None,
"target_temperature": 12,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
{
"name": "vtherm3",
"is_configured": True,
"is_on": True,
"current_temperature": 12,
"target_temperature": 18,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
],
["vtherm1", "vtherm3", "vtherm2"],
),
# None target_temperature are in last
(
[
{
"name": "vtherm1",
"is_configured": True,
"is_on": True,
"current_temperature": 13,
"target_temperature": 12,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
{
"name": "vtherm2",
"is_configured": True,
"is_on": True,
"current_temperature": 18,
"target_temperature": None,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
{
"name": "vtherm3",
"is_configured": True,
"is_on": True,
"current_temperature": 12,
"target_temperature": 18,
"saved_target_temp": 18,
"is_overpowering_detected": False,
},
],
["vtherm1", "vtherm3", "vtherm2"],
),
# simple sort with overpowering detected
(
[
{
"name": "vtherm1",
"is_configured": True,
"is_on": True,
"current_temperature": 13,
# "target_temperature": 12,
"saved_target_temp": 21,
"is_overpowering_detected": True,
},
{
"name": "vtherm2",
"is_configured": True,
"is_on": True,
"current_temperature": 18,
# "target_temperature": 12,
"saved_target_temp": 17,
"is_overpowering_detected": True,
},
{
"name": "vtherm3",
"is_configured": True,
"is_on": True,
"current_temperature": 12,
# "target_temperature": 18,
"saved_target_temp": 16,
"is_overpowering_detected": True,
},
],
["vtherm2", "vtherm3", "vtherm1"],
),
],
)
async def test_central_power_manageer_find_vtherms(
hass: HomeAssistant, vtherm_configs, results
):
"""Test the find_all_vtherm_with_power_management_sorted_by_dtemp"""
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
vtherms = []
for vtherm_config in vtherm_configs:
vtherm = MagicMock(spec=BaseThermostat)
vtherm.name = vtherm_config.get("name")
vtherm.is_on = vtherm_config.get("is_on")
vtherm.current_temperature = vtherm_config.get("current_temperature")
vtherm.target_temperature = vtherm_config.get("target_temperature")
vtherm.saved_target_temp = vtherm_config.get("saved_target_temp")
vtherm.power_manager.is_configured = vtherm_config.get("is_configured")
vtherm.power_manager.is_overpowering_detected = vtherm_config.get("is_overpowering_detected")
vtherms.append(vtherm)
with patch(
"custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.get_climate_components_entities",
return_value=vtherms,
):
vtherm_sorted = (
central_power_manager.find_all_vtherm_with_power_management_sorted_by_dtemp()
)
# extract results
vtherm_results = [vtherm.name for vtherm in vtherm_sorted]
assert vtherm_results == results
@pytest.mark.parametrize(
"current_power, current_max_power, vtherm_configs, expected_results",
[
# simple nominal test (initialize overpowering state in VTherm)
(
1000,
5000,
[
{
"name": "vtherm1",
"device_power": 100,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 0,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
{
"name": "vtherm2",
"device_power": 10000,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 100,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
{
"name": "vtherm3",
"device_power": 5000,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
{"name": "vtherm4", "device_power": 1000, "is_device_active": True, "is_over_climate": True, "is_overpowering_detected": False, "overpowering_state": STATE_OFF},
],
# init vtherm1 to False
{"vtherm3": False, "vtherm2": False, "vtherm1": False},
),
# Un-shedding only (will be taken in reverse order)
(
1000,
2000,
[
# should be not unshedded (too much power will be added)
{
"name": "vtherm1",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# already stay unshedded cause already unshedded
{
"name": "vtherm2",
"device_power": 100,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should be unshedded
{
"name": "vtherm3",
"device_power": 200,
"is_device_active": False,
"is_over_climate": True,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# should be unshedded
{
"name": "vtherm4",
"device_power": 300,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
],
{"vtherm4": False, "vtherm3": False},
),
# Shedding
(
2000,
1000,
[
# should be overpowering
{
"name": "vtherm1",
"device_power": 300,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should be overpowering with many underlmying entities
{
"name": "vtherm2",
"device_power": 400,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
# over_climate should be overpowering
{
"name": "vtherm3",
"device_power": 100,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_OFF,
},
# should pass cause not active
{
"name": "vtherm4",
"device_power": 800,
"is_device_active": False,
"is_over_climate": False,
"nb_underlying_entities": 1,
"on_percent": 1,
"is_overpowering_detected": False,
},
# should be not overpowering (already overpowering)
{
"name": "vtherm5",
"device_power": 400,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": True,
"overpowering_state": STATE_ON,
},
# should be overpowering with many underluying entities
{
"name": "vtherm6",
"device_power": 400,
"is_device_active": True,
"is_over_climate": False,
"nb_underlying_entities": 4,
"on_percent": 0.1,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
# should not be overpowering (we have enough)
{
"name": "vtherm7",
"device_power": 1000,
"is_device_active": True,
"is_over_climate": True,
"is_overpowering_detected": False,
"overpowering_state": STATE_UNKNOWN,
},
],
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm6": True},
),
],
)
# @pytest.mark.skip
async def test_central_power_manageer_calculate_shedding(
hass: HomeAssistant,
current_power,
current_max_power,
vtherm_configs,
expected_results,
):
"""Test the calculate_shedding of the CentralPowerManager"""
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
registered_calls = {}
def register_call(vtherm, overpowering):
"""Register a call to set_overpowering"""
registered_calls.update({vtherm.name: overpowering})
vtherms = []
for vtherm_config in vtherm_configs:
vtherm = MagicMock(spec=BaseThermostat)
vtherm.name = vtherm_config.get("name")
vtherm.is_device_active = vtherm_config.get("is_device_active")
vtherm.is_over_climate = vtherm_config.get("is_over_climate")
vtherm.nb_underlying_entities = vtherm_config.get("nb_underlying_entities")
if not vtherm_config.get("is_over_climate"):
vtherm.proportional_algorithm = MagicMock()
vtherm.on_percent = vtherm.proportional_algorithm.on_percent = vtherm_config.get("on_percent")
else:
vtherm.on_percent = None
vtherm.proportional_algorithm = None
vtherm.power_manager = MagicMock(spec=FeaturePowerManager)
vtherm.power_manager._vtherm = vtherm
vtherm.power_manager.is_overpowering_detected = vtherm_config.get(
"is_overpowering_detected"
)
vtherm.power_manager.device_power = vtherm_config.get("device_power")
vtherm.power_manager.overpowering_state = vtherm_config.get("overpowering_state")
async def mock_set_overpowering(
overpowering, power_consumption_max=0, v=vtherm
):
register_call(v, overpowering)
vtherm.power_manager.set_overpowering = mock_set_overpowering
vtherms.append(vtherm)
# fmt:off
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.find_all_vtherm_with_power_management_sorted_by_dtemp", return_value=vtherms), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=current_max_power), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=current_power), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.is_configured", new_callable=PropertyMock, return_value=True):
# fmt:on
await central_power_manager.calculate_shedding()
# Check registered calls
assert registered_calls == expected_results
@pytest.mark.parametrize(
"dsecs, power, nb_call",
[
(0, 1000, 1),
(0, None, 0),
(0, STATE_UNAVAILABLE, 0),
(0, STATE_UNKNOWN, 0),
(21, 1000, 1),
(19, 1000, 1),
],
)
async def test_central_power_manager_power_event(
hass: HomeAssistant, dsecs, power, nb_call
):
"""Tests the Power sensor event"""
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
assert central_power_manager.current_power is None
assert central_power_manager.power_temperature is None
assert central_power_manager.name == "centralPowerManager"
# 2. post_init
central_power_manager.post_init(
{
CONF_POWER_SENSOR: "sensor.power_entity_id",
CONF_MAX_POWER_SENSOR: "sensor.max_power_entity_id",
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 13,
}
)
assert central_power_manager.is_configured is True
assert central_power_manager.current_max_power is None
assert central_power_manager.current_power is None
assert central_power_manager.power_temperature == 13
# 3. start listening (not really useful but don't eat bread)
await central_power_manager.start_listening()
assert len(central_power_manager._active_listener) == 2
now: datetime = NowClass.get_now(hass)
# vtherm_api._set_now(now) vtherm_api is a MagicMock
vtherm_api.now = now
# 4. Call the _power_sensor_changed
side_effects = SideEffects(
{
"sensor.power_entity_id": State("sensor.power_entity_id", power),
"sensor.max_power_entity_id": State("sensor.max_power_entity_id", power),
},
State("unknown.entity_id", "unknown"),
)
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
# fmt:on
# set a default value to see if it has been replaced
central_power_manager._current_power = -999
await central_power_manager._power_sensor_changed(event=Event(
event_type=EVENT_STATE_CHANGED,
data={
"entity_id": "sensor.power_entity_id",
"new_state": State("sensor.power_entity_id", power),
"old_state": State("sensor.power_entity_id", STATE_UNAVAILABLE),
}))
if nb_call > 0:
await central_power_manager._do_immediate_shedding()
expected_power = power if isinstance(power, (int, float)) else -999
assert central_power_manager.current_power == expected_power
assert mock_calculate_shedding.call_count == nb_call
# Do another call x seconds later
now = now + timedelta(seconds=dsecs)
vtherm_api.now = now
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
# fmt:on
central_power_manager._current_power = -999
await central_power_manager._power_sensor_changed(event=Event(
event_type=EVENT_STATE_CHANGED,
data={
"entity_id": "sensor.power_entity_id",
"new_state": State("sensor.power_entity_id", power),
"old_state": State("sensor.power_entity_id", STATE_UNAVAILABLE),
}))
if nb_call > 0:
await central_power_manager._do_immediate_shedding()
assert central_power_manager.current_power == expected_power
assert mock_calculate_shedding.call_count == nb_call
@pytest.mark.parametrize(
"dsecs, max_power, nb_call",
[
(0, 1000, 1),
(0, None, 0),
(0, STATE_UNAVAILABLE, 0),
(0, STATE_UNKNOWN, 0),
(21, 1000, 1),
(19, 1000, 1),
],
)
async def test_central_power_manager_max_power_event(
hass: HomeAssistant, dsecs, max_power, nb_call
):
"""Tests the Power sensor event"""
vtherm_api: VersatileThermostatAPI = MagicMock(spec=VersatileThermostatAPI)
central_power_manager = CentralFeaturePowerManager(hass, vtherm_api)
assert central_power_manager.current_power is None
assert central_power_manager.power_temperature is None
assert central_power_manager.name == "centralPowerManager"
# 2. post_init
central_power_manager.post_init(
{
CONF_POWER_SENSOR: "sensor.power_entity_id",
CONF_MAX_POWER_SENSOR: "sensor.max_power_entity_id",
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 13,
}
)
assert central_power_manager.is_configured is True
assert central_power_manager.current_max_power is None
assert central_power_manager.current_power is None
assert central_power_manager.power_temperature == 13
# 3. start listening (not really useful but don't eat bread)
await central_power_manager.start_listening()
assert len(central_power_manager._active_listener) == 2
now: datetime = NowClass.get_now(hass)
# vtherm_api._set_now(now) vtherm_api is a MagicMock
vtherm_api.now = now
# 4. Call the _power_sensor_changed
side_effects = SideEffects(
{
"sensor.power_entity_id": State("sensor.power_entity_id", max_power),
"sensor.max_power_entity_id": State(
"sensor.max_power_entity_id", max_power
),
},
State("unknown.entity_id", "unknown"),
)
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
# fmt:on
# set a default value to see if it has been replaced
central_power_manager._current_max_power = -999
await central_power_manager._power_sensor_changed(event=Event(
event_type=EVENT_STATE_CHANGED,
data={
"entity_id": "sensor.max_power_entity_id",
"new_state": State("sensor.max_power_entity_id", max_power),
"old_state": State("sensor.max_power_entity_id", STATE_UNAVAILABLE),
}))
if nb_call > 0:
await central_power_manager._do_immediate_shedding()
expected_power = max_power if isinstance(max_power, (int, float)) else -999
assert central_power_manager.current_max_power == expected_power
assert mock_calculate_shedding.call_count == nb_call
# Do another call x seconds later
now = now + timedelta(seconds=dsecs)
vtherm_api.now = now
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.calculate_shedding", new_callable=AsyncMock) as mock_calculate_shedding:
# fmt:on
central_power_manager._current_max_power = -999
await central_power_manager._power_sensor_changed(event=Event(
event_type=EVENT_STATE_CHANGED,
data={
"entity_id": "sensor.max_power_entity_id",
"new_state": State("sensor.max_power_entity_id", max_power),
"old_state": State("sensor.max_power_entity_id", STATE_UNAVAILABLE),
}))
if nb_call > 0:
await central_power_manager._do_immediate_shedding()
assert central_power_manager.current_max_power == expected_power
assert mock_calculate_shedding.call_count == nb_call

View File

@@ -90,7 +90,7 @@ async def test_motion_feature_manager_refresh(
assert custom_attributes["motion_off_delay_sec"] == 30
# 3. start listening
motion_manager.start_listening()
await motion_manager.start_listening()
assert motion_manager.is_configured is True
assert motion_manager.motion_state == STATE_UNKNOWN
assert motion_manager.is_motion_detected is False
@@ -198,7 +198,7 @@ async def test_motion_feature_manager_event(
CONF_NO_MOTION_PRESET: PRESET_ECO,
}
)
motion_manager.start_listening()
await motion_manager.start_listening()
# 2. test _motion_sensor_changed with the parametrized
# fmt: off

View File

@@ -721,10 +721,14 @@ async def test_multiple_climates_underlying_changes_not_aligned(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_multiple_switch_power_management(
hass: HomeAssistant, skip_hass_states_is_state
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
"""Test the Power management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -737,17 +741,16 @@ 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_HEATER: "switch.mock_switch1",
CONF_HEATER_2: "switch.mock_switch2",
CONF_HEATER_3: "switch.mock_switch3",
CONF_HEATER_4: "switch.mock_switch4",
CONF_UNDERLYING_LIST: [
"switch.mock_switch1",
"switch.mock_switch2",
"switch.mock_switch3",
"switch.mock_switch4",
],
CONF_HEATER_KEEP_ALIVE: 0,
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
@@ -755,15 +758,13 @@ 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"
hass, entry, "climate.theover4switchmockname", temps
)
assert entity
assert entity.is_over_climate is False
@@ -772,6 +773,9 @@ 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
@@ -779,77 +783,109 @@ async def test_multiple_switch_power_management(
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19
# make the heater heats
await send_temperature_change_event(entity, 15, now)
await send_ext_temperature_change_event(entity, 1, now)
await hass.async_block_till_done()
# 1. Send power mesurement
await send_power_change_event(entity, 50, datetime.now())
side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
},
State("unknown.entity_id", "unknown"),
)
# Send power max mesurement
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
# 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
# 2. Send power max mesurement too low and HVACMode is on
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
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
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
#fmt: off
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
#fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
assert entity.power_percent > 0
# 100 of the device / 4 -> 25, current power 50 so max is 75
await send_max_power_change_event(entity, 49, datetime.now())
assert entity.power_manager.is_overpowering_detected is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
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": 49,
"current_power_consumption": 100,
},
),
],
any_order=True,
)
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 4 # The fourth are shutdown
# 3. change PRESET
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
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 cause temperature is very low
assert entity.power_manager.overpowering_state is STATE_ON
# 4. Send hugh power max mesurement to release overpowering
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
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 150))
assert (
mock_heater_on.call_count == 0
) # The fourth are not restarted because temperature is enought
assert mock_heater_off.call_count == 0
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

View File

@@ -10,6 +10,7 @@ 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
@@ -17,28 +18,28 @@ logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize(
"is_over_climate, is_device_active, power, max_power, current_overpowering_state, overpowering_state, nb_call, changed, check_overpowering_ret",
"is_over_climate, is_device_active, power, max_power, check_power_available",
[
# don't switch to overpower (power is enough)
(False, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
(False, False, 1000, 3000, True),
# switch to overpower (power is not enough)
(False, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
(False, False, 2000, 3000, False),
# don't switch to overpower (power is not enough but device is already on)
(False, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
(False, True, 2000, 3000, True),
# Same with a over_climate
# don't switch to overpower (power is enough)
(True, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
(True, False, 1000, 3000, True),
# switch to overpower (power is not enough)
(True, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
(True, False, 2000, 3000, False),
# don't switch to overpower (power is not enough but device is already on)
(True, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
(True, True, 2000, 3000, True),
# Leave overpowering state
# switch to not overpower (power is enough)
(False, False, 1000, 3000, STATE_ON, STATE_OFF, 1, True, False),
(False, False, 1000, 3000, True),
# don't switch to overpower (power is still not enough)
(False, False, 2000, 3000, STATE_ON, STATE_ON, 0, True, True),
(False, False, 2000, 3000, False),
# keep overpower (power is not enough but device is already on)
(False, True, 3000, 3000, STATE_ON, STATE_ON, 0, True, True),
(False, True, 3000, 3000, False),
],
)
async def test_power_feature_manager(
@@ -47,17 +48,15 @@ async def test_power_feature_manager(
is_device_active,
power,
max_power,
current_overpowering_state,
overpowering_state,
nb_call,
changed,
check_overpowering_ret,
check_power_available,
):
"""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)
@@ -80,16 +79,27 @@ async def test_power_feature_manager(
assert custom_attributes["current_max_power"] is None
# 2. post_init
power_manager.post_init(
vtherm_api.find_central_configuration = MagicMock()
vtherm_api.central_power_manager.post_init(
{
CONF_POWER_SENSOR: "sensor.the_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 13,
}
)
assert vtherm_api.central_power_manager.is_configured
power_manager.post_init(
{
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 10,
CONF_DEVICE_POWER: 1234,
}
)
await power_manager.start_listening()
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
@@ -107,25 +117,18 @@ async def test_power_feature_manager(
assert custom_attributes["current_max_power"] is None
# 3. start listening
power_manager.start_listening()
await power_manager.start_listening()
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
assert len(power_manager._active_listener) == 2
assert len(power_manager._active_listener) == 0 # no more listening
# 4. test refresh and check_overpowering with the parametrized
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("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()) as mock_get_state:
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=max_power), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=power):
# fmt:on
# 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"
@@ -134,8 +137,84 @@ async def test_power_feature_manager(
type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate)
type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo)
type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1)
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
type(fake_vtherm)._saved_preset_mode = PropertyMock(return_value=PRESET_ECO)
ret = await power_manager.check_power_available()
assert ret == check_power_available
@pytest.mark.parametrize(
"is_over_climate, current_overpowering_state, is_overpowering, new_overpowering_state, msg_sent",
[
# false -> false
(False, STATE_OFF, False, STATE_OFF, False),
# false -> true
(False, STATE_OFF, True, STATE_ON, True),
# true -> true
(False, STATE_ON, True, STATE_ON, False),
# true -> False
(False, STATE_ON, False, STATE_OFF, True),
# Same with over_climate
# false -> false
(True, STATE_OFF, False, STATE_OFF, False),
# false -> true
(True, STATE_OFF, True, STATE_ON, True),
# true -> true
(True, STATE_ON, True, STATE_ON, False),
# true -> False
(True, STATE_ON, False, STATE_OFF, True),
],
)
async def test_power_feature_manager_set_overpowering(
hass,
is_over_climate,
current_overpowering_state,
is_overpowering,
new_overpowering_state,
msg_sent,
):
"""Test the set_overpowering method of FeaturePowerManager"""
fake_vtherm = MagicMock(spec=BaseThermostat)
type(fake_vtherm).name = PropertyMock(return_value="the name")
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# 1. creation / init
power_manager = FeaturePowerManager(fake_vtherm, hass)
vtherm_api.find_central_configuration = MagicMock()
vtherm_api.central_power_manager.post_init(
{
CONF_POWER_SENSOR: "sensor.the_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 13,
}
)
assert vtherm_api.central_power_manager.is_configured
power_manager.post_init(
{
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 10,
CONF_DEVICE_POWER: 1234,
}
)
await power_manager.start_listening()
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
# check overpowering
power_manager._overpowering_state = current_overpowering_state
# fmt:off
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=2000), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=1000):
# fmt:on
# Finish mocking
fake_vtherm.is_over_climate = is_over_climate
fake_vtherm.preset_mode = MagicMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
fake_vtherm._saved_preset_mode = PRESET_ECO
fake_vtherm.save_hvac_mode = MagicMock()
fake_vtherm.restore_hvac_mode = AsyncMock()
@@ -147,26 +226,17 @@ async def test_power_feature_manager(
fake_vtherm.update_custom_attributes = MagicMock()
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
# Call set_overpowering
await power_manager.set_overpowering(is_overpowering, 1234)
# 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
assert power_manager.overpowering_state == new_overpowering_state
if power_manager.overpowering_state == STATE_OFF:
if not is_overpowering:
assert power_manager.overpowering_state == STATE_OFF
assert fake_vtherm.save_hvac_mode.call_count == 0
assert fake_vtherm.save_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
@@ -178,18 +248,24 @@ async def test_power_feature_manager(
else:
assert fake_vtherm.update_custom_attributes.call_count == 0
if nb_call == 1:
if msg_sent:
fake_vtherm.send_event.assert_has_calls(
[
call.fake_vtherm.send_event(
EventType.POWER_EVENT,
{'type': 'end', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power}),
{
"type": "end",
"current_power": 1000,
"device_power": 1234,
"current_max_power": 2000,
},
),
]
)
elif power_manager.overpowering_state == STATE_ON:
if is_over_climate:
# is_overpowering is True
else:
assert power_manager.overpowering_state == STATE_ON
if is_over_climate and current_overpowering_state == STATE_OFF:
assert fake_vtherm.save_hvac_mode.call_count == 1
else:
assert fake_vtherm.save_hvac_mode.call_count == 0
@@ -209,30 +285,37 @@ async def test_power_feature_manager(
assert fake_vtherm.restore_hvac_mode.call_count == 0
assert fake_vtherm.restore_preset_mode.call_count == 0
if nb_call == 1:
if msg_sent:
fake_vtherm.send_event.assert_has_calls(
[
call.fake_vtherm.send_event(
EventType.POWER_EVENT,
{'type': 'start', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power, 'current_power_consumption': 1234.0}),
{
"type": "start",
"current_power": 1000,
"device_power": 1234,
"current_max_power": 2000,
"current_power_consumption": 1234.0,
},
),
]
)
fake_vtherm.reset_mock()
# 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"] == overpowering_state
assert custom_attributes["overpowering_state"] == new_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"] == power
assert custom_attributes["current_max_power"] == max_power
assert custom_attributes["current_power"] == 1000
assert custom_attributes["current_max_power"] == 2000
power_manager.stop_listening()
await hass.async_block_till_done()
@@ -241,10 +324,15 @@ async def test_power_feature_manager(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_off(
hass: HomeAssistant, skip_hass_states_is_state
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
"""Test the Power management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -257,29 +345,24 @@ 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_HEATER: "switch.mock_switch",
CONF_UNDERLYING_LIST: ["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"
hass, entry, "climate.theoverswitchmockname", temps
)
assert entity
@@ -292,34 +375,53 @@ 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
await send_power_change_event(entity, 50, datetime.now())
assert await entity.power_manager.check_overpowering() is False
# 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
# All configuration is not complete
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
# 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
# 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
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 too low but HVACMode is off
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:
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)
await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.power_manager.check_overpowering() is True
assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_ON
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0
@@ -328,9 +430,17 @@ async def test_power_management_hvac_off(
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state):
async def test_power_management_hvac_on(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
"""Test the Power management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -343,32 +453,30 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
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_HEATER: "switch.mock_switch",
CONF_UNDERLYING_LIST: ["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"
hass, entry, "climate.theoverswitchmockname", temps
)
assert entity
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
tpi_algo = entity._prop_algorithm
assert tpi_algo
@@ -379,25 +487,49 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19
# make the heater heats
await send_temperature_change_event(entity, 15, now)
await send_ext_temperature_change_event(entity, 1, now)
await hass.async_block_till_done()
assert entity.power_percent > 0
# Send power mesurement
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
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
# Send power max mesurement too low and HVACMode is on
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 await entity.power_manager.check_overpowering() is True
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
# 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, \
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
# fmt: on
now = now + timedelta(seconds=30)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 49, now)
assert entity.power_manager.is_overpowering_detected is True
# All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER
assert entity.power_manager.overpowering_state is STATE_ON
@@ -413,7 +545,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"type": "start",
"current_power": 50,
"device_power": 100,
"current_max_power": 149,
"current_max_power": 49,
"current_power_consumption": 100.0,
},
),
@@ -423,16 +555,20 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
assert mock_heater_on.call_count == 0
assert mock_heater_off.call_count == 1
# Send power mesurement low to unseet power preset
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 await entity.power_manager.check_overpowering() is False
# Send power mesurement low to unset power preset
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 48))
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)
await send_power_change_event(entity, 48, now)
assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is < power_max, we restore previous preset
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_OFF
@@ -462,10 +598,16 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_switch(
hass: HomeAssistant, skip_hass_states_is_state
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
"""Test the Power management energy mesurement"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry(
domain=DOMAIN,
title="TheOverSwitchMockName",
@@ -478,30 +620,24 @@ 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_HEATER: "switch.mock_switch",
CONF_HEATER_2: "switch.mock_switch2",
CONF_UNDERLYING_LIST: ["switch.mock_switch", "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"
hass, entry, "climate.theoverswitchmockname", temps
)
assert entity
@@ -523,6 +659,8 @@ 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
@@ -594,6 +732,12 @@ 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",
@@ -611,14 +755,11 @@ 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_CLIMATE: "climate.mock_climate",
CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3,
@@ -630,7 +771,7 @@ async def test_power_management_energy_over_climate(
)
entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverclimatemockname"
hass, entry, "climate.theoverclimatemockname", temps
)
assert entity
assert entity.is_over_climate

View File

@@ -75,7 +75,7 @@ async def test_presence_feature_manager(
assert custom_attributes["is_presence_configured"] is True
# 3. start listening
presence_manager.start_listening()
await presence_manager.start_listening()
assert presence_manager.is_configured is True
assert presence_manager.presence_state == STATE_UNKNOWN
assert presence_manager.is_absence_detected is False

View File

@@ -170,7 +170,7 @@ async def test_window_feature_manager_refresh_sensor_action_turn_off(
)
# 3. start listening
window_manager.start_listening()
await window_manager.start_listening()
assert window_manager.is_configured is True
assert window_manager.window_state == STATE_UNKNOWN
assert window_manager.window_auto_state == STATE_UNAVAILABLE
@@ -288,7 +288,7 @@ async def test_window_feature_manager_refresh_sensor_action_frost_only(
)
# 3. start listening
window_manager.start_listening()
await window_manager.start_listening()
assert window_manager.is_configured is True
assert window_manager.window_state == STATE_UNKNOWN
assert window_manager.window_auto_state == STATE_UNAVAILABLE
@@ -408,7 +408,7 @@ async def test_window_feature_manager_sensor_event_action_turn_off(
)
# 3. start listening
window_manager.start_listening()
await window_manager.start_listening()
assert len(window_manager._active_listener) == 1
# 4. test refresh with the parametrized
@@ -535,7 +535,7 @@ async def test_window_feature_manager_event_sensor_action_frost_only(
)
# 3. start listening
window_manager.start_listening()
await window_manager.start_listening()
# 4. test refresh with the parametrized
# fmt:off
@@ -660,7 +660,7 @@ async def test_window_feature_manager_window_auto(
}
)
assert window_manager.is_window_auto_configured is True
window_manager.start_listening()
await window_manager.start_listening()
# 2. Call manage window auto
tz = get_tz(hass) # pylint: disable=invalid-name