All tests not ok

This commit is contained in:
Jean-Marc Collin
2025-01-03 17:43:27 +00:00
parent 03fbc5362a
commit 24fcb7a161
14 changed files with 441 additions and 392 deletions

View File

@@ -38,6 +38,10 @@ class BaseFeatureManager:
self._active_listener = [] self._active_listener = []
async def refresh_state(self):
"""Refresh the state and return True if a change have been made"""
return False
def add_listener(self, func: CALLBACK_TYPE) -> None: def add_listener(self, func: CALLBACK_TYPE) -> None:
"""Add a listener to the list of active listener""" """Add a listener to the list of active listener"""
self._active_listener.append(func) self._active_listener.append(func)

View File

@@ -50,11 +50,10 @@ from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
STATE_ON,
) )
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .commons import ConfigData, T from .commons import ConfigData, T, deprecated
from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -1611,10 +1610,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Check overpowering condition # Check overpowering condition
# Not necessary for switch because each switch is checking at startup # Not necessary for switch because each switch is checking at startup
overpowering = await self._power_manager.check_overpowering() # overpowering is now centralized
if overpowering == STATE_ON: # overpowering = await self._power_manager.check_overpowering()
_LOGGER.debug("%s - End of cycle (overpowering)", self) # if overpowering == STATE_ON:
return True # _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:
@@ -1957,14 +1957,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return self._presets.get(preset, None) is not None return self._presets.get(preset, None) is not None
# For testing purpose # For testing purpose
@DeprecationWarning # @deprecated
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)
# @deprecated
@property @property
@DeprecationWarning
def now(self) -> datetime: def now(self) -> datetime:
"""Get now. The local datetime or the overloaded _set_now date """Get now. The local datetime or the overloaded _set_now date
This method should be replaced by the vthermAPI equivalent""" This method should be replaced by the vthermAPI equivalent"""

View File

