From 24fcb7a161e7c24525a30ff1ab6276002bfbfae4 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Fri, 3 Jan 2025 17:43:27 +0000 Subject: [PATCH] All tests not ok --- .../versatile_thermostat/base_manager.py | 4 + .../versatile_thermostat/base_thermostat.py | 18 +- .../central_feature_power_manager.py | 41 +- .../versatile_thermostat/commons.py | 18 + .../versatile_thermostat/config_schema.py | 6 + .../feature_power_manager.py | 262 +++--------- .../versatile_thermostat/underlyings.py | 7 +- tests/commons.py | 20 +- tests/conftest.py | 45 +- tests/test_binary_sensors.py | 6 +- tests/test_bugs.py | 6 +- tests/test_central_power_manager.py | 10 +- tests/test_multiple_switch.py | 6 +- tests/test_power.py | 384 ++++++++++++------ 14 files changed, 441 insertions(+), 392 deletions(-) diff --git a/custom_components/versatile_thermostat/base_manager.py b/custom_components/versatile_thermostat/base_manager.py index bd72705..63c7a63 100644 --- a/custom_components/versatile_thermostat/base_manager.py +++ b/custom_components/versatile_thermostat/base_manager.py @@ -38,6 +38,10 @@ class BaseFeatureManager: self._active_listener = [] + async def refresh_state(self): + """Refresh the state and return True if a change have been made""" + return False + def add_listener(self, func: CALLBACK_TYPE) -> None: """Add a listener to the list of active listener""" self._active_listener.append(func) diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 6b3ac5f..3d32c63 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -50,11 +50,10 @@ from homeassistant.const import ( ATTR_TEMPERATURE, STATE_UNAVAILABLE, STATE_UNKNOWN, - STATE_ON, ) from .const import * # pylint: disable=wildcard-import, unused-wildcard-import -from .commons import ConfigData, T +from .commons import ConfigData, T, deprecated from .config_schema import * # pylint: disable=wildcard-import, unused-wildcard-import @@ -1611,10 +1610,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): # Check overpowering condition # Not necessary for switch because each switch is checking at startup - overpowering = await self._power_manager.check_overpowering() - if overpowering == STATE_ON: - _LOGGER.debug("%s - End of cycle (overpowering)", self) - return True + # 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() 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 # For testing purpose - @DeprecationWarning + # @deprecated def _set_now(self, now: datetime): """Set the now timestamp. This is only for tests purpose This method should be replaced by the vthermAPI equivalent""" - VersatileThermostatAPI.get_vtherm_api(self._hass).set_now(now) + VersatileThermostatAPI.get_vtherm_api(self._hass)._set_now(now) + # @deprecated @property - @DeprecationWarning def now(self) -> datetime: """Get now. The local datetime or the overloaded _set_now date This method should be replaced by the vthermAPI equivalent""" diff --git a/custom_components/versatile_thermostat/central_feature_power_manager.py b/custom_components/versatile_thermostat/central_feature_power_manager.py index 2bd01f2..0113993 100644 --- a/custom_components/versatile_thermostat/central_feature_power_manager.py +++ b/custom_components/versatile_thermostat/central_feature_power_manager.py @@ -4,10 +4,6 @@ import logging from typing import Any from functools import cmp_to_key -from homeassistant.const import ( - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) from homeassistant.core import HomeAssistant, Event, callback from homeassistant.helpers.event import ( async_track_state_change_event, @@ -106,17 +102,17 @@ class CentralFeaturePowerManager(BaseFeatureManager): """Handle power changes.""" _LOGGER.debug("Thermostat %s - Receive new Power event", self) _LOGGER.debug(event) - self.refresh_state() + await self.refresh_state() @callback async def _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) - self.refresh_state() + await self.refresh_state() @overrides - def refresh_state(self) -> bool: + async def refresh_state(self) -> bool: """Tries to get the last state from sensor Returns True if a change has been made""" ret = False @@ -156,7 +152,7 @@ class CentralFeaturePowerManager(BaseFeatureManager): else 999 ) if dtimestamp >= MIN_DTEMP_SECS: - self.calculate_shedding() + await self.calculate_shedding() self._last_shedding_date = now return ret @@ -207,16 +203,19 @@ class CentralFeaturePowerManager(BaseFeatureManager): if not vtherm.power_manager.is_overpowering_detected: # To force all others vtherms to be in overpowering force_overpowering = True - await vtherm.power_manager.set_overpowering(True) + await vtherm.power_manager.set_overpowering( + True, power_consumption_max + ) else: total_affected_power += power_consumption_max - if vtherm.power_manager.is_overpowering_detected: - _LOGGER.debug( - "%s - vtherm %s should not be in overpowering state", - self, - vtherm.name, - ) - await vtherm.power_manager.set_overpowering(False) + # Always set to false + # if vtherm.power_manager.is_overpowering_detected: + _LOGGER.debug( + "%s - vtherm %s should not be in overpowering state", + self, + vtherm.name, + ) + await vtherm.power_manager.set_overpowering(False) _LOGGER.debug( "%s - after vtherm %s total_affected_power=%s, available_power=%s", @@ -290,5 +289,15 @@ class CentralFeaturePowerManager(BaseFeatureManager): """Return the power temperature""" return self._power_temp + @property + def power_sensor_entity_id(self) -> float | None: + """Return the power sensor entity id""" + return self._power_sensor_entity_id + + @property + def max_power_sensor_entity_id(self) -> float | None: + """Return the max power sensor entity id""" + return self._max_power_sensor_entity_id + def __str__(self): return "CentralPowerManager" diff --git a/custom_components/versatile_thermostat/commons.py b/custom_components/versatile_thermostat/commons.py index 6018d01..4242e5d 100644 --- a/custom_components/versatile_thermostat/commons.py +++ b/custom_components/versatile_thermostat/commons.py @@ -3,6 +3,7 @@ # pylint: disable=line-too-long import logging +import warnings from types import MappingProxyType from typing import Any, TypeVar @@ -132,3 +133,20 @@ def check_and_extract_service_configuration(service_config) -> dict: "check_and_extract_service_configuration(%s) gives '%s'", service_config, ret ) return ret + + +def deprecated(message): + """A decorator to indicate that the method/attribut is deprecated""" + + def decorator(func): + def wrapper(*args, **kwargs): + warnings.warn( + f"{func.__name__} is deprecated: {message}", + DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/custom_components/versatile_thermostat/config_schema.py b/custom_components/versatile_thermostat/config_schema.py index 90e4121..8a23db4 100644 --- a/custom_components/versatile_thermostat/config_schema.py +++ b/custom_components/versatile_thermostat/config_schema.py @@ -339,6 +339,12 @@ STEP_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name } ) +STEP_NON_CENTRAL_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name + { + vol.Optional(CONF_PRESET_POWER, default="13"): vol.Coerce(float), + } +) + STEP_POWER_DATA_SCHEMA = vol.Schema( # pylint: disable=invalid-name { vol.Required(CONF_USE_POWER_CENTRAL_CONFIG, default=True): cv.boolean, diff --git a/custom_components/versatile_thermostat/feature_power_manager.py b/custom_components/versatile_thermostat/feature_power_manager.py index 424a1c2..64b415d 100644 --- a/custom_components/versatile_thermostat/feature_power_manager.py +++ b/custom_components/versatile_thermostat/feature_power_manager.py @@ -12,22 +12,15 @@ from homeassistant.const import ( STATE_UNKNOWN, ) - from homeassistant.core import ( HomeAssistant, - callback, - Event, ) -from homeassistant.helpers.event import ( - async_track_state_change_event, - EventStateChangedData, -) -from homeassistant.components.climate import HVACMode from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import ConfigData from .base_manager import BaseFeatureManager +from .vtherm_api import VersatileThermostatAPI _LOGGER = logging.getLogger(__name__) @@ -50,10 +43,6 @@ class FeaturePowerManager(BaseFeatureManager): def __init__(self, vtherm: Any, hass: HomeAssistant): """Init of a featureManager""" super().__init__(vtherm, hass) - self._power_sensor_entity_id = None - self._max_power_sensor_entity_id = None - self._current_power = None - self._current_max_power = None self._power_temp = None self._overpowering_state = STATE_UNAVAILABLE self._is_configured: bool = False @@ -64,20 +53,11 @@ class FeaturePowerManager(BaseFeatureManager): """Reinit of the manager""" # Power management - self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR) - self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR) self._power_temp = entry_infos.get(CONF_PRESET_POWER) self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0 self._is_configured = False - self._current_power = None - self._current_max_power = None - if ( - entry_infos.get(CONF_USE_POWER_FEATURE, False) - and self._max_power_sensor_entity_id - and self._power_sensor_entity_id - and self._device_power - ): + if entry_infos.get(CONF_USE_POWER_FEATURE, False) and self._device_power: self._is_configured = True self._overpowering_state = STATE_UNKNOWN else: @@ -85,159 +65,54 @@ class FeaturePowerManager(BaseFeatureManager): @overrides def start_listening(self): - """Start listening the underlying entity""" - if self._is_configured: - self.stop_listening() - else: - return - - self.add_listener( - async_track_state_change_event( - self.hass, - [self._power_sensor_entity_id], - self._async_power_sensor_changed, - ) - ) - - self.add_listener( - async_track_state_change_event( - self.hass, - [self._max_power_sensor_entity_id], - self._async_max_power_sensor_changed, - ) - ) - - @overrides - async def refresh_state(self) -> bool: - """Tries to get the last state from sensor - Returns True if a change has been made""" - ret = False - if self._is_configured: - # try to acquire current power and power max - current_power_state = self.hass.states.get(self._power_sensor_entity_id) - if current_power_state and current_power_state.state not in ( - STATE_UNAVAILABLE, - STATE_UNKNOWN, - ): - self._current_power = float(current_power_state.state) - _LOGGER.debug( - "%s - Current power have been retrieved: %.3f", - self, - self._current_power, - ) - ret = True - - # Try to acquire power max - current_power_max_state = self.hass.states.get( - self._max_power_sensor_entity_id - ) - if current_power_max_state and current_power_max_state.state not in ( - STATE_UNAVAILABLE, - STATE_UNKNOWN, - ): - self._current_max_power = float(current_power_max_state.state) - _LOGGER.debug( - "%s - Current power max have been retrieved: %.3f", - self, - self._current_max_power, - ) - ret = True - - return ret - - @callback - async def _async_power_sensor_changed(self, event: Event[EventStateChangedData]): - """Handle power changes.""" - _LOGGER.debug("Thermostat %s - Receive new Power event", self) - _LOGGER.debug(event) - new_state = event.data.get("new_state") - old_state = event.data.get("old_state") - if ( - new_state is None - or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) - or (old_state is not None and new_state.state == old_state.state) - ): - return - - try: - current_power = float(new_state.state) - if math.isnan(current_power) or math.isinf(current_power): - raise ValueError(f"Sensor has illegal state {new_state.state}") - self._current_power = current_power - - if self._vtherm.preset_mode == PRESET_POWER: - await self._vtherm.async_control_heating() - - except ValueError as ex: - _LOGGER.error("Unable to update current_power from sensor: %s", ex) - - @callback - async def _async_max_power_sensor_changed( - self, event: Event[EventStateChangedData] - ): - """Handle power max changes.""" - _LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name) - _LOGGER.debug(event) - new_state = event.data.get("new_state") - old_state = event.data.get("old_state") - if ( - new_state is None - or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) - or (old_state is not None and new_state.state == old_state.state) - ): - return - - try: - current_power_max = float(new_state.state) - if math.isnan(current_power_max) or math.isinf(current_power_max): - raise ValueError(f"Sensor has illegal state {new_state.state}") - self._current_max_power = current_power_max - if self._vtherm.preset_mode == PRESET_POWER: - await self._vtherm.async_control_heating() - - except ValueError as ex: - _LOGGER.error("Unable to update current_power from sensor: %s", ex) + """Start listening the underlying entity. There is nothing to listen""" def add_custom_attributes(self, extra_state_attributes: dict[str, Any]): """Add some custom attributes""" + vtherm_api = VersatileThermostatAPI.get_vtherm_api() extra_state_attributes.update( { - "power_sensor_entity_id": self._power_sensor_entity_id, - "max_power_sensor_entity_id": self._max_power_sensor_entity_id, + "power_sensor_entity_id": vtherm_api.central_power_manager.power_sensor_entity_id, + "max_power_sensor_entity_id": vtherm_api.central_power_manager.max_power_sensor_entity_id, "overpowering_state": self._overpowering_state, "is_power_configured": self._is_configured, "device_power": self._device_power, "power_temp": self._power_temp, - "current_power": self._current_power, - "current_max_power": self._current_max_power, + "current_power": vtherm_api.central_power_manager.current_power, + "current_max_power": vtherm_api.central_power_manager.current_max_power, "mean_cycle_power": self.mean_cycle_power, } ) - async def check_overpowering(self) -> bool: - """Check the overpowering condition - Turn the preset_mode of the heater to 'power' if power conditions are exceeded - Returns True if overpowering is 'on' + async def check_power_available(self) -> bool: + """Check if the Vtherm can be started considering overpowering. + Returns True if no overpowering conditions are found """ - if not self._is_configured: - return False - + vtherm_api = VersatileThermostatAPI.get_vtherm_api() if ( - self._current_power is None + not self._is_configured + or not vtherm_api.central_power_manager.is_configured + ): + return True + + current_power = vtherm_api.central_power_manager.current_power + current_max_power = vtherm_api.central_power_manager.current_max_power + if ( + current_power is None + or current_max_power is None or self._device_power is None - or self._current_max_power is None ): _LOGGER.warning( - "%s - power not valued. check_overpowering not available", self + "%s - power not valued. check_power_available not available", self ) - return False + return True _LOGGER.debug( "%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f", self, - self._current_power, - self._current_max_power, + current_power, + current_max_power, self._device_power, ) @@ -253,18 +128,35 @@ class FeaturePowerManager(BaseFeatureManager): self._device_power * self._vtherm.proportional_algorithm.on_percent, ) - ret = (self._current_power + power_consumption_max) >= self._current_max_power - if ( - self._overpowering_state == STATE_OFF - and ret - and self._vtherm.hvac_mode != HVACMode.OFF - ): + ret = (current_power + power_consumption_max) < current_max_power + if not ret: + _LOGGER.info( + "%s - there is not enough power available power=%.3f, max_power=%.3f heater power=%.3f", + self, + current_power, + current_max_power, + self._device_power, + ) + return ret + + async def set_overpowering(self, overpowering: bool, power_consumption_max=0): + """Force the overpowering state for the VTherm""" + + vtherm_api = VersatileThermostatAPI.get_vtherm_api() + current_power = vtherm_api.central_power_manager.current_power + current_max_power = vtherm_api.central_power_manager.current_max_power + + if overpowering and not self.is_overpowering_detected: _LOGGER.warning( "%s - overpowering is detected. Heater preset will be set to 'power'", self, ) + + self._overpowering_state = STATE_ON + if self._vtherm.is_over_climate: self._vtherm.save_hvac_mode() + self._vtherm.save_preset_mode() await self._vtherm.async_underlying_entity_turn_off() await self._vtherm.async_set_preset_mode_internal(PRESET_POWER) @@ -272,24 +164,20 @@ class FeaturePowerManager(BaseFeatureManager): EventType.POWER_EVENT, { "type": "start", - "current_power": self._current_power, + "current_power": current_power, "device_power": self._device_power, - "current_max_power": self._current_max_power, + "current_max_power": current_max_power, "current_power_consumption": power_consumption_max, }, ) - - # Check if we need to remove the POWER preset - if ( - self._overpowering_state == STATE_ON - and not ret - and self._vtherm.preset_mode == PRESET_POWER - ): + elif not overpowering and self.is_overpowering_detected: _LOGGER.warning( "%s - end of overpowering is detected. Heater preset will be restored to '%s'", self, self._vtherm._saved_preset_mode, # pylint: disable=protected-access ) + self._overpowering_state = STATE_OFF + if self._vtherm.is_over_climate: await self._vtherm.restore_hvac_mode(False) await self._vtherm.restore_preset_mode() @@ -297,22 +185,18 @@ class FeaturePowerManager(BaseFeatureManager): EventType.POWER_EVENT, { "type": "end", - "current_power": self._current_power, + "current_power": current_power, "device_power": self._device_power, - "current_max_power": self._current_max_power, + "current_max_power": current_max_power, }, ) - - new_overpowering_state = STATE_ON if ret else STATE_OFF - if self._overpowering_state != new_overpowering_state: - self._overpowering_state = new_overpowering_state - self._vtherm.update_custom_attributes() - - return self._overpowering_state == STATE_ON - - async def set_overpowering(self, overpowering: bool): - """Force the overpowering state for the VTherm""" - raise NotImplementedError + elif not overpowering and self._overpowering_state != STATE_OFF: + # just set to not overpowering the state which was not set + self._overpowering_state = STATE_OFF + else: + # Nothing to do (already in the right state) + return + self._vtherm.update_custom_attributes() @overrides @property @@ -333,16 +217,6 @@ class FeaturePowerManager(BaseFeatureManager): """Return True if the Vtherm is in overpowering state""" 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 def power_temperature(self) -> bool: """Return the power temperature""" @@ -353,16 +227,6 @@ class FeaturePowerManager(BaseFeatureManager): """Return the device power""" return self._device_power - @property - def current_power(self) -> bool: - """Return the current power from sensor""" - return self._current_power - - @property - def current_max_power(self) -> bool: - """Return the current power from sensor""" - return self._current_max_power - @property def mean_cycle_power(self) -> float | None: """Returns the mean power consumption during the cycle""" diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index a79a918..1b95f44 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -409,9 +409,10 @@ class UnderlyingSwitch(UnderlyingEntity): await self.turn_off() return - if await self._thermostat.power_manager.check_overpowering(): - _LOGGER.debug("%s - End of cycle (3)", self) - return + # if await self._thermostat.power_manager.check_overpowering(): + # _LOGGER.debug("%s - End of cycle (3)", self) + # return + # safety mode could have change the on_time percent await self._thermostat.safety_manager.refresh_state() time = self._on_time_sec diff --git a/tests/commons.py b/tests/commons.py index 1b12423..7a54d3d 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -592,7 +592,10 @@ class MockNumber(NumberEntity): async def create_thermostat( - hass: HomeAssistant, entry: MockConfigEntry, entity_id: str + hass: HomeAssistant, + entry: MockConfigEntry, + entity_id: str, + temps: dict | None = None, ) -> BaseThermostat: """Creates and return a TPI Thermostat""" entry.add_to_hass(hass) @@ -601,6 +604,11 @@ async def create_thermostat( entity = search_entity(hass, entity_id, CLIMATE_DOMAIN) + if entity and temps: + await set_all_climate_preset_temp( + hass, entity, temps, entity.entity_id.replace("climate.", "") + ) + return entity @@ -741,9 +749,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: - await asyncio.sleep(0.1) + await entity.hass.async_block_till_done() 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: - await asyncio.sleep(0.1) + await entity.hass.async_block_till_done() async def send_window_change_event( diff --git a/tests/conftest.py b/tests/conftest.py index b6be1f5..41e5723 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,8 @@ from unittest.mock import patch import pytest +# https://github.com/miketheman/pytest-socket/pull/275 +from pytest_socket import socket_allow_hosts from homeassistant.core import StateMachine @@ -26,6 +28,12 @@ from custom_components.versatile_thermostat.config_flow import ( VersatileThermostatBaseConfigFlow, ) +from custom_components.versatile_thermostat.const import ( + CONF_POWER_SENSOR, + CONF_MAX_POWER_SENSOR, + CONF_USE_POWER_FEATURE, + CONF_PRESET_POWER, +) from custom_components.versatile_thermostat.vtherm_api import VersatileThermostatAPI from custom_components.versatile_thermostat.base_thermostat import BaseThermostat @@ -35,12 +43,6 @@ from .commons import ( FULL_CENTRAL_CONFIG_WITH_BOILER, ) -# https://github.com/miketheman/pytest-socket/pull/275 -from pytest_socket import socket_allow_hosts - -# ... - - # ... def pytest_runtest_setup(): """setup tests""" @@ -51,16 +53,6 @@ def pytest_runtest_setup(): pytest_plugins = "pytest_homeassistant_custom_component" # pylint: disable=invalid-name -# Permet d'exclure certains test en mode d'ex -# sequential = pytest.mark.sequential - - -# This fixture allow to execute some tests first and not in // -# @pytest.fixture -# def order(): -# return 1 -# - # This fixture enables loading custom integrations in all tests. # Remove to enable selective use of this fixture @pytest.fixture(autouse=True) @@ -167,3 +159,24 @@ async def init_central_config_with_boiler_fixture( await create_central_config(hass, FULL_CENTRAL_CONFIG_WITH_BOILER) yield + + +@pytest.fixture(name="init_central_power_manager") +async def init_central_power_manager_fixture( + hass, init_central_config +): # pylint: disable=unused-argument + """Initialize the central power_manager""" + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + + # 1. creation / init + vtherm_api.central_power_manager.post_init( + { + CONF_POWER_SENSOR: "sensor.the_power_sensor", + CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor", + CONF_USE_POWER_FEATURE: True, + CONF_PRESET_POWER: 13, + } + ) + assert vtherm_api.central_power_manager.is_configured + + yield diff --git a/tests/test_binary_sensors.py b/tests/test_binary_sensors.py index d2c3f15..2829566 100644 --- a/tests/test_binary_sensors.py +++ b/tests/test_binary_sensors.py @@ -159,7 +159,7 @@ async def test_overpowering_binary_sensors( await entity.async_set_preset_mode(PRESET_COMFORT) await entity.async_set_hvac_mode(HVACMode.HEAT) await send_temperature_change_event(entity, 15, now) - assert await entity.power_manager.check_overpowering() is False + assert entity.power_manager.is_overpowering_detected is False assert entity.power_manager.overpowering_state is STATE_UNKNOWN await overpowering_binary_sensor.async_my_climate_changed() @@ -168,7 +168,7 @@ async def test_overpowering_binary_sensors( await send_power_change_event(entity, 100, now) await send_max_power_change_event(entity, 150, now) - assert await entity.power_manager.check_overpowering() is True + assert entity.power_manager.is_overpowering_detected is True assert entity.power_manager.overpowering_state is STATE_ON # Simulate the event reception @@ -177,7 +177,7 @@ async def test_overpowering_binary_sensors( # set max power to a low value await send_max_power_change_event(entity, 201, now) - assert await entity.power_manager.check_overpowering() is False + assert entity.power_manager.is_overpowering_detected is False assert entity.power_manager.overpowering_state is STATE_OFF # Simulate the event reception await overpowering_binary_sensor.async_my_climate_changed() diff --git a/tests/test_bugs.py b/tests/test_bugs.py index e19ba34..4ceba72 100644 --- a/tests/test_bugs.py +++ b/tests/test_bugs.py @@ -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) await send_power_change_event(entity, 100, datetime.now()) # No overpowering yet - assert await entity.power_manager.check_overpowering() is False + assert entity.power_manager.is_overpowering_detected is False # All configuration is complete and power is < power_max assert entity.preset_mode is PRESET_COMFORT assert entity.power_manager.overpowering_state is STATE_OFF @@ -365,7 +365,7 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state): # waits that the heater starts 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.preset_mode is PRESET_BOOST 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 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.preset_mode is PRESET_POWER assert entity.power_manager.overpowering_state is STATE_ON diff --git a/tests/test_central_power_manager.py b/tests/test_central_power_manager.py index eed4a28..4bdd374 100644 --- a/tests/test_central_power_manager.py +++ b/tests/test_central_power_manager.py @@ -398,19 +398,15 @@ async def test_central_power_manageer_calculate_shedding( ) 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) vtherm.power_manager.set_overpowering = mock_set_overpowering 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 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), \ diff --git a/tests/test_multiple_switch.py b/tests/test_multiple_switch.py index e0e8583..b8f848a 100644 --- a/tests/test_multiple_switch.py +++ b/tests/test_multiple_switch.py @@ -783,7 +783,7 @@ async def test_multiple_switch_power_management( await send_power_change_event(entity, 50, datetime.now()) # Send power max mesurement await send_max_power_change_event(entity, 300, datetime.now()) - assert await entity.power_manager.check_overpowering() is False + 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 @@ -798,7 +798,7 @@ async def test_multiple_switch_power_management( ) as mock_heater_off: # 100 of the device / 4 -> 25, current power 50 so max is 75 await send_max_power_change_event(entity, 74, datetime.now()) - assert await entity.power_manager.check_overpowering() is True + assert entity.power_manager.is_overpowering_detected is True # All configuration is complete and power is > power_max we switch to POWER preset assert entity.preset_mode is PRESET_POWER assert entity.power_manager.overpowering_state is STATE_ON @@ -843,7 +843,7 @@ async def test_multiple_switch_power_management( ) as mock_heater_off: # 100 of the device / 4 -> 25, current power 50 so max is 75. With 150 no overheating await send_max_power_change_event(entity, 150, datetime.now()) - assert await entity.power_manager.check_overpowering() is False + assert entity.power_manager.is_overpowering_detected is False # All configuration is complete and power is > power_max we switch to POWER preset assert entity.preset_mode is PRESET_ECO assert entity.power_manager.overpowering_state is STATE_OFF diff --git a/tests/test_power.py b/tests/test_power.py index c2b1828..1da2e82 100644 --- a/tests/test_power.py +++ b/tests/test_power.py @@ -10,6 +10,7 @@ from custom_components.versatile_thermostat.thermostat_switch import ( from custom_components.versatile_thermostat.feature_power_manager import ( FeaturePowerManager, ) + from custom_components.versatile_thermostat.prop_algorithm import PropAlgorithm from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import @@ -17,28 +18,28 @@ logging.getLogger().setLevel(logging.DEBUG) @pytest.mark.parametrize( - "is_over_climate, is_device_active, power, max_power, current_overpowering_state, overpowering_state, nb_call, changed, check_overpowering_ret", + "is_over_climate, is_device_active, power, max_power, check_power_available", [ # don't switch to overpower (power is enough) - (False, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False), + (False, False, 1000, 3000, True), # switch to overpower (power is not enough) - (False, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True), + (False, False, 2000, 3000, False), # don't switch to overpower (power is not enough but device is already on) - (False, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False), + (False, True, 2000, 3000, True), # Same with a over_climate # don't switch to overpower (power is enough) - (True, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False), + (True, False, 1000, 3000, True), # switch to overpower (power is not enough) - (True, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True), + (True, False, 2000, 3000, False), # don't switch to overpower (power is not enough but device is already on) - (True, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False), + (True, True, 2000, 3000, True), # Leave overpowering state # switch to not overpower (power is enough) - (False, False, 1000, 3000, STATE_ON, STATE_OFF, 1, True, False), + (False, False, 1000, 3000, True), # don't switch to overpower (power is still not enough) - (False, False, 2000, 3000, STATE_ON, STATE_ON, 0, True, True), + (False, False, 2000, 3000, False), # keep overpower (power is not enough but device is already on) - (False, True, 3000, 3000, STATE_ON, STATE_ON, 0, True, True), + (False, True, 3000, 3000, False), ], ) async def test_power_feature_manager( @@ -47,17 +48,15 @@ async def test_power_feature_manager( is_device_active, power, max_power, - current_overpowering_state, - overpowering_state, - nb_call, - changed, - check_overpowering_ret, + check_power_available, ): """Test the FeaturePresenceManager class direclty""" fake_vtherm = MagicMock(spec=BaseThermostat) type(fake_vtherm).name = PropertyMock(return_value="the name") + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + # 1. creation power_manager = FeaturePowerManager(fake_vtherm, hass) @@ -80,10 +79,19 @@ async def test_power_feature_manager( assert custom_attributes["current_max_power"] is None # 2. post_init - power_manager.post_init( + vtherm_api.find_central_configuration = MagicMock() + vtherm_api.central_power_manager.post_init( { CONF_POWER_SENSOR: "sensor.the_power_sensor", CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor", + CONF_USE_POWER_FEATURE: True, + CONF_PRESET_POWER: 13, + } + ) + assert vtherm_api.central_power_manager.is_configured + + power_manager.post_init( + { CONF_USE_POWER_FEATURE: True, CONF_PRESET_POWER: 10, CONF_DEVICE_POWER: 1234, @@ -111,21 +119,14 @@ async def test_power_feature_manager( assert power_manager.is_configured is True assert power_manager.overpowering_state == STATE_UNKNOWN - assert len(power_manager._active_listener) == 2 + assert len(power_manager._active_listener) == 0 # no more listening # 4. test refresh and check_overpowering with the parametrized - side_effects = SideEffects( - { - "sensor.the_power_sensor": State("sensor.the_power_sensor", power), - "sensor.the_max_power_sensor": State( - "sensor.the_max_power_sensor", max_power - ), - }, - State("unknown.entity_id", "unknown"), - ) # fmt:off - with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()) as mock_get_state: + with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=max_power), \ + patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=power): # fmt:on + # Finish the mock configuration tpi_algo = PropAlgorithm(PROPORTIONAL_FUNCTION_TPI, 0.6, 0.01, 5, 0, "climate.vtherm") tpi_algo._on_percent = 1 # pylint: disable="protected-access" @@ -134,8 +135,82 @@ async def test_power_feature_manager( type(fake_vtherm).is_over_climate = PropertyMock(return_value=is_over_climate) type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=tpi_algo) type(fake_vtherm).nb_underlying_entities = PropertyMock(return_value=1) - type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER) - type(fake_vtherm)._saved_preset_mode = PropertyMock(return_value=PRESET_ECO) + + ret = await power_manager.check_power_available() + assert ret == check_power_available + + +@pytest.mark.parametrize( + "is_over_climate, current_overpowering_state, is_overpowering, new_overpowering_state, msg_sent", + [ + # false -> false + (False, STATE_OFF, False, STATE_OFF, False), + # false -> true + (False, STATE_OFF, True, STATE_ON, True), + # true -> true + (False, STATE_ON, True, STATE_ON, False), + # true -> False + (False, STATE_ON, False, STATE_OFF, True), + # Same with over_climate + # false -> false + (True, STATE_OFF, False, STATE_OFF, False), + # false -> true + (True, STATE_OFF, True, STATE_ON, True), + # true -> true + (True, STATE_ON, True, STATE_ON, False), + # true -> False + (True, STATE_ON, False, STATE_OFF, True), + ], +) +async def test_power_feature_manager_set_overpowering( + hass, + is_over_climate, + current_overpowering_state, + is_overpowering, + new_overpowering_state, + msg_sent, +): + """Test the set_overpowering method of FeaturePowerManager""" + fake_vtherm = MagicMock(spec=BaseThermostat) + type(fake_vtherm).name = PropertyMock(return_value="the name") + + vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(hass) + + # 1. creation / init + power_manager = FeaturePowerManager(fake_vtherm, hass) + vtherm_api.find_central_configuration = MagicMock() + vtherm_api.central_power_manager.post_init( + { + CONF_POWER_SENSOR: "sensor.the_power_sensor", + CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor", + CONF_USE_POWER_FEATURE: True, + CONF_PRESET_POWER: 13, + } + ) + assert vtherm_api.central_power_manager.is_configured + + power_manager.post_init( + { + CONF_USE_POWER_FEATURE: True, + CONF_PRESET_POWER: 10, + CONF_DEVICE_POWER: 1234, + } + ) + + assert power_manager.is_configured is True + assert power_manager.overpowering_state == STATE_UNKNOWN + + # check overpowering + power_manager._overpowering_state = current_overpowering_state + + # fmt:off + with patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_max_power", new_callable=PropertyMock, return_value=2000), \ + patch("custom_components.versatile_thermostat.central_feature_power_manager.CentralFeaturePowerManager.current_power", new_callable=PropertyMock, return_value=1000): + # fmt:on + # Finish mocking + fake_vtherm.is_over_climate = is_over_climate + fake_vtherm.preset_mode = MagicMock(return_value=PRESET_COMFORT if current_overpowering_state == STATE_OFF else PRESET_POWER) + fake_vtherm._saved_preset_mode = PRESET_ECO fake_vtherm.save_hvac_mode = MagicMock() fake_vtherm.restore_hvac_mode = AsyncMock() @@ -147,26 +222,17 @@ async def test_power_feature_manager( fake_vtherm.update_custom_attributes = MagicMock() - ret = await power_manager.refresh_state() - assert ret == changed - assert power_manager.is_configured is True - assert power_manager.overpowering_state == STATE_UNKNOWN - assert power_manager.current_power == power - assert power_manager.current_max_power == max_power + # Call set_overpowering + await power_manager.set_overpowering(is_overpowering, 1234) - # check overpowering - power_manager._overpowering_state = current_overpowering_state - ret2 = await power_manager.check_overpowering() - assert ret2 == check_overpowering_ret - assert power_manager.overpowering_state == overpowering_state - assert mock_get_state.call_count == 2 + assert power_manager.overpowering_state == new_overpowering_state - if power_manager.overpowering_state == STATE_OFF: + if not is_overpowering: + assert power_manager.overpowering_state == STATE_OFF assert fake_vtherm.save_hvac_mode.call_count == 0 assert fake_vtherm.save_preset_mode.call_count == 0 assert fake_vtherm.async_underlying_entity_turn_off.call_count == 0 assert fake_vtherm.async_set_preset_mode_internal.call_count == 0 - assert fake_vtherm.send_event.call_count == nb_call if current_overpowering_state == STATE_ON: assert fake_vtherm.update_custom_attributes.call_count == 1 @@ -178,18 +244,24 @@ async def test_power_feature_manager( else: assert fake_vtherm.update_custom_attributes.call_count == 0 - if nb_call == 1: + if msg_sent: fake_vtherm.send_event.assert_has_calls( [ call.fake_vtherm.send_event( EventType.POWER_EVENT, - {'type': 'end', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power}), + { + "type": "end", + "current_power": 1000, + "device_power": 1234, + "current_max_power": 2000, + }, + ), ] ) - - - elif power_manager.overpowering_state == STATE_ON: - if is_over_climate: + # is_overpowering is True + else: + assert power_manager.overpowering_state == STATE_ON + if is_over_climate and current_overpowering_state == STATE_OFF: assert fake_vtherm.save_hvac_mode.call_count == 1 else: assert fake_vtherm.save_hvac_mode.call_count == 0 @@ -209,30 +281,37 @@ async def test_power_feature_manager( assert fake_vtherm.restore_hvac_mode.call_count == 0 assert fake_vtherm.restore_preset_mode.call_count == 0 - if nb_call == 1: + if msg_sent: fake_vtherm.send_event.assert_has_calls( [ call.fake_vtherm.send_event( EventType.POWER_EVENT, - {'type': 'start', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power, 'current_power_consumption': 1234.0}), + { + "type": "start", + "current_power": 1000, + "device_power": 1234, + "current_max_power": 2000, + "current_power_consumption": 1234.0, + }, + ), ] ) fake_vtherm.reset_mock() - # 5. Check custom_attributes + # 5. Check custom_attributes custom_attributes = {} power_manager.add_custom_attributes(custom_attributes) assert custom_attributes["power_sensor_entity_id"] == "sensor.the_power_sensor" assert ( custom_attributes["max_power_sensor_entity_id"] == "sensor.the_max_power_sensor" ) - assert custom_attributes["overpowering_state"] == overpowering_state + assert custom_attributes["overpowering_state"] == new_overpowering_state assert custom_attributes["is_power_configured"] is True assert custom_attributes["device_power"] == 1234 assert custom_attributes["power_temp"] == 10 - assert custom_attributes["current_power"] == power - assert custom_attributes["current_max_power"] == max_power + assert custom_attributes["current_power"] == 1000 + assert custom_attributes["current_max_power"] == 2000 power_manager.stop_listening() await hass.async_block_till_done() @@ -241,10 +320,15 @@ async def test_power_feature_manager( @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_power_management_hvac_off( - hass: HomeAssistant, skip_hass_states_is_state + hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager ): """Test the Power management""" + temps = { + "eco": 17, + "comfort": 18, + "boost": 19, + } entry = MockConfigEntry( domain=DOMAIN, title="TheOverSwitchMockName", @@ -257,29 +341,24 @@ async def test_power_management_hvac_off( CONF_CYCLE_MIN: 5, CONF_TEMP_MIN: 15, CONF_TEMP_MAX: 30, - "eco_temp": 17, - "comfort_temp": 18, - "boost_temp": 19, CONF_USE_WINDOW_FEATURE: False, CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: True, CONF_USE_PRESENCE_FEATURE: False, - CONF_HEATER: "switch.mock_switch", + CONF_UNDERLYING_LIST: ["switch.mock_switch"], CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_EXT: 0.01, CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, - CONF_POWER_SENSOR: "sensor.mock_power_sensor", - CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor", CONF_DEVICE_POWER: 100, CONF_PRESET_POWER: 12, }, ) entity: ThermostatOverSwitch = await create_thermostat( - hass, entry, "climate.theoverswitchmockname" + hass, entry, "climate.theoverswitchmockname", temps ) assert entity @@ -292,34 +371,53 @@ async def test_power_management_hvac_off( assert entity.power_manager.overpowering_state is STATE_UNKNOWN assert entity.hvac_mode == HVACMode.OFF + now: datetime = NowClass.get_now(hass) + VersatileThermostatAPI.get_vtherm_api()._set_now(now) + # Send power mesurement - await send_power_change_event(entity, 50, datetime.now()) - assert await entity.power_manager.check_overpowering() is False + # fmt:off + side_effects = SideEffects( + { + "sensor.the_power_sensor": State("sensor.the_power_sensor", 50), + "sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300), + }, + State("unknown.entity_id", "unknown"), + ) + # fmt:off + with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()): + # fmt: on + await send_power_change_event(entity, 50, now) + assert entity.power_manager.is_overpowering_detected is False - # All configuration is not complete - assert entity.preset_mode is PRESET_BOOST - assert entity.power_manager.overpowering_state is STATE_UNKNOWN + # All configuration is not complete + assert entity.preset_mode is PRESET_BOOST + assert entity.power_manager.overpowering_state is STATE_UNKNOWN # due to hvac_off - # Send power max mesurement - await send_max_power_change_event(entity, 300, datetime.now()) - assert await entity.power_manager.check_overpowering() is False - # All configuration is complete and power is < power_max - assert entity.preset_mode is PRESET_BOOST - assert entity.power_manager.overpowering_state is STATE_OFF + # Send power max mesurement + now = now + timedelta(seconds=61) + VersatileThermostatAPI.get_vtherm_api()._set_now(now) + await send_max_power_change_event(entity, 300, now) + assert entity.power_manager.is_overpowering_detected is False + # All configuration is complete and power is < power_max + assert entity.preset_mode is PRESET_BOOST + assert entity.power_manager.overpowering_state is STATE_UNKNOWN # # due to hvac_off # Send power max mesurement too low but HVACMode is off - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off: + side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149)) + # fmt:off + with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ + patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off: + # fmt: on + now = now + timedelta(seconds=61) + VersatileThermostatAPI.get_vtherm_api()._set_now(now) + await send_max_power_change_event(entity, 149, datetime.now()) - assert await entity.power_manager.check_overpowering() is True + assert entity.power_manager.is_overpowering_detected is False # All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off assert entity.preset_mode is PRESET_BOOST - assert entity.power_manager.overpowering_state is STATE_ON + assert entity.power_manager.overpowering_state is STATE_UNKNOWN assert mock_send_event.call_count == 0 assert mock_heater_on.call_count == 0 @@ -328,9 +426,17 @@ async def test_power_management_hvac_off( @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) -async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is_state): +async def test_power_management_hvac_on( + hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager +): """Test the Power management""" + temps = { + "eco": 17, + "comfort": 18, + "boost": 19, + } + entry = MockConfigEntry( domain=DOMAIN, title="TheOverSwitchMockName", @@ -343,32 +449,30 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is CONF_CYCLE_MIN: 5, CONF_TEMP_MIN: 15, CONF_TEMP_MAX: 30, - "eco_temp": 17, - "comfort_temp": 18, - "boost_temp": 19, CONF_USE_WINDOW_FEATURE: False, CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: True, CONF_USE_PRESENCE_FEATURE: False, - CONF_HEATER: "switch.mock_switch", + CONF_UNDERLYING_LIST: ["switch.mock_switch"], CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_EXT: 0.01, CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, - CONF_POWER_SENSOR: "sensor.mock_power_sensor", - CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor", CONF_DEVICE_POWER: 100, CONF_PRESET_POWER: 12, }, ) entity: ThermostatOverSwitch = await create_thermostat( - hass, entry, "climate.theoverswitchmockname" + hass, entry, "climate.theoverswitchmockname", temps ) assert entity + now: datetime = NowClass.get_now(hass) + VersatileThermostatAPI.get_vtherm_api()._set_now(now) + tpi_algo = entity._prop_algorithm assert tpi_algo @@ -380,24 +484,40 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is assert entity.target_temperature == 19 # Send power mesurement - await send_power_change_event(entity, 50, datetime.now()) - # Send power max mesurement - await send_max_power_change_event(entity, 300, datetime.now()) - assert await entity.power_manager.check_overpowering() is False - # All configuration is complete and power is < power_max - assert entity.preset_mode is PRESET_BOOST - assert entity.power_manager.overpowering_state is STATE_OFF + side_effects = SideEffects( + { + "sensor.the_power_sensor": State("sensor.the_power_sensor", 50), + "sensor.the_max_power_sensor": State("sensor.the_max_power_sensor", 300), + }, + State("unknown.entity_id", "unknown"), + ) + # fmt:off + with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()): + # fmt: on + await send_power_change_event(entity, 50, datetime.now()) + # Send power max mesurement + now = now + timedelta(seconds=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 - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off: + side_effects.add_or_update_side_effect("sensor.the_max_power_sensor", State("sensor.the_max_power_sensor", 149)) + # fmt:off + with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ + patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off: + # fmt: on + now = now + timedelta(seconds=61) + VersatileThermostatAPI.get_vtherm_api()._set_now(now) + await send_max_power_change_event(entity, 149, datetime.now()) - assert await entity.power_manager.check_overpowering() is True + assert entity.power_manager.is_overpowering_detected is True # All configuration is complete and power is > power_max we switch to POWER preset assert entity.preset_mode is PRESET_POWER assert entity.power_manager.overpowering_state is STATE_ON @@ -424,15 +544,18 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is assert mock_heater_off.call_count == 1 # Send power mesurement low to unseet power preset - with patch( - "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" - ) as mock_send_event, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" - ) as mock_heater_on, patch( - "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" - ) as mock_heater_off: + side_effects.add_or_update_side_effect("sensor.the_power_sensor", State("sensor.the_power_sensor", 48)) + # fmt:off + with patch("homeassistant.core.StateMachine.get", side_effect=side_effects.get_side_effects()), \ + patch("custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event") as mock_send_event, \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on") as mock_heater_on, \ + patch("custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off") as mock_heater_off: + # fmt: on + now = now + timedelta(seconds=61) + VersatileThermostatAPI.get_vtherm_api()._set_now(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 assert entity.preset_mode is PRESET_BOOST 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_timers", [True]) async def test_power_management_energy_over_switch( - hass: HomeAssistant, skip_hass_states_is_state + hass: HomeAssistant, skip_hass_states_is_state, init_central_power_manager ): """Test the Power management energy mesurement""" + temps = { + "eco": 17, + "comfort": 18, + "boost": 19, + } + entry = MockConfigEntry( domain=DOMAIN, title="TheOverSwitchMockName", @@ -478,30 +607,24 @@ async def test_power_management_energy_over_switch( CONF_CYCLE_MIN: 5, CONF_TEMP_MIN: 15, CONF_TEMP_MAX: 30, - "eco_temp": 17, - "comfort_temp": 18, - "boost_temp": 19, CONF_USE_WINDOW_FEATURE: False, CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: True, CONF_USE_PRESENCE_FEATURE: False, - CONF_HEATER: "switch.mock_switch", - CONF_HEATER_2: "switch.mock_switch2", + CONF_UNDERLYING_LIST: ["switch.mock_switch", "switch.mock_switch2"], CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_EXT: 0.01, CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, - CONF_POWER_SENSOR: "sensor.mock_power_sensor", - CONF_MAX_POWER_SENSOR: "sensor.mock_power_max_sensor", CONF_DEVICE_POWER: 100, CONF_PRESET_POWER: 12, }, ) entity: ThermostatOverSwitch = await create_thermostat( - hass, entry, "climate.theoverswitchmockname" + hass, entry, "climate.theoverswitchmockname", temps ) assert entity @@ -523,6 +646,8 @@ async def test_power_management_energy_over_switch( await entity.async_set_preset_mode(PRESET_BOOST) await send_temperature_change_event(entity, 15, datetime.now()) + await hass.async_block_till_done() + assert entity.hvac_mode is HVACMode.HEAT assert entity.preset_mode is PRESET_BOOST assert entity.target_temperature == 19 @@ -594,6 +719,12 @@ async def test_power_management_energy_over_climate( ): """Test the Power management for a over_climate thermostat""" + temps = { + "eco": 17, + "comfort": 18, + "boost": 19, + } + the_mock_underlying = MagicMockClimate() with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", @@ -611,14 +742,11 @@ async def test_power_management_energy_over_climate( CONF_CYCLE_MIN: 5, CONF_TEMP_MIN: 15, CONF_TEMP_MAX: 30, - "eco_temp": 17, - "comfort_temp": 18, - "boost_temp": 19, CONF_USE_WINDOW_FEATURE: False, CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: True, CONF_USE_PRESENCE_FEATURE: False, - CONF_CLIMATE: "climate.mock_climate", + CONF_UNDERLYING_LIST: ["climate.mock_climate"], CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SAFETY_DELAY_MIN: 5, CONF_SAFETY_MIN_ON_PERCENT: 0.3, @@ -630,7 +758,7 @@ async def test_power_management_energy_over_climate( ) entity: ThermostatOverSwitch = await create_thermostat( - hass, entry, "climate.theoverclimatemockname" + hass, entry, "climate.theoverclimatemockname", temps ) assert entity assert entity.is_over_climate