Compare commits
4 Commits
7.1.0.beta
...
7.1.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b58e69755 | ||
|
|
81231f977c | ||
|
|
6bdcecefac | ||
|
|
34181b4204 |
@@ -27,7 +27,7 @@ class BaseFeatureManager:
|
|||||||
"""Initialize the attributes of the FeatureManager"""
|
"""Initialize the attributes of the FeatureManager"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
from .commons import ConfigData, T, deprecated
|
from .commons import ConfigData, T
|
||||||
|
|
||||||
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||||
|
|
||||||
@@ -98,7 +98,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
"comfort_away_temp",
|
"comfort_away_temp",
|
||||||
"power_temp",
|
"power_temp",
|
||||||
"ac_mode",
|
"ac_mode",
|
||||||
"current_max_power",
|
|
||||||
"saved_preset_mode",
|
"saved_preset_mode",
|
||||||
"saved_target_temp",
|
"saved_target_temp",
|
||||||
"saved_hvac_mode",
|
"saved_hvac_mode",
|
||||||
@@ -478,7 +477,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
|
|
||||||
# start listening for all managers
|
# start listening for all managers
|
||||||
for manager in self._managers:
|
for manager in self._managers:
|
||||||
manager.start_listening()
|
await manager.start_listening()
|
||||||
|
|
||||||
await self.get_my_previous_state()
|
await self.get_my_previous_state()
|
||||||
|
|
||||||
@@ -1609,14 +1608,8 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Check overpowering condition
|
# Check overpowering condition
|
||||||
await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
|
# Not usefull. Will be done at the next power refresh
|
||||||
|
# await VersatileThermostatAPI.get_vtherm_api().central_power_manager.refresh_state()
|
||||||
# TODO remove this
|
|
||||||
# overpowering is now centralized
|
|
||||||
# overpowering = await self._power_manager.check_overpowering()
|
|
||||||
# if overpowering == STATE_ON:
|
|
||||||
# _LOGGER.debug("%s - End of cycle (overpowering)", self)
|
|
||||||
# return True
|
|
||||||
|
|
||||||
safety: bool = await self._safety_manager.refresh_state()
|
safety: bool = await self._safety_manager.refresh_state()
|
||||||
if safety and self.is_over_climate:
|
if safety and self.is_over_climate:
|
||||||
@@ -1963,7 +1956,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
def _set_now(self, now: datetime):
|
def _set_now(self, now: datetime):
|
||||||
"""Set the now timestamp. This is only for tests purpose
|
"""Set the now timestamp. This is only for tests purpose
|
||||||
This method should be replaced by the vthermAPI equivalent"""
|
This method should be replaced by the vthermAPI equivalent"""
|
||||||
VersatileThermostatAPI.get_vtherm_api(self._hass)._set_now(now)
|
VersatileThermostatAPI.get_vtherm_api(self._hass)._set_now(now) # pylint: disable=protected-access
|
||||||
|
|
||||||
# @deprecated
|
# @deprecated
|
||||||
@property
|
@property
|
||||||
@@ -1974,5 +1967,17 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def power_percent(self) -> float | None:
|
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"""
|
"""Get the current on_percent value. valid only for Vtherm with a TPI algo"""
|
||||||
return None
|
if self._prop_algorithm and self._prop_algorithm.on_percent is not None:
|
||||||
|
return self._prop_algorithm.on_percent
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from functools import cmp_to_key
|
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.core import HomeAssistant, Event, callback
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_track_state_change_event,
|
async_track_state_change_event,
|
||||||
EventStateChangedData,
|
EventStateChangedData,
|
||||||
|
async_call_later,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
@@ -42,6 +46,8 @@ class CentralFeaturePowerManager(BaseFeatureManager):
|
|||||||
self._current_power: float = None
|
self._current_power: float = None
|
||||||
self._current_max_power: float = None
|
self._current_max_power: float = None
|
||||||
self._power_temp: float = None
|
self._power_temp: float = None
|
||||||
|
self._cancel_calculate_shedding_call = None
|
||||||
|
# Not used now
|
||||||
self._last_shedding_date = None
|
self._last_shedding_date = None
|
||||||
|
|
||||||
def post_init(self, entry_infos: ConfigData):
|
def post_init(self, entry_infos: ConfigData):
|
||||||
@@ -68,7 +74,7 @@ class CentralFeaturePowerManager(BaseFeatureManager):
|
|||||||
else:
|
else:
|
||||||
_LOGGER.info("Power management is not fully configured and will be deactivated")
|
_LOGGER.info("Power management is not fully configured and will be deactivated")
|
||||||
|
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the power sensor"""
|
"""Start listening the power sensor"""
|
||||||
if not self._is_configured:
|
if not self._is_configured:
|
||||||
return
|
return
|
||||||
@@ -109,45 +115,54 @@ class CentralFeaturePowerManager(BaseFeatureManager):
|
|||||||
async def refresh_state(self) -> bool:
|
async def refresh_state(self) -> bool:
|
||||||
"""Tries to get the last state from sensor
|
"""Tries to get the last state from sensor
|
||||||
Returns True if a change has been made"""
|
Returns True if a change has been made"""
|
||||||
ret = False
|
|
||||||
if self._is_configured:
|
|
||||||
# try to acquire current power and power max
|
|
||||||
if (
|
|
||||||
new_state := get_safe_float(self._hass, self._power_sensor_entity_id)
|
|
||||||
) is not None:
|
|
||||||
self._current_power = new_state
|
|
||||||
_LOGGER.debug("Current power have been retrieved: %.3f", self._current_power)
|
|
||||||
ret = True
|
|
||||||
|
|
||||||
# Try to acquire power max
|
async def _calculate_shedding_internal(_):
|
||||||
if (
|
_LOGGER.debug("Do the shedding calculation")
|
||||||
new_state := get_safe_float(
|
await self.calculate_shedding()
|
||||||
self._hass, self._max_power_sensor_entity_id
|
if self._cancel_calculate_shedding_call:
|
||||||
)
|
self._cancel_calculate_shedding_call()
|
||||||
) is not None:
|
self._cancel_calculate_shedding_call = None
|
||||||
self._current_max_power = new_state
|
|
||||||
_LOGGER.debug("Current power max have been retrieved: %.3f", self._current_max_power)
|
|
||||||
ret = True
|
|
||||||
|
|
||||||
# check if we need to re-calculate shedding
|
if not self._is_configured:
|
||||||
if ret:
|
return False
|
||||||
now = self._vtherm_api.now
|
|
||||||
dtimestamp = (
|
|
||||||
(now - self._last_shedding_date).seconds
|
|
||||||
if self._last_shedding_date
|
|
||||||
else 999
|
|
||||||
)
|
|
||||||
if dtimestamp >= MIN_DTEMP_SECS:
|
|
||||||
await self.calculate_shedding()
|
|
||||||
self._last_shedding_date = now
|
|
||||||
|
|
||||||
return ret
|
# 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):
|
async def calculate_shedding(self):
|
||||||
"""Do the shedding calculation and set/unset VTherm into overpowering state"""
|
"""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:
|
if not self.is_configured or self.current_max_power is None or self.current_power is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug("-------- Start of calculate_shedding")
|
||||||
# Find all VTherms
|
# Find all VTherms
|
||||||
available_power = self.current_max_power - self.current_power
|
available_power = self.current_max_power - self.current_power
|
||||||
vtherms_sorted = self.find_all_vtherm_with_power_management_sorted_by_dtemp()
|
vtherms_sorted = self.find_all_vtherm_with_power_management_sorted_by_dtemp()
|
||||||
@@ -163,54 +178,56 @@ class CentralFeaturePowerManager(BaseFeatureManager):
|
|||||||
total_power_gain = 0
|
total_power_gain = 0
|
||||||
|
|
||||||
for vtherm in vtherms_sorted:
|
for vtherm in vtherms_sorted:
|
||||||
device_power = vtherm.power_manager.device_power
|
|
||||||
if vtherm.is_device_active and not vtherm.power_manager.is_overpowering_detected:
|
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
|
total_power_gain += device_power
|
||||||
_LOGGER.debug("vtherm %s should be in overpowering state", vtherm.name)
|
_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)
|
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)
|
_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:
|
if total_power_gain >= -available_power:
|
||||||
_LOGGER.debug("We have found enough vtherm to set to overpowering")
|
_LOGGER.debug("We have found enough vtherm to set to overpowering")
|
||||||
break
|
break
|
||||||
|
# unshedding only
|
||||||
else:
|
else:
|
||||||
# vtherms_sorted.reverse()
|
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)
|
_LOGGER.debug("The available power is is > 0 (%s). Do a complete shedding/un-shedding calculation for list: %s", available_power, vtherms_sorted)
|
||||||
|
|
||||||
total_affected_power = 0
|
total_power_added = 0
|
||||||
force_overpowering = False
|
|
||||||
|
|
||||||
for vtherm in vtherms_sorted:
|
for vtherm in vtherms_sorted:
|
||||||
device_power = vtherm.power_manager.device_power
|
# 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
|
# calculate the power_consumption_max
|
||||||
if vtherm.is_device_active:
|
if vtherm.on_percent is not None:
|
||||||
power_consumption_max = 0
|
power_consumption_max = max(
|
||||||
else:
|
device_power / vtherm.nb_underlying_entities,
|
||||||
if vtherm.is_over_climate:
|
device_power * vtherm.on_percent,
|
||||||
power_consumption_max = device_power
|
)
|
||||||
else:
|
|
||||||
if vtherm.proportional_algorithm.on_percent > 0:
|
|
||||||
power_consumption_max = max(
|
|
||||||
device_power / vtherm.nb_underlying_entities,
|
|
||||||
device_power * vtherm.proportional_algorithm.on_percent,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
power_consumption_max = 0
|
|
||||||
|
|
||||||
_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)
|
_LOGGER.debug("vtherm %s power_consumption_max is %s (device_power=%s, overclimate=%s)", vtherm.name, power_consumption_max, device_power, vtherm.is_over_climate)
|
||||||
if force_overpowering or (total_affected_power + power_consumption_max >= available_power):
|
|
||||||
_LOGGER.debug("vtherm %s should be in overpowering state", vtherm.name)
|
# or not ... is for initializing the overpowering state if not already done
|
||||||
if not vtherm.power_manager.is_overpowering_detected:
|
if total_power_added + power_consumption_max < available_power or not vtherm.power_manager.is_overpowering_detected:
|
||||||
# To force all others vtherms to be in overpowering
|
# we count the unshedding only if the VTherm was in shedding
|
||||||
force_overpowering = True
|
if vtherm.power_manager.is_overpowering_detected:
|
||||||
await vtherm.power_manager.set_overpowering(True, power_consumption_max)
|
_LOGGER.info("vtherm %s should not be in overpowering state (power_consumption_max=%.2f)", vtherm.name, power_consumption_max)
|
||||||
else:
|
total_power_added += power_consumption_max
|
||||||
total_affected_power += power_consumption_max
|
|
||||||
# Always set to false to init the state
|
|
||||||
_LOGGER.debug("vtherm %s should not be in overpowering state", vtherm.name)
|
|
||||||
await vtherm.power_manager.set_overpowering(False)
|
await vtherm.power_manager.set_overpowering(False)
|
||||||
|
|
||||||
_LOGGER.debug("after vtherm %s total_affected_power=%s, available_power=%s", vtherm.name, total_affected_power, available_power)
|
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:
|
def get_climate_components_entities(self) -> list:
|
||||||
"""Get all VTherms entitites"""
|
"""Get all VTherms entitites"""
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class FeatureAutoStartStopManager(BaseFeatureManager):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class FeatureMotionManager(BaseFeatureManager):
|
|||||||
self._motion_state = STATE_UNKNOWN
|
self._motion_state = STATE_UNKNOWN
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
if self._is_configured:
|
if self._is_configured:
|
||||||
self.stop_listening()
|
self.stop_listening()
|
||||||
|
|||||||
@@ -61,19 +61,17 @@ class FeaturePowerManager(BaseFeatureManager):
|
|||||||
self._is_configured = False
|
self._is_configured = False
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity. There is nothing to listen"""
|
"""Start listening the underlying entity. There is nothing to listen"""
|
||||||
central_power_configuration = (
|
central_power_configuration = (
|
||||||
VersatileThermostatAPI.get_vtherm_api().central_power_manager.is_configured
|
VersatileThermostatAPI.get_vtherm_api().central_power_manager.is_configured
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if self._use_power_feature and self._device_power and central_power_configuration:
|
||||||
self._use_power_feature
|
|
||||||
and self._device_power
|
|
||||||
and central_power_configuration
|
|
||||||
):
|
|
||||||
self._is_configured = True
|
self._is_configured = True
|
||||||
self._overpowering_state = STATE_UNKNOWN
|
# Try to restore _overpowering_state from previous state
|
||||||
|
old_state = await self._vtherm.async_get_last_state()
|
||||||
|
self._overpowering_state = STATE_ON if old_state and old_state.attributes and old_state.attributes.get("overpowering_state") == STATE_ON else STATE_UNKNOWN
|
||||||
else:
|
else:
|
||||||
if self._use_power_feature:
|
if self._use_power_feature:
|
||||||
if not central_power_configuration:
|
if not central_power_configuration:
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class FeaturePresenceManager(BaseFeatureManager):
|
|||||||
self._presence_state = STATE_UNKNOWN
|
self._presence_state = STATE_UNKNOWN
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
if self._is_configured:
|
if self._is_configured:
|
||||||
self.stop_listening()
|
self.stop_listening()
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class FeatureSafetyManager(BaseFeatureManager):
|
|||||||
self._is_configured = True
|
self._is_configured = True
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class FeatureWindowManager(BaseFeatureManager):
|
|||||||
self._window_state = STATE_UNKNOWN
|
self._window_state = STATE_UNKNOWN
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def start_listening(self):
|
async def start_listening(self):
|
||||||
"""Start listening the underlying entity"""
|
"""Start listening the underlying entity"""
|
||||||
if self._is_configured:
|
if self._is_configured:
|
||||||
self.stop_listening()
|
self.stop_listening()
|
||||||
|
|||||||
@@ -263,15 +263,6 @@ class ThermostatOverClimateValve(ThermostatOverClimate):
|
|||||||
"""True if the Thermostat is regulated by valve"""
|
"""True if the Thermostat is regulated by valve"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@overrides
|
|
||||||
@property
|
|
||||||
def power_percent(self) -> float | None:
|
|
||||||
"""Get the current on_percent value"""
|
|
||||||
if self._prop_algorithm:
|
|
||||||
return round(self._prop_algorithm.on_percent * 100, 0)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# @property
|
# @property
|
||||||
# def hvac_modes(self) -> list[HVACMode]:
|
# def hvac_modes(self) -> list[HVACMode]:
|
||||||
# """Get the hvac_modes"""
|
# """Get the hvac_modes"""
|
||||||
|
|||||||
@@ -26,23 +26,21 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
||||||
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
"""Representation of a base class for a Versatile Thermostat over a switch."""
|
||||||
|
|
||||||
_entity_component_unrecorded_attributes = (
|
_entity_component_unrecorded_attributes = BaseThermostat._entity_component_unrecorded_attributes.union( # pylint: disable=protected-access
|
||||||
BaseThermostat._entity_component_unrecorded_attributes.union(
|
frozenset(
|
||||||
frozenset(
|
{
|
||||||
{
|
"is_over_switch",
|
||||||
"is_over_switch",
|
"is_inversed",
|
||||||
"is_inversed",
|
"underlying_entities",
|
||||||
"underlying_entities",
|
"on_time_sec",
|
||||||
"on_time_sec",
|
"off_time_sec",
|
||||||
"off_time_sec",
|
"cycle_min",
|
||||||
"cycle_min",
|
"function",
|
||||||
"function",
|
"tpi_coef_int",
|
||||||
"tpi_coef_int",
|
"tpi_coef_ext",
|
||||||
"tpi_coef_ext",
|
"power_percent",
|
||||||
"power_percent",
|
"calculated_on_percent",
|
||||||
"calculated_on_percent",
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,15 +59,6 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
|
|||||||
"""True if the switch is inversed (for pilot wire and diode)"""
|
"""True if the switch is inversed (for pilot wire and diode)"""
|
||||||
return self._is_inversed is True
|
return self._is_inversed is True
|
||||||
|
|
||||||
@overrides
|
|
||||||
@property
|
|
||||||
def power_percent(self) -> float | None:
|
|
||||||
"""Get the current on_percent value"""
|
|
||||||
if self._prop_algorithm:
|
|
||||||
return round(self._prop_algorithm.on_percent * 100, 0)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@overrides
|
@overrides
|
||||||
def post_init(self, config_entry: ConfigData):
|
def post_init(self, config_entry: ConfigData):
|
||||||
"""Initialize the Thermostat"""
|
"""Initialize the Thermostat"""
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ class VersatileThermostatAPI(dict):
|
|||||||
|
|
||||||
# start listening for the central power manager if not only one vtherm reload
|
# start listening for the central power manager if not only one vtherm reload
|
||||||
if not entry_id:
|
if not entry_id:
|
||||||
self.central_power_manager.start_listening()
|
await self.central_power_manager.start_listening()
|
||||||
|
|
||||||
async def init_vtherm_preset_with_central(self):
|
async def init_vtherm_preset_with_central(self):
|
||||||
"""Init all VTherm presets when the VTherm uses central temperature"""
|
"""Init all VTherm presets when the VTherm uses central temperature"""
|
||||||
|
|||||||
@@ -751,6 +751,7 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
|
|||||||
)
|
)
|
||||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||||
await vtherm_api.central_power_manager._power_sensor_changed(power_event)
|
await vtherm_api.central_power_manager._power_sensor_changed(power_event)
|
||||||
|
await vtherm_api.central_power_manager._do_immediate_shedding()
|
||||||
if sleep:
|
if sleep:
|
||||||
await entity.hass.async_block_till_done()
|
await entity.hass.async_block_till_done()
|
||||||
|
|
||||||
@@ -778,6 +779,7 @@ async def send_max_power_change_event(
|
|||||||
)
|
)
|
||||||
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
|
||||||
await vtherm_api.central_power_manager._max_power_sensor_changed(power_event)
|
await vtherm_api.central_power_manager._max_power_sensor_changed(power_event)
|
||||||
|
await vtherm_api.central_power_manager._do_immediate_shedding()
|
||||||
if sleep:
|
if sleep:
|
||||||
await entity.hass.async_block_till_done()
|
await entity.hass.async_block_till_done()
|
||||||
|
|
||||||
|
|||||||
@@ -172,16 +172,17 @@ async def test_overpowering_binary_sensors(
|
|||||||
# Send power mesurement
|
# Send power mesurement
|
||||||
side_effects = SideEffects(
|
side_effects = SideEffects(
|
||||||
{
|
{
|
||||||
"sensor.the_power_sensor": State("sensor.the_power_sensor", 100),
|
"sensor.the_power_sensor": State("sensor.the_power_sensor", 150),
|
||||||
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 150),
|
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 100),
|
||||||
},
|
},
|
||||||
State("unknown.entity_id", "unknown"),
|
State("unknown.entity_id", "unknown"),
|
||||||
)
|
)
|
||||||
# fmt:off
|
# fmt:off
|
||||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
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
|
# fmt: on
|
||||||
await send_power_change_event(entity, 100, now)
|
await send_power_change_event(entity, 150, now)
|
||||||
await send_max_power_change_event(entity, 150, now)
|
await send_max_power_change_event(entity, 100, now)
|
||||||
|
|
||||||
assert entity.power_manager.is_overpowering_detected is True
|
assert entity.power_manager.is_overpowering_detected is True
|
||||||
assert entity.power_manager.overpowering_state is STATE_ON
|
assert entity.power_manager.overpowering_state is STATE_ON
|
||||||
@@ -191,13 +192,13 @@ async def test_overpowering_binary_sensors(
|
|||||||
assert overpowering_binary_sensor.state == STATE_ON
|
assert overpowering_binary_sensor.state == STATE_ON
|
||||||
|
|
||||||
# set max power to a low value
|
# set max power to a low value
|
||||||
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 201))
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 251))
|
||||||
# fmt:off
|
# fmt:off
|
||||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
|
||||||
# fmt: on
|
# fmt: on
|
||||||
now = now + timedelta(seconds=30)
|
now = now + timedelta(seconds=30)
|
||||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
await send_max_power_change_event(entity, 201, now)
|
await send_max_power_change_event(entity, 251, now)
|
||||||
assert entity.power_manager.is_overpowering_detected is False
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
# Simulate the event reception
|
# Simulate the event reception
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ async def test_bug_407(
|
|||||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||||
assert entity.hvac_mode is HVACMode.HEAT
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.preset_mode is PRESET_COMFORT
|
assert entity.preset_mode is PRESET_COMFORT
|
||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
assert entity.power_manager.overpowering_state is STATE_UNKNOWN
|
||||||
assert entity.target_temperature == 18
|
assert entity.target_temperature == 18
|
||||||
# waits that the heater starts
|
# waits that the heater starts
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@@ -398,7 +398,8 @@ async def test_bug_407(
|
|||||||
assert entity.target_temperature == 19
|
assert entity.target_temperature == 19
|
||||||
assert mock_service_call.call_count >= 1
|
assert mock_service_call.call_count >= 1
|
||||||
|
|
||||||
# 3. if heater is stopped (is_device_active==False), then overpowering should be started
|
# 3. if heater is stopped (is_device_active==False) and power is over max, then overpowering should be started
|
||||||
|
side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 150))
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.core.ServiceRegistry.async_call"
|
"homeassistant.core.ServiceRegistry.async_call"
|
||||||
) as mock_service_call, patch(
|
) as mock_service_call, patch(
|
||||||
@@ -420,10 +421,10 @@ async def test_bug_407(
|
|||||||
# simulate a refresh for central power (not necessary)
|
# simulate a refresh for central power (not necessary)
|
||||||
await do_central_power_refresh(hass)
|
await do_central_power_refresh(hass)
|
||||||
|
|
||||||
assert entity.power_manager.is_overpowering_detected is True
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
assert entity.hvac_mode is HVACMode.HEAT
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
assert entity.preset_mode is PRESET_POWER
|
assert entity.preset_mode is PRESET_COMFORT
|
||||||
assert entity.power_manager.overpowering_state is STATE_ON
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ async def test_central_power_manager_init(
|
|||||||
assert central_power_manager.power_temperature == power_temp
|
assert central_power_manager.power_temperature == power_temp
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
central_power_manager.start_listening()
|
await central_power_manager.start_listening()
|
||||||
assert len(central_power_manager._active_listener) == (2 if is_configured else 0)
|
assert len(central_power_manager._active_listener) == (2 if is_configured else 0)
|
||||||
|
|
||||||
# 4. stop listening
|
# 4. stop listening
|
||||||
@@ -273,7 +273,7 @@ async def test_central_power_manageer_find_vtherms(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"current_power, current_max_power, vtherm_configs, expected_results",
|
"current_power, current_max_power, vtherm_configs, expected_results",
|
||||||
[
|
[
|
||||||
# simple nominal test (no shedding)
|
# simple nominal test (initialize overpowering state in VTherm)
|
||||||
(
|
(
|
||||||
1000,
|
1000,
|
||||||
5000,
|
5000,
|
||||||
@@ -286,139 +286,80 @@ async def test_central_power_manageer_find_vtherms(
|
|||||||
"nb_underlying_entities": 1,
|
"nb_underlying_entities": 1,
|
||||||
"on_percent": 0,
|
"on_percent": 0,
|
||||||
"is_overpowering_detected": False,
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
},
|
},
|
||||||
],
|
|
||||||
{"vtherm1": False},
|
|
||||||
),
|
|
||||||
# Simple trivial shedding
|
|
||||||
(
|
|
||||||
1000,
|
|
||||||
2000,
|
|
||||||
[
|
|
||||||
# should be overpowering
|
|
||||||
{
|
|
||||||
"name": "vtherm1",
|
|
||||||
"device_power": 1100,
|
|
||||||
"is_device_active": False,
|
|
||||||
"is_over_climate": False,
|
|
||||||
"nb_underlying_entities": 1,
|
|
||||||
"on_percent": 1,
|
|
||||||
"is_overpowering_detected": False,
|
|
||||||
},
|
|
||||||
# should be overpowering with many underlmying entities
|
|
||||||
{
|
{
|
||||||
"name": "vtherm2",
|
"name": "vtherm2",
|
||||||
"device_power": 4000,
|
"device_power": 10000,
|
||||||
"is_device_active": False,
|
"is_device_active": True,
|
||||||
"is_over_climate": False,
|
"is_over_climate": False,
|
||||||
"nb_underlying_entities": 4,
|
"nb_underlying_entities": 4,
|
||||||
"on_percent": 0.1,
|
"on_percent": 100,
|
||||||
"is_overpowering_detected": False,
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
},
|
},
|
||||||
# over_climate should be overpowering
|
|
||||||
{
|
{
|
||||||
"name": "vtherm3",
|
"name": "vtherm3",
|
||||||
"device_power": 1000,
|
"device_power": 5000,
|
||||||
"is_device_active": False,
|
"is_device_active": True,
|
||||||
"is_over_climate": True,
|
"is_over_climate": True,
|
||||||
"is_overpowering_detected": False,
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
},
|
},
|
||||||
# should pass but because will be also overpowering because previous was overpowering
|
{"name": "vtherm4", "device_power": 1000, "is_device_active": True, "is_over_climate": True, "is_overpowering_detected": False, "overpowering_state": STATE_OFF},
|
||||||
{
|
|
||||||
"name": "vtherm4",
|
|
||||||
"device_power": 800,
|
|
||||||
"is_device_active": False,
|
|
||||||
"is_over_climate": False,
|
|
||||||
"nb_underlying_entities": 1,
|
|
||||||
"on_percent": 1,
|
|
||||||
"is_overpowering_detected": False,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm4": True},
|
# init vtherm1 to False
|
||||||
|
{"vtherm3": False, "vtherm2": False, "vtherm1": False},
|
||||||
),
|
),
|
||||||
# More complex shedding
|
# Un-shedding only (will be taken in reverse order)
|
||||||
(
|
(
|
||||||
1000,
|
1000,
|
||||||
2000,
|
2000,
|
||||||
[
|
[
|
||||||
# already overpowering (non change)
|
# should be not unshedded (too much power will be added)
|
||||||
{
|
{
|
||||||
"name": "vtherm1",
|
"name": "vtherm1",
|
||||||
"device_power": 1100,
|
|
||||||
"is_device_active": False,
|
|
||||||
"is_over_climate": False,
|
|
||||||
"nb_underlying_entities": 1,
|
|
||||||
"on_percent": 1,
|
|
||||||
"is_overpowering_detected": True,
|
|
||||||
},
|
|
||||||
# already overpowering and already active (can be un overpowered)
|
|
||||||
{
|
|
||||||
"name": "vtherm2",
|
|
||||||
"device_power": 1100,
|
|
||||||
"is_device_active": True,
|
|
||||||
"is_over_climate": True,
|
|
||||||
"is_overpowering_detected": True,
|
|
||||||
},
|
|
||||||
# should terminate the overpowering
|
|
||||||
{
|
|
||||||
"name": "vtherm3",
|
|
||||||
"device_power": 800,
|
"device_power": 800,
|
||||||
"is_device_active": False,
|
"is_device_active": False,
|
||||||
"is_over_climate": False,
|
"is_over_climate": False,
|
||||||
"nb_underlying_entities": 1,
|
"nb_underlying_entities": 1,
|
||||||
"on_percent": 1,
|
"on_percent": 1,
|
||||||
"is_overpowering_detected": True,
|
"is_overpowering_detected": True,
|
||||||
|
"overpowering_state": STATE_ON,
|
||||||
},
|
},
|
||||||
# should terminate the overpowering and active
|
# already stay unshedded cause already unshedded
|
||||||
{
|
|
||||||
"name": "vtherm4",
|
|
||||||
"device_power": 3800,
|
|
||||||
"is_device_active": True,
|
|
||||||
"is_over_climate": False,
|
|
||||||
"nb_underlying_entities": 1,
|
|
||||||
"on_percent": 1,
|
|
||||||
"is_overpowering_detected": True,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{"vtherm2": False, "vtherm3": False, "vtherm4": False},
|
|
||||||
),
|
|
||||||
# More complex shedding
|
|
||||||
(
|
|
||||||
1000,
|
|
||||||
2000,
|
|
||||||
[
|
|
||||||
# already overpowering (non change)
|
|
||||||
{
|
|
||||||
"name": "vtherm1",
|
|
||||||
"device_power": 1100,
|
|
||||||
"is_device_active": True,
|
|
||||||
"is_over_climate": False,
|
|
||||||
"nb_underlying_entities": 1,
|
|
||||||
"on_percent": 1,
|
|
||||||
"is_overpowering_detected": True,
|
|
||||||
},
|
|
||||||
# should be overpowering
|
|
||||||
{
|
{
|
||||||
"name": "vtherm2",
|
"name": "vtherm2",
|
||||||
"device_power": 1800,
|
|
||||||
"is_device_active": False,
|
|
||||||
"is_over_climate": True,
|
|
||||||
"is_overpowering_detected": False,
|
|
||||||
},
|
|
||||||
# should terminate the overpowering and active but just before is overpowering
|
|
||||||
{
|
|
||||||
"name": "vtherm3",
|
|
||||||
"device_power": 100,
|
"device_power": 100,
|
||||||
"is_device_active": True,
|
"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,
|
"is_over_climate": False,
|
||||||
"nb_underlying_entities": 1,
|
"nb_underlying_entities": 1,
|
||||||
"on_percent": 1,
|
"on_percent": 1,
|
||||||
"is_overpowering_detected": False,
|
"is_overpowering_detected": True,
|
||||||
|
"overpowering_state": STATE_ON,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{"vtherm1": False, "vtherm2": True, "vtherm3": True},
|
{"vtherm4": False, "vtherm3": False},
|
||||||
),
|
),
|
||||||
# Sheeding only current_power > max_power (need to gain 1000 )
|
# Shedding
|
||||||
(
|
(
|
||||||
2000,
|
2000,
|
||||||
1000,
|
1000,
|
||||||
@@ -432,36 +373,31 @@ async def test_central_power_manageer_find_vtherms(
|
|||||||
"nb_underlying_entities": 1,
|
"nb_underlying_entities": 1,
|
||||||
"on_percent": 1,
|
"on_percent": 1,
|
||||||
"is_overpowering_detected": False,
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_OFF,
|
||||||
},
|
},
|
||||||
# should be overpowering but is already
|
# should be overpowering with many underlmying entities
|
||||||
{
|
{
|
||||||
"name": "vtherm2",
|
"name": "vtherm2",
|
||||||
"device_power": 600,
|
"device_power": 400,
|
||||||
"is_device_active": True,
|
"is_device_active": True,
|
||||||
"is_over_climate": False,
|
"is_over_climate": False,
|
||||||
"nb_underlying_entities": 4,
|
"nb_underlying_entities": 4,
|
||||||
"on_percent": 0.1,
|
"on_percent": 0.1,
|
||||||
"is_overpowering_detected": True,
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_UNKNOWN,
|
||||||
},
|
},
|
||||||
# over_climate should be not overpowering (device not active)
|
# over_climate should be overpowering
|
||||||
{
|
{
|
||||||
"name": "vtherm3",
|
"name": "vtherm3",
|
||||||
"device_power": 690,
|
"device_power": 100,
|
||||||
"is_device_active": False,
|
|
||||||
"is_over_climate": True,
|
|
||||||
"is_overpowering_detected": False,
|
|
||||||
},
|
|
||||||
# over_climate should be overpowering (device active and not already overpowering)
|
|
||||||
{
|
|
||||||
"name": "vtherm4",
|
|
||||||
"device_power": 690,
|
|
||||||
"is_device_active": True,
|
"is_device_active": True,
|
||||||
"is_over_climate": True,
|
"is_over_climate": True,
|
||||||
"is_overpowering_detected": False,
|
"is_overpowering_detected": False,
|
||||||
|
"overpowering_state": STATE_OFF,
|
||||||
},
|
},
|
||||||
# should not overpower (keep as is)
|
# should pass cause not active
|
||||||
{
|
{
|
||||||
"name": "vtherm5",
|
"name": "vtherm4",
|
||||||
"device_power": 800,
|
"device_power": 800,
|
||||||
"is_device_active": False,
|
"is_device_active": False,
|
||||||
"is_over_climate": False,
|
"is_over_climate": False,
|
||||||
@@ -469,8 +405,39 @@ async def test_central_power_manageer_find_vtherms(
|
|||||||
"on_percent": 1,
|
"on_percent": 1,
|
||||||
"is_overpowering_detected": False,
|
"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, "vtherm4": True},
|
{"vtherm1": True, "vtherm2": True, "vtherm3": True, "vtherm6": True},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -501,7 +468,10 @@ async def test_central_power_manageer_calculate_shedding(
|
|||||||
vtherm.nb_underlying_entities = vtherm_config.get("nb_underlying_entities")
|
vtherm.nb_underlying_entities = vtherm_config.get("nb_underlying_entities")
|
||||||
if not vtherm_config.get("is_over_climate"):
|
if not vtherm_config.get("is_over_climate"):
|
||||||
vtherm.proportional_algorithm = MagicMock()
|
vtherm.proportional_algorithm = MagicMock()
|
||||||
vtherm.proportional_algorithm.on_percent = vtherm_config.get("on_percent")
|
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 = MagicMock(spec=FeaturePowerManager)
|
||||||
vtherm.power_manager._vtherm = vtherm
|
vtherm.power_manager._vtherm = vtherm
|
||||||
@@ -510,6 +480,7 @@ async def test_central_power_manageer_calculate_shedding(
|
|||||||
"is_overpowering_detected"
|
"is_overpowering_detected"
|
||||||
)
|
)
|
||||||
vtherm.power_manager.device_power = vtherm_config.get("device_power")
|
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(
|
async def mock_set_overpowering(
|
||||||
overpowering, power_consumption_max=0, v=vtherm
|
overpowering, power_consumption_max=0, v=vtherm
|
||||||
@@ -571,7 +542,7 @@ async def test_central_power_manager_power_event(
|
|||||||
assert central_power_manager.power_temperature == 13
|
assert central_power_manager.power_temperature == 13
|
||||||
|
|
||||||
# 3. start listening (not really useful but don't eat bread)
|
# 3. start listening (not really useful but don't eat bread)
|
||||||
central_power_manager.start_listening()
|
await central_power_manager.start_listening()
|
||||||
assert len(central_power_manager._active_listener) == 2
|
assert len(central_power_manager._active_listener) == 2
|
||||||
|
|
||||||
now: datetime = NowClass.get_now(hass)
|
now: datetime = NowClass.get_now(hass)
|
||||||
@@ -600,6 +571,9 @@ async def test_central_power_manager_power_event(
|
|||||||
"old_state": State("sensor.power_entity_id", STATE_UNAVAILABLE),
|
"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
|
expected_power = power if isinstance(power, (int, float)) else -999
|
||||||
assert central_power_manager.current_power == expected_power
|
assert central_power_manager.current_power == expected_power
|
||||||
assert mock_calculate_shedding.call_count == nb_call
|
assert mock_calculate_shedding.call_count == nb_call
|
||||||
@@ -621,8 +595,11 @@ async def test_central_power_manager_power_event(
|
|||||||
"old_state": State("sensor.power_entity_id", STATE_UNAVAILABLE),
|
"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 central_power_manager.current_power == expected_power
|
||||||
assert mock_calculate_shedding.call_count == (nb_call if dsecs >= 20 else 0)
|
assert mock_calculate_shedding.call_count == nb_call
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -663,7 +640,7 @@ async def test_central_power_manager_max_power_event(
|
|||||||
assert central_power_manager.power_temperature == 13
|
assert central_power_manager.power_temperature == 13
|
||||||
|
|
||||||
# 3. start listening (not really useful but don't eat bread)
|
# 3. start listening (not really useful but don't eat bread)
|
||||||
central_power_manager.start_listening()
|
await central_power_manager.start_listening()
|
||||||
assert len(central_power_manager._active_listener) == 2
|
assert len(central_power_manager._active_listener) == 2
|
||||||
|
|
||||||
now: datetime = NowClass.get_now(hass)
|
now: datetime = NowClass.get_now(hass)
|
||||||
@@ -694,6 +671,9 @@ async def test_central_power_manager_max_power_event(
|
|||||||
"old_state": State("sensor.max_power_entity_id", STATE_UNAVAILABLE),
|
"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
|
expected_power = max_power if isinstance(max_power, (int, float)) else -999
|
||||||
assert central_power_manager.current_max_power == expected_power
|
assert central_power_manager.current_max_power == expected_power
|
||||||
assert mock_calculate_shedding.call_count == nb_call
|
assert mock_calculate_shedding.call_count == nb_call
|
||||||
@@ -715,5 +695,8 @@ async def test_central_power_manager_max_power_event(
|
|||||||
"old_state": State("sensor.max_power_entity_id", STATE_UNAVAILABLE),
|
"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 central_power_manager.current_max_power == expected_power
|
||||||
assert mock_calculate_shedding.call_count == (nb_call if dsecs >= 20 else 0)
|
assert mock_calculate_shedding.call_count == nb_call
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ async def test_motion_feature_manager_refresh(
|
|||||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
motion_manager.start_listening()
|
await motion_manager.start_listening()
|
||||||
assert motion_manager.is_configured is True
|
assert motion_manager.is_configured is True
|
||||||
assert motion_manager.motion_state == STATE_UNKNOWN
|
assert motion_manager.motion_state == STATE_UNKNOWN
|
||||||
assert motion_manager.is_motion_detected is False
|
assert motion_manager.is_motion_detected is False
|
||||||
@@ -198,7 +198,7 @@ async def test_motion_feature_manager_event(
|
|||||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
motion_manager.start_listening()
|
await motion_manager.start_listening()
|
||||||
|
|
||||||
# 2. test _motion_sensor_changed with the parametrized
|
# 2. test _motion_sensor_changed with the parametrized
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
|||||||
@@ -812,21 +812,20 @@ async def test_multiple_switch_power_management(
|
|||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
|
|
||||||
# 2. Send power max mesurement too low and HVACMode is on
|
# 2. Send power max mesurement too low and HVACMode is on
|
||||||
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 74))
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
|
||||||
|
|
||||||
with patch(
|
#fmt: off
|
||||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
with patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||||
) as mock_send_event, patch(
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off, \
|
||||||
) as mock_heater_on, patch(
|
patch("custom_components.versatile_thermostat.thermostat_switch.ThermostatOverSwitch.is_device_active", return_value="True"):
|
||||||
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
#fmt: on
|
||||||
) as mock_heater_off:
|
|
||||||
now = now + timedelta(seconds=30)
|
now = now + timedelta(seconds=30)
|
||||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
assert entity.power_percent > 0
|
assert entity.power_percent > 0
|
||||||
# 100 of the device / 4 -> 25, current power 50 so max is 75
|
# 100 of the device / 4 -> 25, current power 50 so max is 75
|
||||||
await send_max_power_change_event(entity, 74, datetime.now())
|
await send_max_power_change_event(entity, 49, datetime.now())
|
||||||
assert entity.power_manager.is_overpowering_detected is True
|
assert entity.power_manager.is_overpowering_detected is True
|
||||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||||
assert entity.preset_mode is PRESET_POWER
|
assert entity.preset_mode is PRESET_POWER
|
||||||
@@ -843,7 +842,7 @@ async def test_multiple_switch_power_management(
|
|||||||
"type": "start",
|
"type": "start",
|
||||||
"current_power": 50,
|
"current_power": 50,
|
||||||
"device_power": 100,
|
"device_power": 100,
|
||||||
"current_max_power": 74,
|
"current_max_power": 49,
|
||||||
"current_power_consumption": 100,
|
"current_power_consumption": 100,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ async def test_power_feature_manager(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
power_manager.start_listening()
|
await power_manager.start_listening()
|
||||||
|
|
||||||
assert power_manager.is_configured is True
|
assert power_manager.is_configured is True
|
||||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||||
@@ -117,7 +117,7 @@ async def test_power_feature_manager(
|
|||||||
assert custom_attributes["current_max_power"] is None
|
assert custom_attributes["current_max_power"] is None
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
power_manager.start_listening()
|
await power_manager.start_listening()
|
||||||
assert power_manager.is_configured is True
|
assert power_manager.is_configured is True
|
||||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ async def test_power_feature_manager_set_overpowering(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
power_manager.start_listening()
|
await power_manager.start_listening()
|
||||||
|
|
||||||
assert power_manager.is_configured is True
|
assert power_manager.is_configured is True
|
||||||
assert power_manager.overpowering_state == STATE_UNKNOWN
|
assert power_manager.overpowering_state == STATE_UNKNOWN
|
||||||
@@ -517,17 +517,18 @@ async def test_power_management_hvac_on(
|
|||||||
assert entity.power_manager.overpowering_state is STATE_OFF
|
assert entity.power_manager.overpowering_state is STATE_OFF
|
||||||
|
|
||||||
# Send power max mesurement too low and HVACMode is on
|
# Send power max mesurement too low and HVACMode is on
|
||||||
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
|
side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 49))
|
||||||
# fmt:off
|
# fmt:off
|
||||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
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.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_on") as mock_heater_on, \
|
||||||
patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
|
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
|
# fmt: on
|
||||||
now = now + timedelta(seconds=30)
|
now = now + timedelta(seconds=30)
|
||||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
await send_max_power_change_event(entity, 149, datetime.now())
|
await send_max_power_change_event(entity, 49, now)
|
||||||
assert entity.power_manager.is_overpowering_detected is True
|
assert entity.power_manager.is_overpowering_detected is True
|
||||||
# All configuration is complete and power is > power_max we switch to POWER preset
|
# All configuration is complete and power is > power_max we switch to POWER preset
|
||||||
assert entity.preset_mode is PRESET_POWER
|
assert entity.preset_mode is PRESET_POWER
|
||||||
@@ -544,7 +545,7 @@ async def test_power_management_hvac_on(
|
|||||||
"type": "start",
|
"type": "start",
|
||||||
"current_power": 50,
|
"current_power": 50,
|
||||||
"device_power": 100,
|
"device_power": 100,
|
||||||
"current_max_power": 149,
|
"current_max_power": 49,
|
||||||
"current_power_consumption": 100.0,
|
"current_power_consumption": 100.0,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -554,8 +555,9 @@ async def test_power_management_hvac_on(
|
|||||||
assert mock_heater_on.call_count == 0
|
assert mock_heater_on.call_count == 0
|
||||||
assert mock_heater_off.call_count == 1
|
assert mock_heater_off.call_count == 1
|
||||||
|
|
||||||
# Send power mesurement low to unseet power preset
|
# Send power mesurement low to unset power preset
|
||||||
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_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
|
# fmt:off
|
||||||
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
|
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.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
|
||||||
@@ -565,7 +567,7 @@ async def test_power_management_hvac_on(
|
|||||||
now = now + timedelta(seconds=30)
|
now = now + timedelta(seconds=30)
|
||||||
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
|
||||||
|
|
||||||
await send_power_change_event(entity, 48, datetime.now())
|
await send_power_change_event(entity, 48, now)
|
||||||
assert entity.power_manager.is_overpowering_detected is False
|
assert entity.power_manager.is_overpowering_detected is False
|
||||||
# All configuration is complete and power is < power_max, we restore previous preset
|
# All configuration is complete and power is < power_max, we restore previous preset
|
||||||
assert entity.preset_mode is PRESET_BOOST
|
assert entity.preset_mode is PRESET_BOOST
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ async def test_presence_feature_manager(
|
|||||||
assert custom_attributes["is_presence_configured"] is True
|
assert custom_attributes["is_presence_configured"] is True
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
presence_manager.start_listening()
|
await presence_manager.start_listening()
|
||||||
assert presence_manager.is_configured is True
|
assert presence_manager.is_configured is True
|
||||||
assert presence_manager.presence_state == STATE_UNKNOWN
|
assert presence_manager.presence_state == STATE_UNKNOWN
|
||||||
assert presence_manager.is_absence_detected is False
|
assert presence_manager.is_absence_detected is False
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ async def test_window_feature_manager_refresh_sensor_action_turn_off(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
assert window_manager.is_configured is True
|
assert window_manager.is_configured is True
|
||||||
assert window_manager.window_state == STATE_UNKNOWN
|
assert window_manager.window_state == STATE_UNKNOWN
|
||||||
assert window_manager.window_auto_state == STATE_UNAVAILABLE
|
assert window_manager.window_auto_state == STATE_UNAVAILABLE
|
||||||
@@ -288,7 +288,7 @@ async def test_window_feature_manager_refresh_sensor_action_frost_only(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
assert window_manager.is_configured is True
|
assert window_manager.is_configured is True
|
||||||
assert window_manager.window_state == STATE_UNKNOWN
|
assert window_manager.window_state == STATE_UNKNOWN
|
||||||
assert window_manager.window_auto_state == STATE_UNAVAILABLE
|
assert window_manager.window_auto_state == STATE_UNAVAILABLE
|
||||||
@@ -408,7 +408,7 @@ async def test_window_feature_manager_sensor_event_action_turn_off(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
assert len(window_manager._active_listener) == 1
|
assert len(window_manager._active_listener) == 1
|
||||||
|
|
||||||
# 4. test refresh with the parametrized
|
# 4. test refresh with the parametrized
|
||||||
@@ -535,7 +535,7 @@ async def test_window_feature_manager_event_sensor_action_frost_only(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. start listening
|
# 3. start listening
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
|
|
||||||
# 4. test refresh with the parametrized
|
# 4. test refresh with the parametrized
|
||||||
# fmt:off
|
# fmt:off
|
||||||
@@ -660,7 +660,7 @@ async def test_window_feature_manager_window_auto(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert window_manager.is_window_auto_configured is True
|
assert window_manager.is_window_auto_configured is True
|
||||||
window_manager.start_listening()
|
await window_manager.start_listening()
|
||||||
|
|
||||||
# 2. Call manage window auto
|
# 2. Call manage window auto
|
||||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
|||||||
Reference in New Issue
Block a user