@@ -4,10 +4,6 @@ import logging
from typing import Any from typing import Any
from functools import cmp_to_key from functools import cmp_to_key
from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
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,
@@ -106,17 +102,17 @@ class CentralFeaturePowerManager(BaseFeatureManager):
"""Handle power changes.""" """Handle power changes."""
_LOGGER.debug("Thermostat %s - Receive new Power event", self) _LOGGER.debug("Thermostat %s - Receive new Power event", self)
_LOGGER.debug(event) _LOGGER.debug(event)
self.refresh_state() await self.refresh_state()
@callback @callback
async def _max_power_sensor_changed(self, event: Event[EventStateChangedData]): async def _max_power_sensor_changed(self, event: Event[EventStateChangedData]):
"""Handle power max changes.""" """Handle power max changes."""
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name) _LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
_LOGGER.debug(event) _LOGGER.debug(event)
self.refresh_state() await self.refresh_state()
@overrides @overrides
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 ret = False
@@ -156,7 +152,7 @@ class CentralFeaturePowerManager(BaseFeatureManager):
else 999 else 999
) )
if dtimestamp >= MIN_DTEMP_SECS: if dtimestamp >= MIN_DTEMP_SECS:
self.calculate_shedding() await self.calculate_shedding()
self._last_shedding_date = now self._last_shedding_date = now
return ret return ret
@@ -207,16 +203,19 @@ class CentralFeaturePowerManager(BaseFeatureManager):
if not vtherm.power_manager.is_overpowering_detected: if not vtherm.power_manager.is_overpowering_detected:
# To force all others vtherms to be in overpowering # To force all others vtherms to be in overpowering
force_overpowering = True force_overpowering = True
await vtherm.power_manager.set_overpowering(True) await vtherm.power_manager.set_overpowering(
True, power_consumption_max
)
else: else:
total_affected_power += power_consumption_max total_affected_power += power_consumption_max
if vtherm.power_manager.is_overpowering_detected: # Always set to false
_LOGGER.debug( # if vtherm.power_manager.is_overpowering_detected:
"%s - vtherm %s should not be in overpowering state", _LOGGER.debug(
self, "%s - vtherm %s should not be in overpowering state",
vtherm.name, self,
) vtherm.name,
await vtherm.power_manager.set_overpowering(False) )
await vtherm.power_manager.set_overpowering(False)
_LOGGER.debug( _LOGGER.debug(
"%s - after vtherm %s total_affected_power=%s, available_power=%s", "%s - after vtherm %s total_affected_power=%s, available_power=%s",
@@ -290,5 +289,15 @@ class CentralFeaturePowerManager(BaseFeatureManager):
"""Return the power temperature""" """Return the power temperature"""
return self._power_temp 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): def __str__(self):
return "CentralPowerManager" return "CentralPowerManager"

View File

@@ -3,6 +3,7 @@
# pylint: disable=line-too-long # pylint: disable=line-too-long
import logging import logging
import warnings
from types import MappingProxyType from types import MappingProxyType
from typing import Any, TypeVar from typing import Any, TypeVar
@@ -132,3 +133,20 @@ def check_and_extract_service_configuration(service_config) -> dict:
"check_and_extract_service_configuration(%s) gives '%s'", service_config, ret "check_and_extract_service_configuration(%s) gives '%s'", service_config, ret
) )
return ret return ret
def deprecated(message):
"""A decorator to indicate that the method/attribut is deprecated"""
def decorator(func):
def wrapper(*args, **kwargs):
warnings.warn(
f"{func.__name__} is deprecated: {message}",
DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return wrapper
return decorator

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 STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name
{ {
vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean, vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean,

View File

@@ -12,22 +12,15 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import ( from homeassistant.core import (
HomeAssistant, HomeAssistant,
callback,
Event,
) )
from homeassistant.helpers.event import (
async_track_state_change_event,
EventStateChangedData,
)
from homeassistant.components.climate import HVACMode
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
from .commons import ConfigData from .commons import ConfigData
from .base_manager import BaseFeatureManager from .base_manager import BaseFeatureManager
from .vtherm_api import VersatileThermostatAPI
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -50,10 +43,6 @@ class FeaturePowerManager(BaseFeatureManager):
def __init__(self, vtherm: Any, hass: HomeAssistant): def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager""" """Init of a featureManager"""
super().__init__(vtherm, hass) super().__init__(vtherm, hass)
self._power_sensor_entity_id = None
self._max_power_sensor_entity_id = None
self._current_power = None
self._current_max_power = None
self._power_temp = None self._power_temp = None
self._overpowering_state = STATE_UNAVAILABLE self._overpowering_state = STATE_UNAVAILABLE
self._is_configured: bool = False self._is_configured: bool = False
@@ -64,20 +53,11 @@ class FeaturePowerManager(BaseFeatureManager):
"""Reinit of the manager""" """Reinit of the manager"""
# Power management # Power management
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
self._power_temp = entry_infos.get(CONF_PRESET_POWER) self._power_temp = entry_infos.get(CONF_PRESET_POWER)
self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0 self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
self._is_configured = False self._is_configured = False
self._current_power = None if entry_infos.get(CONF_USE_POWER_FEATURE, False) and self._device_power:
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
):
self._is_configured = True self._is_configured = True
self._overpowering_state = STATE_UNKNOWN self._overpowering_state = STATE_UNKNOWN
else: else:
@@ -85,159 +65,54 @@ class FeaturePowerManager(BaseFeatureManager):
@overrides @overrides
def start_listening(self): def start_listening(self):
"""Start listening the underlying entity""" """Start listening the underlying entity. There is nothing to listen"""
if self._is_configured:
self.stop_listening()
else:
return
self.add_listener(
async_track_state_change_event(
self.hass,
[self._power_sensor_entity_id],
self._async_power_sensor_changed,
)
)
self.add_listener(
async_track_state_change_event(
self.hass,
[self._max_power_sensor_entity_id],
self._async_max_power_sensor_changed,
)
)
@overrides
async def refresh_state(self) -> bool:
"""Tries to get the last state from sensor
Returns True if a change has been made"""
ret = False
if self._is_configured:
# try to acquire current power and power max
current_power_state = self.hass.states.get(self._power_sensor_entity_id)
if current_power_state and current_power_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_power = float(current_power_state.state)
_LOGGER.debug(
"%s - Current power have been retrieved: %.3f",
self,
self._current_power,
)
ret = True
# Try to acquire power max
current_power_max_state = self.hass.states.get(
self._max_power_sensor_entity_id
)
if current_power_max_state and current_power_max_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_max_power = float(current_power_max_state.state)
_LOGGER.debug(
"%s - Current power max have been retrieved: %.3f",
self,
self._current_max_power,
)
ret = True
return ret
@callback
async def _async_power_sensor_changed(self, event: Event[EventStateChangedData]):
"""Handle power changes."""
_LOGGER.debug("Thermostat %s - Receive new Power event", self)
_LOGGER.debug(event)
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if (
new_state is None
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
or (old_state is not None and new_state.state == old_state.state)
):
return
try:
current_power = float(new_state.state)
if math.isnan(current_power) or math.isinf(current_power):
raise ValueError(f"Sensor has illegal state {new_state.state}")
self._current_power = current_power
if self._vtherm.preset_mode == PRESET_POWER:
await self._vtherm.async_control_heating()
except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
@callback
async def _async_max_power_sensor_changed(
self, event: Event[EventStateChangedData]
):
"""Handle power max changes."""
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
_LOGGER.debug(event)
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if (
new_state is None
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
or (old_state is not None and new_state.state == old_state.state)
):
return
try:
current_power_max = float(new_state.state)
if math.isnan(current_power_max) or math.isinf(current_power_max):
raise ValueError(f"Sensor has illegal state {new_state.state}")
self._current_max_power = current_power_max
if self._vtherm.preset_mode == PRESET_POWER:
await self._vtherm.async_control_heating()
except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
"""Add some custom attributes""" """Add some custom attributes"""
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
extra_state_attributes.update( extra_state_attributes.update(
{ {
"power_sensor_entity_id": self._power_sensor_entity_id, "power_sensor_entity_id": vtherm_api.central_power_manager.power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id, "max_power_sensor_entity_id": vtherm_api.central_power_manager.max_power_sensor_entity_id,
"overpowering_state": self._overpowering_state, "overpowering_state": self._overpowering_state,
"is_power_configured": self._is_configured, "is_power_configured": self._is_configured,
"device_power": self._device_power, "device_power": self._device_power,
"power_temp": self._power_temp, "power_temp": self._power_temp,
"current_power": self._current_power, "current_power": vtherm_api.central_power_manager.current_power,
"current_max_power": self._current_max_power, "current_max_power": vtherm_api.central_power_manager.current_max_power,
"mean_cycle_power": self.mean_cycle_power, "mean_cycle_power": self.mean_cycle_power,
} }
) )
async def check_overpowering(self) -> bool: async def check_power_available(self) -> bool:
"""Check the overpowering condition """Check if the Vtherm can be started considering overpowering.
Turn the preset_mode of the heater to 'power' if power conditions are exceeded Returns True if no overpowering conditions are found
Returns True if overpowering is 'on'
""" """
if not self._is_configured: vtherm_api = VersatileThermostatAPI.get_vtherm_api()
return False
if ( if (
self._current_power is None not self._is_configured
or not vtherm_api.central_power_manager.is_configured
):
return True
current_power = vtherm_api.central_power_manager.current_power
current_max_power = vtherm_api.central_power_manager.current_max_power
if (
current_power is None
or current_max_power is None
or self._device_power is None or self._device_power is None
or self._current_max_power is None
): ):
_LOGGER.warning( _LOGGER.warning(
"%s - power not valued. check_overpowering not available", self "%s - power not valued. check_power_available not available", self
) )
return False return True
_LOGGER.debug( _LOGGER.debug(
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f", "%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
self, self,
self._current_power, current_power,
self._current_max_power, current_max_power,
self._device_power, self._device_power,
) )
@@ -253,18 +128,35 @@ class FeaturePowerManager(BaseFeatureManager):
self._device_power * self._vtherm.proportional_algorithm.on_percent, self._device_power * self._vtherm.proportional_algorithm.on_percent,
) )
ret = (self._current_power + power_consumption_max) >= self._current_max_power ret = (current_power + power_consumption_max) < current_max_power
if ( if not ret:
self._overpowering_state == STATE_OFF _LOGGER.info(
and ret "%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f",
and self._vtherm.hvac_mode != HVACMode.OFF self,
): current_power,
current_max_power,
self._device_power,
)
return ret
async def set_overpowering(self, overpowering: bool, power_consumption_max=0):
"""Force the overpowering state for the VTherm"""
vtherm_api = VersatileThermostatAPI.get_vtherm_api()
current_power = vtherm_api.central_power_manager.current_power
current_max_power = vtherm_api.central_power_manager.current_max_power
if overpowering and not self.is_overpowering_detected:
_LOGGER.warning( _LOGGER.warning(
"%s - overpowering is detected. Heater preset will be set to 'power'", "%s - overpowering is detected. Heater preset will be set to 'power'",
self, self,
) )
self._overpowering_state = STATE_ON
if self._vtherm.is_over_climate: if self._vtherm.is_over_climate:
self._vtherm.save_hvac_mode() self._vtherm.save_hvac_mode()
self._vtherm.save_preset_mode() self._vtherm.save_preset_mode()
await self._vtherm.async_underlying_entity_turn_off() await self._vtherm.async_underlying_entity_turn_off()
await self._vtherm.async_set_preset_mode_internal(PRESET_POWER) await self._vtherm.async_set_preset_mode_internal(PRESET_POWER)
@@ -272,24 +164,20 @@ class FeaturePowerManager(BaseFeatureManager):
EventType.POWER_EVENT, EventType.POWER_EVENT,
{ {
"type": "start", "type": "start",
"current_power": self._current_power, "current_power": current_power,
"device_power": self._device_power, "device_power": self._device_power,
"current_max_power": self._current_max_power, "current_max_power": current_max_power,
"current_power_consumption": power_consumption_max, "current_power_consumption": power_consumption_max,
}, },
) )
elif not overpowering and self.is_overpowering_detected:
# Check if we need to remove the POWER preset
if (
self._overpowering_state == STATE_ON
and not ret
and self._vtherm.preset_mode == PRESET_POWER
):
_LOGGER.warning( _LOGGER.warning(
"%s - end of overpowering is detected. Heater preset will be restored to '%s'", "%s - end of overpowering is detected. Heater preset will be restored to '%s'",
self, self,
self._vtherm._saved_preset_mode, # pylint: disable=protected-access self._vtherm._saved_preset_mode, # pylint: disable=protected-access
) )
self._overpowering_state = STATE_OFF
if self._vtherm.is_over_climate: if self._vtherm.is_over_climate:
await self._vtherm.restore_hvac_mode(False) await self._vtherm.restore_hvac_mode(False)
await self._vtherm.restore_preset_mode() await self._vtherm.restore_preset_mode()
@@ -297,22 +185,18 @@ class FeaturePowerManager(BaseFeatureManager):
EventType.POWER_EVENT, EventType.POWER_EVENT,
{ {
"type": "end", "type": "end",
"current_power": self._current_power, "current_power": current_power,
"device_power": self._device_power, "device_power": self._device_power,
"current_max_power": self._current_max_power, "current_max_power": current_max_power,
}, },
) )
elif not overpowering and self._overpowering_state != STATE_OFF:
new_overpowering_state = STATE_ON if ret else STATE_OFF # just set to not overpowering the state which was not set
if self._overpowering_state != new_overpowering_state: self._overpowering_state = STATE_OFF
self._overpowering_state = new_overpowering_state else:
self._vtherm.update_custom_attributes() # Nothing to do (already in the right state)
return
return self._overpowering_state == STATE_ON self._vtherm.update_custom_attributes()
async def set_overpowering(self, overpowering: bool):
"""Force the overpowering state for the VTherm"""
raise NotImplementedError
@overrides @overrides
@property @property
@@ -333,16 +217,6 @@ class FeaturePowerManager(BaseFeatureManager):
"""Return True if the Vtherm is in overpowering state""" """Return True if the Vtherm is in overpowering state"""
return self._overpowering_state == STATE_ON return self._overpowering_state == STATE_ON
@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
@property @property
def power_temperature(self) -> bool: def power_temperature(self) -> bool:
"""Return the power temperature""" """Return the power temperature"""
@@ -353,16 +227,6 @@ class FeaturePowerManager(BaseFeatureManager):
"""Return the device power""" """Return the device power"""
return self._device_power return self._device_power
@property
def current_power(self) -> bool:
"""Return the current power from sensor"""
return self._current_power
@property
def current_max_power(self) -> bool:
"""Return the current power from sensor"""
return self._current_max_power
@property @property
def mean_cycle_power(self) -> float | None: def mean_cycle_power(self) -> float | None:
"""Returns the mean power consumption during the cycle""" """Returns the mean power consumption during the cycle"""

View File

@@ -409,9 +409,10 @@ class UnderlyingSwitch(UnderlyingEntity):
await self.turn_off() await self.turn_off()
return return
if await self._thermostat.power_manager.check_overpowering(): # if await self._thermostat.power_manager.check_overpowering():
_LOGGER.debug("%s - End of cycle (3)", self) # _LOGGER.debug("%s - End of cycle (3)", self)
return # return
# safety mode could have change the on_time percent # safety mode could have change the on_time percent
await self._thermostat.safety_manager.refresh_state() await self._thermostat.safety_manager.refresh_state()
time = self._on_time_sec time = self._on_time_sec

View File

@@ -592,7 +592,10 @@ class MockNumber(NumberEntity):
async def create_thermostat( async def create_thermostat(
hass: HomeAssistant, entry: MockConfigEntry, entity_id: str hass: HomeAssistant,
entry: MockConfigEntry,
entity_id: str,
temps: dict | None = None,
) -> BaseThermostat: ) -> BaseThermostat:
"""Creates and return a TPI Thermostat""" """Creates and return a TPI Thermostat"""
entry.add_to_hass(hass) entry.add_to_hass(hass)
@@ -601,6 +604,11 @@ async def create_thermostat(
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN) entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
if entity and temps:
await set_all_climate_preset_temp(
hass, entity, temps, entity.entity_id.replace("climate.", "")
)
return entity return entity
@@ -741,9 +749,10 @@ 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)
if sleep: if sleep:
await asyncio.sleep(0.1) await entity.hass.async_block_till_done()
async def send_max_power_change_event( async def send_max_power_change_event(
@@ -767,9 +776,10 @@ 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)
if sleep: if sleep:
await asyncio.sleep(0.1) await entity.hass.async_block_till_done()
async def send_window_change_event( async def send_window_change_event(

View File

@@ -19,6 +19,8 @@
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
# https://github.com/miketheman/pytest-socket/pull/275
from pytest_socket import socket_allow_hosts
from homeassistant.core import StateMachine from homeassistant.core import StateMachine
@@ -26,6 +28,12 @@ from custom_components.versatile_thermostat.config_flow import (
VersatileThermostatBaseConfigFlow, VersatileThermostatBaseConfigFlow,
) )
from custom_components.versatile_thermostat.const import (
CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_USE_POWER_FEATURE,
CONF_PRESET_POWER,
)
from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
@@ -35,12 +43,6 @@ from .commons import (
FULL_CENTRAL_CONFIG_WITH_BOILER, FULL_CENTRAL_CONFIG_WITH_BOILER,
) )
# https://github.com/miketheman/pytest-socket/pull/275
from pytest_socket import socket_allow_hosts
# ...
# ... # ...
def pytest_runtest_setup(): def pytest_runtest_setup():
"""setup tests""" """setup tests"""
@@ -51,16 +53,6 @@ def pytest_runtest_setup():
pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name
# Permet d'exclure certains test en mode d'ex
# sequential = pytest.mark.sequential
# This fixture allow to execute some tests first and not in //
# @pytest.fixture
# def order():
# return 1
#
# This fixture enables loading custom integrations in all tests. # This fixture enables loading custom integrations in all tests.
# Remove to enable selective use of this fixture # Remove to enable selective use of this fixture
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -167,3 +159,24 @@ async def init_central_config_with_boiler_fixture(
await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER) await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER)
yield yield
@pytest.fixture(name="init_central_power_manager")
async def init_central_power_manager_fixture(
hass, init_central_config
): # pylint: disable=unused-argument
"""Initialize the central power_manager"""
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# 1. creation / init
vtherm_api.central_power_manager.post_init(
{
CONF_POWER_SENSOR: "sensor.the_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 13,
}
)
assert vtherm_api.central_power_manager.is_configured
yield

View File

@@ -159,7 +159,7 @@ async def test_overpowering_binary_sensors(
await entity.async_set_preset_mode(PRESET_COMFORT) await entity.async_set_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now) await send_temperature_change_event(entity, 15, now)
assert await entity.power_manager.check_overpowering() is False assert entity.power_manager.is_overpowering_detected is False
assert entity.power_manager.overpowering_state is STATE_UNKNOWN assert entity.power_manager.overpowering_state is STATE_UNKNOWN
await overpowering_binary_sensor.async_my_climate_changed() await overpowering_binary_sensor.async_my_climate_changed()
@@ -168,7 +168,7 @@ async def test_overpowering_binary_sensors(
await send_power_change_event(entity, 100, now) await send_power_change_event(entity, 100, now)
await send_max_power_change_event(entity, 150, now) await send_max_power_change_event(entity, 150, now)
assert await entity.power_manager.check_overpowering() 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
# Simulate the event reception # Simulate the event reception
@@ -177,7 +177,7 @@ async def test_overpowering_binary_sensors(
# set max power to a low value # set max power to a low value
await send_max_power_change_event(entity, 201, now) await send_max_power_change_event(entity, 201, 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_OFF assert entity.power_manager.overpowering_state is STATE_OFF
# Simulate the event reception # Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed() await overpowering_binary_sensor.async_my_climate_changed()

View File

@@ -346,7 +346,7 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
# Send power mesurement (theheater is already in the power measurement) # Send power mesurement (theheater is already in the power measurement)
await send_power_change_event(entity, 100, datetime.now()) await send_power_change_event(entity, 100, datetime.now())
# No overpowering yet # No overpowering yet
assert await entity.power_manager.check_overpowering() is False assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is < power_max # All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_COMFORT assert entity.preset_mode is PRESET_COMFORT
assert entity.power_manager.overpowering_state is STATE_OFF assert entity.power_manager.overpowering_state is STATE_OFF
@@ -365,7 +365,7 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
# waits that the heater starts # waits that the heater starts
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert await entity.power_manager.check_overpowering() is False assert entity.power_manager.is_overpowering_detected is False
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_OFF assert entity.power_manager.overpowering_state is STATE_OFF
@@ -385,7 +385,7 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
# waits that the heater starts # waits that the heater starts
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
assert await entity.power_manager.check_overpowering() is True assert entity.power_manager.is_overpowering_detected is True
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_POWER
assert entity.power_manager.overpowering_state is STATE_ON assert entity.power_manager.overpowering_state is STATE_ON

View File

@@ -398,19 +398,15 @@ async def test_central_power_manageer_calculate_shedding(
) )
vtherm.power_manager.device_power = vtherm_config.get("device_power") vtherm.power_manager.device_power = vtherm_config.get("device_power")
async def mock_set_overpowering(overpowering, v=vtherm): async def mock_set_overpowering(
overpowering, power_consumption_max=0, v=vtherm
):
register_call(v, overpowering) register_call(v, overpowering)
vtherm.power_manager.set_overpowering = mock_set_overpowering vtherm.power_manager.set_overpowering = mock_set_overpowering
vtherms.append(vtherm) vtherms.append(vtherm)
# type(central_power_manager).current_max_power = PropertyMock(
# return_value=current_max_power
# )
# type(central_power_manager).current_power = PropertyMock(return_value=current_power)
# type(central_power_manager).is_configured = PropertyMock(return_value=True)
# fmt:off # 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), \ 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_max_power", new_callable=PropertyMock, return_value=current_max_power), \

View File

@@ -783,7 +783,7 @@ async def test_multiple_switch_power_management(
await send_power_change_event(entity, 50, datetime.now()) await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement # Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now()) await send_max_power_change_event(entity, 300, datetime.now())
assert await entity.power_manager.check_overpowering() is False assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is < power_max # All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_OFF assert entity.power_manager.overpowering_state is STATE_OFF
@@ -798,7 +798,7 @@ async def test_multiple_switch_power_management(
) as mock_heater_off: ) as mock_heater_off:
# 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, 74, datetime.now())
assert await entity.power_manager.check_overpowering() 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
assert entity.power_manager.overpowering_state is STATE_ON assert entity.power_manager.overpowering_state is STATE_ON
@@ -843,7 +843,7 @@ async def test_multiple_switch_power_management(
) as mock_heater_off: ) as mock_heater_off:
# 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating # 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()) await send_max_power_change_event(entity, 150, datetime.now())
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 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_ECO assert entity.preset_mode is PRESET_ECO
assert entity.power_manager.overpowering_state is STATE_OFF assert entity.power_manager.overpowering_state is STATE_OFF

View File

@@ -10,6 +10,7 @@ from custom_components.versatile_thermostat.thermostat_switch import (
from custom_components.versatile_thermostat.feature_power_manager import ( from custom_components.versatile_thermostat.feature_power_manager import (
FeaturePowerManager, FeaturePowerManager,
) )
from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@@ -17,28 +18,28 @@ logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"is_over_climate, is_device_active, power, max_power, current_overpowering_state, overpowering_state, nb_call, changed, check_overpowering_ret", "is_over_climate, is_device_active, power, max_power, check_power_available",
[ [
# don't switch to overpower (power is enough) # don't switch to overpower (power is enough)
(False, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False), (False, False, 1000, 3000, True),
# switch to overpower (power is not enough) # switch to overpower (power is not enough)
(False, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True), (False, False, 2000, 3000, False),
# don't switch to overpower (power is not enough but device is already on) # don't switch to overpower (power is not enough but device is already on)
(False, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False), (False, True, 2000, 3000, True),
# Same with a over_climate # Same with a over_climate
# don't switch to overpower (power is enough) # don't switch to overpower (power is enough)
(True, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False), (True, False, 1000, 3000, True),
# switch to overpower (power is not enough) # switch to overpower (power is not enough)
(True, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True), (True, False, 2000, 3000, False),
# don't switch to overpower (power is not enough but device is already on) # don't switch to overpower (power is not enough but device is already on)
(True, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False), (True, True, 2000, 3000, True),
# Leave overpowering state # Leave overpowering state
# switch to not overpower (power is enough) # switch to not overpower (power is enough)
(False, False, 1000, 3000, STATE_ON, STATE_OFF, 1, True, False), (False, False, 1000, 3000, True),
# don't switch to overpower (power is still not enough) # don't switch to overpower (power is still not enough)
(False, False, 2000, 3000, STATE_ON, STATE_ON, 0, True, True), (False, False, 2000, 3000, False),
# keep overpower (power is not enough but device is already on) # keep overpower (power is not enough but device is already on)
(False, True, 3000, 3000, STATE_ON, STATE_ON, 0, True, True), (False, True, 3000, 3000, False),
], ],
) )
async def test_power_feature_manager( async def test_power_feature_manager(
@@ -47,17 +48,15 @@ async def test_power_feature_manager(
is_device_active, is_device_active,
power, power,
max_power, max_power,
current_overpowering_state, check_power_available,
overpowering_state,
nb_call,
changed,
check_overpowering_ret,
): ):
"""Test the FeaturePresenceManager class direclty""" """Test the FeaturePresenceManager class direclty"""
fake_vtherm = MagicMock(spec=BaseThermostat) fake_vtherm = MagicMock(spec=BaseThermostat)
type(fake_vtherm).name = PropertyMock(return_value="the name") type(fake_vtherm).name = PropertyMock(return_value="the name")
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# 1. creation # 1. creation
power_manager = FeaturePowerManager(fake_vtherm, hass) power_manager = FeaturePowerManager(fake_vtherm, hass)
@@ -80,10 +79,19 @@ async def test_power_feature_manager(
assert custom_attributes["current_max_power"] is None assert custom_attributes["current_max_power"] is None
# 2. post_init # 2. post_init
power_manager.post_init( vtherm_api.find_central_configuration = MagicMock()
vtherm_api.central_power_manager.post_init(
{ {
CONF_POWER_SENSOR: "sensor.the_power_sensor", CONF_POWER_SENSOR: "sensor.the_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor", CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 13,
}
)
assert vtherm_api.central_power_manager.is_configured
power_manager.post_init(
{
CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 10, CONF_PRESET_POWER: 10,
CONF_DEVICE_POWER: 1234, CONF_DEVICE_POWER: 1234,
@@ -111,21 +119,14 @@ async def test_power_feature_manager(
assert power_manager.is_configured is True assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN assert power_manager.overpowering_state == STATE_UNKNOWN
assert len(power_manager._active_listener) == 2 assert len(power_manager._active_listener) == 0 # no more listening
# 4. test refresh and check_overpowering with the parametrized # 4. test refresh and check_overpowering with the parametrized
side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", power),
"sensor.the_max_power_sensor": State(
"sensor.the_max_power_sensor", max_power
),
},
State("unknown.entity_id", "unknown"),
)
# fmt:off # fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()) as mock_get_state: with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=max_power), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=power):
# fmt:on # fmt:on
# Finish the mock configuration # Finish the mock configuration
tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, "climate.vtherm") tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, "climate.vtherm")
tpi_algo._on_percent = 1 # pylint: disable="protected-access" tpi_algo._on_percent = 1 # pylint: disable="protected-access"
@@ -134,8 +135,82 @@ async def test_power_feature_manager(
type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate) type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate)
type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo) type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo)
type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1) type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1)
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
type(fake_vtherm)._saved_preset_mode = PropertyMock(return_value=PRESET_ECO) ret = await power_manager.check_power_available()
assert ret == check_power_available
@pytest.mark.parametrize(
"is_over_climate, current_overpowering_state, is_overpowering, new_overpowering_state, msg_sent",
[
# false -> false
(False, STATE_OFF, False, STATE_OFF, False),
# false -> true
(False, STATE_OFF, True, STATE_ON, True),
# true -> true
(False, STATE_ON, True, STATE_ON, False),
# true -> False
(False, STATE_ON, False, STATE_OFF, True),
# Same with over_climate
# false -> false
(True, STATE_OFF, False, STATE_OFF, False),
# false -> true
(True, STATE_OFF, True, STATE_ON, True),
# true -> true
(True, STATE_ON, True, STATE_ON, False),
# true -> False
(True, STATE_ON, False, STATE_OFF, True),
],
)
async def test_power_feature_manager_set_overpowering(
hass,
is_over_climate,
current_overpowering_state,
is_overpowering,
new_overpowering_state,
msg_sent,
):
"""Test the set_overpowering method of FeaturePowerManager"""
fake_vtherm = MagicMock(spec=BaseThermostat)
type(fake_vtherm).name = PropertyMock(return_value="the name")
vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass)
# 1. creation / init
power_manager = FeaturePowerManager(fake_vtherm, hass)
vtherm_api.find_central_configuration = MagicMock()
vtherm_api.central_power_manager.post_init(
{
CONF_POWER_SENSOR: "sensor.the_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 13,
}
)
assert vtherm_api.central_power_manager.is_configured
power_manager.post_init(
{
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 10,
CONF_DEVICE_POWER: 1234,
}
)
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
# check overpowering
power_manager._overpowering_state = current_overpowering_state
# fmt:off
with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=2000), \
patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=1000):
# fmt:on
# Finish mocking
fake_vtherm.is_over_climate = is_over_climate
fake_vtherm.preset_mode = MagicMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER)
fake_vtherm._saved_preset_mode = PRESET_ECO
fake_vtherm.save_hvac_mode = MagicMock() fake_vtherm.save_hvac_mode = MagicMock()
fake_vtherm.restore_hvac_mode = AsyncMock() fake_vtherm.restore_hvac_mode = AsyncMock()
@@ -147,26 +222,17 @@ async def test_power_feature_manager(
fake_vtherm.update_custom_attributes = MagicMock() fake_vtherm.update_custom_attributes = MagicMock()
ret = await power_manager.refresh_state() # Call set_overpowering
assert ret == changed await power_manager.set_overpowering(is_overpowering, 1234)
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
assert power_manager.current_power == power
assert power_manager.current_max_power == max_power
# check overpowering assert power_manager.overpowering_state == new_overpowering_state
power_manager._overpowering_state = current_overpowering_state
ret2 = await power_manager.check_overpowering()
assert ret2 == check_overpowering_ret
assert power_manager.overpowering_state == overpowering_state
assert mock_get_state.call_count == 2
if power_manager.overpowering_state == STATE_OFF: if not is_overpowering:
assert power_manager.overpowering_state == STATE_OFF
assert fake_vtherm.save_hvac_mode.call_count == 0 assert fake_vtherm.save_hvac_mode.call_count == 0
assert fake_vtherm.save_preset_mode.call_count == 0 assert fake_vtherm.save_preset_mode.call_count == 0
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0 assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0
assert fake_vtherm.async_set_preset_mode_internal.call_count == 0 assert fake_vtherm.async_set_preset_mode_internal.call_count == 0
assert fake_vtherm.send_event.call_count == nb_call
if current_overpowering_state == STATE_ON: if current_overpowering_state == STATE_ON:
assert fake_vtherm.update_custom_attributes.call_count == 1 assert fake_vtherm.update_custom_attributes.call_count == 1
@@ -178,18 +244,24 @@ async def test_power_feature_manager(
else: else:
assert fake_vtherm.update_custom_attributes.call_count == 0 assert fake_vtherm.update_custom_attributes.call_count == 0
if nb_call == 1: if msg_sent:
fake_vtherm.send_event.assert_has_calls( fake_vtherm.send_event.assert_has_calls(
[ [
call.fake_vtherm.send_event( call.fake_vtherm.send_event(
EventType.POWER_EVENT, EventType.POWER_EVENT,
{'type': 'end', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power}), {
"type": "end",
"current_power": 1000,
"device_power": 1234,
"current_max_power": 2000,
},
),
] ]
) )
# is_overpowering is True
else:
elif power_manager.overpowering_state == STATE_ON: assert power_manager.overpowering_state == STATE_ON
if is_over_climate: if is_over_climate and current_overpowering_state == STATE_OFF:
assert fake_vtherm.save_hvac_mode.call_count == 1 assert fake_vtherm.save_hvac_mode.call_count == 1
else: else:
assert fake_vtherm.save_hvac_mode.call_count == 0 assert fake_vtherm.save_hvac_mode.call_count == 0
@@ -209,30 +281,37 @@ async def test_power_feature_manager(
assert fake_vtherm.restore_hvac_mode.call_count == 0 assert fake_vtherm.restore_hvac_mode.call_count == 0
assert fake_vtherm.restore_preset_mode.call_count == 0 assert fake_vtherm.restore_preset_mode.call_count == 0
if nb_call == 1: if msg_sent:
fake_vtherm.send_event.assert_has_calls( fake_vtherm.send_event.assert_has_calls(
[ [
call.fake_vtherm.send_event( call.fake_vtherm.send_event(
EventType.POWER_EVENT, EventType.POWER_EVENT,
{'type': 'start', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power, 'current_power_consumption': 1234.0}), {
"type": "start",
"current_power": 1000,
"device_power": 1234,
"current_max_power": 2000,
"current_power_consumption": 1234.0,
},
),
] ]
) )
fake_vtherm.reset_mock() fake_vtherm.reset_mock()
# 5. Check custom_attributes # 5. Check custom_attributes
custom_attributes = {} custom_attributes = {}
power_manager.add_custom_attributes(custom_attributes) power_manager.add_custom_attributes(custom_attributes)
assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor" assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor"
assert ( assert (
custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor" custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor"
) )
assert custom_attributes["overpowering_state"] == overpowering_state assert custom_attributes["overpowering_state"] == new_overpowering_state
assert custom_attributes["is_power_configured"] is True assert custom_attributes["is_power_configured"] is True
assert custom_attributes["device_power"] == 1234 assert custom_attributes["device_power"] == 1234
assert custom_attributes["power_temp"] == 10 assert custom_attributes["power_temp"] == 10
assert custom_attributes["current_power"] == power assert custom_attributes["current_power"] == 1000
assert custom_attributes["current_max_power"] == max_power assert custom_attributes["current_max_power"] == 2000
power_manager.stop_listening() power_manager.stop_listening()
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -241,10 +320,15 @@ async def test_power_feature_manager(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_off( async def test_power_management_hvac_off(
hass: HomeAssistant, skip_hass_states_is_state hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
): ):
"""Test the Power management""" """Test the Power management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="TheOverSwitchMockName", title="TheOverSwitchMockName",
@@ -257,29 +341,24 @@ async def test_power_management_hvac_off(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch", CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01, CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100, CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12, CONF_PRESET_POWER: 12,
}, },
) )
entity: ThermostatOverSwitch = await create_thermostat( entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname", temps
) )
assert entity assert entity
@@ -292,34 +371,53 @@ async def test_power_management_hvac_off(
assert entity.power_manager.overpowering_state is STATE_UNKNOWN assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.hvac_mode == HVACMode.OFF assert entity.hvac_mode == HVACMode.OFF
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
# Send power mesurement # Send power mesurement
await send_power_change_event(entity, 50, datetime.now()) # fmt:off
assert await entity.power_manager.check_overpowering() is False side_effects = SideEffects(
{
"sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
"sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
},
State("unknown.entity_id", "unknown"),
)
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
# fmt: on
await send_power_change_event(entity, 50, now)
assert entity.power_manager.is_overpowering_detected is False
# All configuration is not complete # All configuration is not complete
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_UNKNOWN assert entity.power_manager.overpowering_state is STATE_UNKNOWN # due to hvac_off
# Send power max mesurement # Send power max mesurement
await send_max_power_change_event(entity, 300, datetime.now()) now = now + timedelta(seconds=61)
assert await entity.power_manager.check_overpowering() is False VersatileThermostatAPI.get_vtherm_api()._set_now(now)
# All configuration is complete and power is < power_max await send_max_power_change_event(entity, 300, now)
assert entity.preset_mode is PRESET_BOOST assert entity.power_manager.is_overpowering_detected is False
assert entity.power_manager.overpowering_state is STATE_OFF # All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_UNKNOWN # # due to hvac_off
# Send power max mesurement too low but HVACMode is off # Send power max mesurement too low but HVACMode is off
with patch( side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" # fmt:off
) as mock_send_event, patch( with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
) as mock_heater_on, patch( patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
) as mock_heater_off: # fmt: on
now = now + timedelta(seconds=61)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 149, datetime.now()) await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.power_manager.check_overpowering() is True assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off # All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_ON assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
assert mock_heater_on.call_count == 0 assert mock_heater_on.call_count == 0
@@ -328,9 +426,17 @@ async def test_power_management_hvac_off(
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state): async def test_power_management_hvac_on(
hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
):
"""Test the Power management""" """Test the Power management"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="TheOverSwitchMockName", title="TheOverSwitchMockName",
@@ -343,32 +449,30 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch", CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01, CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100, CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12, CONF_PRESET_POWER: 12,
}, },
) )
entity: ThermostatOverSwitch = await create_thermostat( entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname", temps
) )
assert entity assert entity
now: datetime = NowClass.get_now(hass)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
tpi_algo = entity._prop_algorithm tpi_algo = entity._prop_algorithm
assert tpi_algo assert tpi_algo
@@ -380,24 +484,40 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
assert entity.target_temperature == 19 assert entity.target_temperature == 19
# Send power mesurement # Send power mesurement
await send_power_change_event(entity, 50, datetime.now()) side_effects = SideEffects(
# Send power max mesurement {
await send_max_power_change_event(entity, 300, datetime.now()) "sensor.the_power_sensor": State("sensor.the_power_sensor", 50),
assert await entity.power_manager.check_overpowering() is False "sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300),
# All configuration is complete and power is < power_max },
assert entity.preset_mode is PRESET_BOOST State("unknown.entity_id", "unknown"),
assert entity.power_manager.overpowering_state is STATE_OFF )
# fmt:off
with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()):
# fmt: on
await send_power_change_event(entity, 50, datetime.now())
# Send power max mesurement
now = now + timedelta(seconds=61)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 300, datetime.now())
assert entity.power_manager.is_overpowering_detected is False
# All configuration is complete and power is < power_max
assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_OFF
# Send power max mesurement too low and HVACMode is on # Send power max mesurement too low and HVACMode is on
with patch( side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149))
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" # fmt:off
) as mock_send_event, patch( with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
) as mock_heater_on, patch( patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
) as mock_heater_off: # fmt: on
now = now + timedelta(seconds=61)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_max_power_change_event(entity, 149, datetime.now()) await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.power_manager.check_overpowering() is True assert entity.power_manager.is_overpowering_detected is True
# All configuration is complete and power is > power_max we switch to POWER preset # All configuration is complete and power is > power_max we switch to POWER preset
assert entity.preset_mode is PRESET_POWER assert entity.preset_mode is PRESET_POWER
assert entity.power_manager.overpowering_state is STATE_ON assert entity.power_manager.overpowering_state is STATE_ON
@@ -424,15 +544,18 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
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 unseet power preset
with patch( side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 48))
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" # fmt:off
) as mock_send_event, patch( with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \
) as mock_heater_on, patch( patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off:
) as mock_heater_off: # fmt: on
now = now + timedelta(seconds=61)
VersatileThermostatAPI.get_vtherm_api()._set_now(now)
await send_power_change_event(entity, 48, datetime.now()) await send_power_change_event(entity, 48, datetime.now())
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, we restore previous preset # All configuration is complete and power is < power_max, we restore previous preset
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.power_manager.overpowering_state is STATE_OFF assert entity.power_manager.overpowering_state is STATE_OFF
@@ -462,10 +585,16 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_power_management_energy_over_switch( async def test_power_management_energy_over_switch(
hass: HomeAssistant, skip_hass_states_is_state hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager
): ):
"""Test the Power management energy mesurement""" """Test the Power management energy mesurement"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="TheOverSwitchMockName", title="TheOverSwitchMockName",
@@ -478,30 +607,24 @@ async def test_power_management_energy_over_switch(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_HEATER: "switch.mock_switch", CONF_UNDERLYING_LIST: ["switch.mock_switch", "switch.mock_switch2"],
CONF_HEATER_2: "switch.mock_switch2",
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01, CONF_TPI_COEF_EXT: 0.01,
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_MIN_ON_PERCENT: 0.3,
CONF_POWER_SENSOR: "sensor.mock_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor",
CONF_DEVICE_POWER: 100, CONF_DEVICE_POWER: 100,
CONF_PRESET_POWER: 12, CONF_PRESET_POWER: 12,
}, },
) )
entity: ThermostatOverSwitch = await create_thermostat( entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname", temps
) )
assert entity assert entity
@@ -523,6 +646,8 @@ async def test_power_management_energy_over_switch(
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
await send_temperature_change_event(entity, 15, datetime.now()) await send_temperature_change_event(entity, 15, datetime.now())
await hass.async_block_till_done()
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19 assert entity.target_temperature == 19
@@ -594,6 +719,12 @@ async def test_power_management_energy_over_climate(
): ):
"""Test the Power management for a over_climate thermostat""" """Test the Power management for a over_climate thermostat"""
temps = {
"eco": 17,
"comfort": 18,
"boost": 19,
}
the_mock_underlying = MagicMockClimate() the_mock_underlying = MagicMockClimate()
with patch( with patch(
"custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate",
@@ -611,14 +742,11 @@ async def test_power_management_energy_over_climate(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, CONF_TEMP_MAX: 30,
"eco_temp": 17,
"comfort_temp": 18,
"boost_temp": 19,
CONF_USE_WINDOW_FEATURE: False, CONF_USE_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: False, CONF_USE_MOTION_FEATURE: False,
CONF_USE_POWER_FEATURE: True, CONF_USE_POWER_FEATURE: True,
CONF_USE_PRESENCE_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False,
CONF_CLIMATE: "climate.mock_climate", CONF_UNDERLYING_LIST: ["climate.mock_climate"],
CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_MINIMAL_ACTIVATION_DELAY: 30,
CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_DELAY_MIN: 5,
CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_SAFETY_MIN_ON_PERCENT: 0.3,
@@ -630,7 +758,7 @@ async def test_power_management_energy_over_climate(
) )
entity: ThermostatOverSwitch = await create_thermostat( entity: ThermostatOverSwitch = await create_thermostat(
hass, entry, "climate.theoverclimatemockname" hass, entry, "climate.theoverclimatemockname", temps
) )
assert entity assert entity
assert entity.is_over_climate assert entity.is_over_climate