Refactor power feature

This commit is contained in:
Jean-Marc Collin
2024-12-23 19:07:44 +00:00
parent eb503c0a02
commit 887d59a08f
21 changed files with 729 additions and 347 deletions

2
.devcontainer/pytest.ini Normal file
View File

@@ -0,0 +1,2 @@
[pytest]
asyncio_default_fixture_loop_scope = function

View File

@@ -27,7 +27,6 @@ from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
async_call_later, async_call_later,
EventStateChangedData,
) )
from homeassistant.exceptions import ConditionError from homeassistant.exceptions import ConditionError
@@ -72,7 +71,9 @@ from .prop_algorithm import PropAlgorithm
from .open_window_algorithm import WindowOpenDetectionAlgorithm from .open_window_algorithm import WindowOpenDetectionAlgorithm
from .ema import ExponentialMovingAverage from .ema import ExponentialMovingAverage
from .presence_manager import FeaturePresenceManager from .base_manager import BaseFeatureManager
from .feature_presence_manager import FeaturePresenceManager
from .feature_power_manager import FeaturePowerManager
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -124,8 +125,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"window_action", "window_action",
"motion_sensor_entity_id", "motion_sensor_entity_id",
"presence_sensor_entity_id", "presence_sensor_entity_id",
"is_presence_configured",
"power_sensor_entity_id", "power_sensor_entity_id",
"max_power_sensor_entity_id", "max_power_sensor_entity_id",
"is_power_configured",
"temperature_unit", "temperature_unit",
"is_device_active", "is_device_active",
"device_actives", "device_actives",
@@ -169,8 +172,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._fan_mode = None self._fan_mode = None
self._humidity = None self._humidity = None
self._swing_mode = None self._swing_mode = None
self._current_power = None
self._current_power_max = None
self._window_state = None self._window_state = None
self._motion_state = None self._motion_state = None
self._saved_hvac_mode = None self._saved_hvac_mode = None
@@ -184,7 +185,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._last_ext_temperature_measure = None self._last_ext_temperature_measure = None
self._last_temperature_measure = None self._last_temperature_measure = None
self._cur_ext_temp = None self._cur_ext_temp = None
self._overpowering_state = None
self._should_relaunch_control_heating = None self._should_relaunch_control_heating = None
self._security_delay_min = None self._security_delay_min = None
@@ -243,12 +243,22 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_off_reason: HVAC_OFF_REASONS | None = None self._hvac_off_reason: HVAC_OFF_REASONS | None = None
# Instanciate all features manager
self._managers: list[BaseFeatureManager] = []
self._presence_manager: FeaturePresenceManager = FeaturePresenceManager( self._presence_manager: FeaturePresenceManager = FeaturePresenceManager(
self, hass self, hass
) )
self._power_manager: FeaturePowerManager = FeaturePowerManager(self, hass)
self.register_manager(self._presence_manager)
self.register_manager(self._power_manager)
self.post_init(entry_infos) self.post_init(entry_infos)
def register_manager(self, manager: BaseFeatureManager):
"""Register a manager"""
self._managers.append(manager)
def clean_central_config_doublon( def clean_central_config_doublon(
self, config_entry: ConfigData, central_config: ConfigEntry | None self, config_entry: ConfigData, central_config: ConfigEntry | None
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -311,7 +321,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._entry_infos = entry_infos self._entry_infos = entry_infos
self._presence_manager.post_init(entry_infos) # Post init all managers
for manager in self._managers:
manager.post_init(entry_infos)
self._use_central_config_temperature = entry_infos.get( self._use_central_config_temperature = entry_infos.get(
CONF_USE_PRESETS_CENTRAL_CONFIG CONF_USE_PRESETS_CENTRAL_CONFIG
@@ -346,8 +358,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
CONF_LAST_SEEN_TEMP_SENSOR CONF_LAST_SEEN_TEMP_SENSOR
) )
self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR) self._ext_temp_sensor_entity_id = entry_infos.get(CONF_EXTERNAL_TEMP_SENSOR)
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._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR) self._window_sensor_entity_id = entry_infos.get(CONF_WINDOW_SENSOR)
self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY) self._window_delay_sec = entry_infos.get(CONF_WINDOW_DELAY)
@@ -388,7 +398,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._tpi_coef_int = entry_infos.get(CONF_TPI_COEF_INT) self._tpi_coef_int = entry_infos.get(CONF_TPI_COEF_INT)
self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT) self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT)
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
if self._ac_mode: if self._ac_mode:
# Added by https://github.com/jmcollin78/versatile_thermostat/pull/144 # Added by https://github.com/jmcollin78/versatile_thermostat/pull/144
@@ -412,20 +421,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._attr_preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
self._saved_preset_mode = PRESET_NONE self._saved_preset_mode = PRESET_NONE
# Power management
self._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
self._pmax_on = False
self._current_power = None
self._current_power_max = None
if (
self._max_power_sensor_entity_id
and self._power_sensor_entity_id
and self._device_power
):
self._pmax_on = True
else:
_LOGGER.info("%s - Power management is not fully configured", self)
# will be restored if possible # will be restored if possible
self._target_temp = None self._target_temp = None
self._saved_target_temp = PRESET_NONE self._saved_target_temp = PRESET_NONE
@@ -468,7 +463,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Memory synthesis state # Memory synthesis state
self._motion_state = None self._motion_state = None
self._window_state = None self._window_state = None
self._overpowering_state = None
self._total_energy = None self._total_energy = None
_LOGGER.debug("%s - post_init_ resetting energy to None", self) _LOGGER.debug("%s - post_init_ resetting energy to None", self)
@@ -557,25 +551,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
) )
) )
if self._power_sensor_entity_id: # start listening for all managers
self.async_on_remove( for manager in self._managers:
async_track_state_change_event( manager.start_listening()
self.hass,
[self._power_sensor_entity_id],
self._async_power_changed,
)
)
if self._max_power_sensor_entity_id:
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self._max_power_sensor_entity_id],
self._async_max_power_changed,
)
)
self._presence_manager.start_listening()
self.async_on_remove(self.remove_thermostat) self.async_on_remove(self.remove_thermostat)
@@ -594,7 +572,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Called when the thermostat will be removed""" """Called when the thermostat will be removed"""
_LOGGER.info("%s - Removing thermostat", self) _LOGGER.info("%s - Removing thermostat", self)
self._presence_manager.stop_listening() # stop listening for all managers
for manager in self._managers:
manager.stop_listening()
for under in self._underlyings: for under in self._underlyings:
under.remove_entity() under.remove_entity()
@@ -652,37 +632,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self, self,
) )
if self._pmax_on:
# 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,
)
need_write_state = 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_power_max = float(current_power_max_state.state)
_LOGGER.debug(
"%s - Current power max have been retrieved: %.3f",
self,
self._current_power_max,
)
need_write_state = True
# try to acquire window entity state # try to acquire window entity state
if self._window_sensor_entity_id: if self._window_sensor_entity_id:
window_state = self.hass.states.get(self._window_sensor_entity_id) window_state = self.hass.states.get(self._window_sensor_entity_id)
@@ -715,8 +664,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
await self._async_update_motion_temp() await self._async_update_motion_temp()
need_write_state = True need_write_state = True
if await self._presence_manager.refresh_state(): # refresh states for all managers
need_write_state = True for manager in self._managers:
if await manager.refresh_state():
need_write_state = True
if need_write_state: if need_write_state:
self.async_write_ha_state() self.async_write_ha_state()
@@ -1001,14 +952,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
""" """
return None return None
@property
def mean_cycle_power(self) -> float | None:
"""Returns the mean power consumption during the cycle"""
if not self._device_power:
return None
return float(self._device_power * self._prop_algorithm.on_percent)
@property @property
def total_energy(self) -> float | None: def total_energy(self) -> float | None:
"""Returns the total energy calculated for this thermostast""" """Returns the total energy calculated for this thermostast"""
@@ -1017,15 +960,20 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
else: else:
return None return None
@property
def device_power(self) -> float | None:
"""Returns the device_power for this thermostast"""
return self._device_power
@property @property
def overpowering_state(self) -> bool | None: def overpowering_state(self) -> bool | None:
"""Get the overpowering_state""" """Get the overpowering_state"""
return self._overpowering_state return self._power_manager.overpowering_state
@property
def power_manager(self) -> FeaturePowerManager | None:
"""Get the power manager"""
return self._power_manager
@property
def presence_manager(self) -> FeaturePresenceManager | None:
"""Get the presence manager"""
return self._presence_manager
@property @property
def window_state(self) -> str | None: def window_state(self) -> str | None:
@@ -1079,10 +1027,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property @property
def preset_mode(self) -> str | None: def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp. """Return the current preset mode comfort, eco, boost,...,"""
Requires ClimateEntityFeature.PRESET_MODE.
"""
return self._attr_preset_mode return self._attr_preset_mode
@property @property
@@ -1226,9 +1171,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode) # If AC is on maybe we have to change the temperature in force mode, but not in frost mode (there is no Frost protection possible in AC mode)
if self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL] and self.preset_mode != PRESET_NONE: if self._hvac_mode in [HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL] and self.preset_mode != PRESET_NONE:
if self.preset_mode != PRESET_FROST_PROTECTION: if self.preset_mode != PRESET_FROST_PROTECTION:
await self._async_set_preset_mode_internal(self.preset_mode, True) await self.async_set_preset_mode_internal(self.preset_mode, True)
else: else:
await self._async_set_preset_mode_internal(PRESET_ECO, True, False) await self.async_set_preset_mode_internal(PRESET_ECO, True, False)
if need_control_heating and sub_need_control_heating: if need_control_heating and sub_need_control_heating:
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
@@ -1271,12 +1216,12 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
return return
await self._async_set_preset_mode_internal( await self.async_set_preset_mode_internal(
preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset preset_mode, force=False, overwrite_saved_preset=overwrite_saved_preset
) )
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
async def _async_set_preset_mode_internal( async def async_set_preset_mode_internal(
self, preset_mode: str, force=False, overwrite_saved_preset=True self, preset_mode: str, force=False, overwrite_saved_preset=True
): ):
"""Set new preset mode.""" """Set new preset mode."""
@@ -1369,7 +1314,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._target_temp self._target_temp
) # in security just keep the current target temperature, the thermostat should be off ) # in security just keep the current target temperature, the thermostat should be off
if preset_mode == PRESET_POWER: if preset_mode == PRESET_POWER:
return self._power_temp return self._power_manager.power_temperature
if preset_mode == PRESET_ACTIVITY: if preset_mode == PRESET_ACTIVITY:
if self._ac_mode and self._hvac_mode == HVACMode.COOL: if self._ac_mode and self._hvac_mode == HVACMode.COOL:
motion_preset = ( motion_preset = (
@@ -1826,57 +1771,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
except ValueError as ex: except ValueError as ex:
_LOGGER.error("Unable to update external temperature from sensor: %s", ex) _LOGGER.error("Unable to update external temperature from sensor: %s", ex)
@callback
async def _async_power_changed(self, event: Event[EventStateChangedData]):
"""Handle power changes."""
_LOGGER.debug("Thermostat %s - Receive new Power 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 = 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._attr_preset_mode == PRESET_POWER:
await self.async_control_heating()
except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
@callback
async def _async_max_power_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_power_max = current_power_max
if self._attr_preset_mode == PRESET_POWER:
await self.async_control_heating()
except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
async def _async_update_motion_temp(self): async def _async_update_motion_temp(self):
"""Update the temperature considering the ACTIVITY preset and current motion state""" """Update the temperature considering the ACTIVITY preset and current motion state"""
_LOGGER.debug( _LOGGER.debug(
@@ -1912,7 +1806,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._target_temp, self._target_temp,
) )
async def _async_underlying_entity_turn_off(self): async def async_underlying_entity_turn_off(self):
"""Turn heater toggleable device off. Used by Window, overpowering, control_heating to turn all off""" """Turn heater toggleable device off. Used by Window, overpowering, control_heating to turn all off"""
for under in self._underlyings: for under in self._underlyings:
@@ -2041,7 +1935,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._saved_preset_mode not in HIDDEN_PRESETS self._saved_preset_mode not in HIDDEN_PRESETS
and self._saved_preset_mode is not None and self._saved_preset_mode is not None
): ):
await self._async_set_preset_mode_internal(self._saved_preset_mode) await self.async_set_preset_mode_internal(self._saved_preset_mode)
def save_hvac_mode(self): def save_hvac_mode(self):
"""Save the current hvac-mode to be restored later""" """Save the current hvac-mode to be restored later"""
@@ -2067,99 +1961,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._hvac_mode, self._hvac_mode,
) )
async def check_overpowering(self) -> bool:
"""Check the overpowering condition
Turn the preset_mode of the heater to 'power' if power conditions are exceeded
"""
if not self._pmax_on:
_LOGGER.debug(
"%s - power not configured. check_overpowering not available", self
)
return False
if (
self._current_power is None
or self._device_power is None
or self._current_power_max is None
):
_LOGGER.warning(
"%s - power not valued. check_overpowering not available", self
)
return False
_LOGGER.debug(
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
self,
self._current_power,
self._current_power_max,
self._device_power,
)
# issue 407 - power_consumption_max is power we need to add. If already active we don't need to add more power
if self.is_device_active:
power_consumption_max = 0
else:
if self.is_over_climate:
power_consumption_max = self._device_power
else:
power_consumption_max = max(
self._device_power / self.nb_underlying_entities,
self._device_power * self._prop_algorithm.on_percent,
)
ret = (self._current_power + power_consumption_max) >= self._current_power_max
if not self._overpowering_state and ret and self._hvac_mode != HVACMode.OFF:
_LOGGER.warning(
"%s - overpowering is detected. Heater preset will be set to 'power'",
self,
)
if self.is_over_climate:
self.save_hvac_mode()
self.save_preset_mode()
await self._async_underlying_entity_turn_off()
await self._async_set_preset_mode_internal(PRESET_POWER)
self.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": self._current_power,
"device_power": self._device_power,
"current_power_max": self._current_power_max,
"current_power_consumption": power_consumption_max,
},
)
# Check if we need to remove the POWER preset
if (
self._overpowering_state
and not ret
and self._attr_preset_mode == PRESET_POWER
):
_LOGGER.warning(
"%s - end of overpowering is detected. Heater preset will be restored to '%s'",
self,
self._saved_preset_mode,
)
if self.is_over_climate:
await self.restore_hvac_mode(False)
await self.restore_preset_mode()
self.send_event(
EventType.POWER_EVENT,
{
"type": "end",
"current_power": self._current_power,
"device_power": self._device_power,
"current_power_max": self._current_power_max,
},
)
if self._overpowering_state != ret:
self._overpowering_state = ret
self.update_custom_attributes()
return self._overpowering_state
async def check_central_mode( async def check_central_mode(
self, new_central_mode: str | None, old_central_mode: str | None self, new_central_mode: str | None, old_central_mode: str | None
): ):
@@ -2345,7 +2146,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self.save_preset_mode() self.save_preset_mode()
if self._prop_algorithm: if self._prop_algorithm:
self._prop_algorithm.set_security(self._security_default_on_percent) self._prop_algorithm.set_security(self._security_default_on_percent)
await self._async_set_preset_mode_internal(PRESET_SECURITY) await self.async_set_preset_mode_internal(PRESET_SECURITY)
# Turn off the underlying climate or heater if security default on_percent is 0 # Turn off the underlying climate or heater if security default on_percent is 0
if self.is_over_climate or self._security_default_on_percent <= 0.0: if self.is_over_climate or self._security_default_on_percent <= 0.0:
await self.async_set_hvac_mode(HVACMode.OFF, False) await self.async_set_hvac_mode(HVACMode.OFF, False)
@@ -2492,7 +2293,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# check auto_window conditions # check auto_window conditions
await self._async_manage_window_auto(in_cycle=True) await self._async_manage_window_auto(in_cycle=True)
# Issue 56 in over_climate mode, if the underlying climate is not initialized, try to initialize it # In over_climate mode, if the underlying climate is not initialized, try to initialize it
if not self.is_initialized: if not self.is_initialized:
if not self.init_underlyings(): if not self.init_underlyings():
# still not found, we an stop here # still not found, we an stop here
@@ -2500,8 +2301,8 @@ 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: bool = await self.check_overpowering() overpowering = await self._power_manager.check_overpowering()
if overpowering: if overpowering == STATE_ON:
_LOGGER.debug("%s - End of cycle (overpowering)", self) _LOGGER.debug("%s - End of cycle (overpowering)", self)
return True return True
@@ -2515,7 +2316,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
_LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF)", self) _LOGGER.debug("%s - End of cycle (HVAC_MODE_OFF)", self)
# A security to force stop heater if still active # A security to force stop heater if still active
if self.is_device_active: if self.is_device_active:
await self._async_underlying_entity_turn_off() await self.async_underlying_entity_turn_off()
return True return True
for under in self._underlyings: for under in self._underlyings:
@@ -2570,20 +2371,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"comfort_away_temp": self._presets_away.get( "comfort_away_temp": self._presets_away.get(
self.get_preset_away_name(PRESET_COMFORT), 0 self.get_preset_away_name(PRESET_COMFORT), 0
), ),
"power_temp": self._power_temp,
"target_temperature_step": self.target_temperature_step, "target_temperature_step": self.target_temperature_step,
"ext_current_temperature": self._cur_ext_temp, "ext_current_temperature": self._cur_ext_temp,
"ac_mode": self._ac_mode, "ac_mode": self._ac_mode,
"current_power": self._current_power,
"current_power_max": self._current_power_max,
"saved_preset_mode": self._saved_preset_mode, "saved_preset_mode": self._saved_preset_mode,
"saved_target_temp": self._saved_target_temp, "saved_target_temp": self._saved_target_temp,
"saved_hvac_mode": self._saved_hvac_mode, "saved_hvac_mode": self._saved_hvac_mode,
"motion_sensor_entity_id": self._motion_sensor_entity_id, "motion_sensor_entity_id": self._motion_sensor_entity_id,
"motion_state": self._motion_state, "motion_state": self._motion_state,
"power_sensor_entity_id": self._power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
"overpowering_state": self.overpowering_state,
"window_state": self.window_state, "window_state": self.window_state,
"window_auto_state": self.window_auto_state, "window_auto_state": self.window_auto_state,
"window_bypass_state": self._window_bypass_state, "window_bypass_state": self._window_bypass_state,
@@ -2605,8 +2400,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
).isoformat(), ).isoformat(),
"security_state": self._security_state, "security_state": self._security_state,
"minimal_activation_delay_sec": self._minimal_activation_delay, "minimal_activation_delay_sec": self._minimal_activation_delay,
"device_power": self._device_power, ATTR_MEAN_POWER_CYCLE: self._power_manager.mean_cycle_power,
ATTR_MEAN_POWER_CYCLE: self.mean_cycle_power,
ATTR_TOTAL_ENERGY: self.total_energy, ATTR_TOTAL_ENERGY: self.total_energy,
"last_update_datetime": self.now.isoformat(), "last_update_datetime": self.now.isoformat(),
"timezone": str(self._current_tz), "timezone": str(self._current_tz),
@@ -2697,7 +2491,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# If the changed preset is active, change the current temperature # If the changed preset is active, change the current temperature
# Issue #119 - reload new preset temperature also in ac mode # Issue #119 - reload new preset temperature also in ac mode
if preset.startswith(self._attr_preset_mode): if preset.startswith(self._attr_preset_mode):
await self._async_set_preset_mode_internal( await self.async_set_preset_mode_internal(
preset.rstrip(PRESET_AC_SUFFIX), force=True preset.rstrip(PRESET_AC_SUFFIX), force=True
) )
await self.async_control_heating(force=True) await self.async_control_heating(force=True)
@@ -2844,7 +2638,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
# Re-applicate the last preset if any to take change into account # Re-applicate the last preset if any to take change into account
if self._attr_preset_mode: if self._attr_preset_mode:
await self._async_set_preset_mode_internal(self._attr_preset_mode, True) await self.async_set_preset_mode_internal(self._attr_preset_mode, True)
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
await self.async_set_hvac_mode(HVACMode.OFF) await self.async_set_hvac_mode(HVACMode.OFF)

View File

@@ -148,7 +148,7 @@ class OverpoweringBinarySensor(VersatileThermostatBaseEntity, BinarySensorEntity
# _LOGGER.debug("%s - climate state change", self._attr_unique_id) # _LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_is_on old_state = self._attr_is_on
self._attr_is_on = self.my_climate.overpowering_state is True self._attr_is_on = self.my_climate.overpowering_state is STATE_ON
if old_state != self._attr_is_on: if old_state != self._attr_is_on:
self.async_write_ha_state() self.async_write_ha_state()
return return

View File

@@ -0,0 +1,355 @@
""" Implements the Power Feature Manager """
# pylint: disable=line-too-long
import logging
from typing import Any
from homeassistant.const import (
STATE_ON,
STATE_OFF,
STATE_UNAVAILABLE,
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
_LOGGER = logging.getLogger(__name__)
class FeaturePowerManager(BaseFeatureManager):
"""The implementation of the Power feature"""
def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager"""
super().__init__(vtherm, hass)
self._power_sensor_entity_id = None
self._max_power_sensor_entity_id = None
self._current_power = None
self._current_max_power = None
self._power_temp = None
self._overpowering_state = STATE_UNAVAILABLE
self._is_configured: bool = False
self._device_power: float = 0
@overrides
def post_init(self, entry_infos: ConfigData):
"""Reinit of the manager"""
# Power management
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
self._power_temp = entry_infos.get(CONF_PRESET_POWER)
self._overpowering_state = STATE_UNKNOWN
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
):
self._is_configured = True
else:
_LOGGER.info("%s - Power management is not fully configured", self)
@overrides
def start_listening(self):
"""Start listening the underlying entity"""
if self._is_configured:
self.stop_listening()
else:
return
self.add_listener(
async_track_state_change_event(
self.hass,
[self._power_sensor_entity_id],
self._async_power_sensor_changed,
)
)
self.add_listener(
async_track_state_change_event(
self.hass,
[self._max_power_sensor_entity_id],
self._async_max_power_sensor_changed,
)
)
@overrides
async def refresh_state(self) -> bool:
"""Tries to get the last state from sensor
Returns True if a change has been made"""
ret = False
if self._is_configured:
# try to acquire current power and power max
current_power_state = self.hass.states.get(self._power_sensor_entity_id)
if current_power_state and current_power_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_power = float(current_power_state.state)
_LOGGER.debug(
"%s - Current power have been retrieved: %.3f",
self,
self._current_power,
)
ret = True
# Try to acquire power max
current_power_max_state = self.hass.states.get(
self._max_power_sensor_entity_id
)
if current_power_max_state and current_power_max_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_max_power = float(current_power_max_state.state)
_LOGGER.debug(
"%s - Current power max have been retrieved: %.3f",
self,
self._current_max_power,
)
ret = True
return ret
@callback
async def _async_power_sensor_changed(self, event: Event[EventStateChangedData]):
"""Handle power changes."""
_LOGGER.debug("Thermostat %s - Receive new Power event", self)
_LOGGER.debug(event)
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if (
new_state is None
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
or (old_state is not None and new_state.state == old_state.state)
):
return
try:
current_power = float(new_state.state)
if math.isnan(current_power) or math.isinf(current_power):
raise ValueError(f"Sensor has illegal state {new_state.state}")
self._current_power = current_power
if self._vtherm.preset_mode == PRESET_POWER:
await self._vtherm.async_control_heating()
except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
@callback
async def _async_max_power_sensor_changed(
self, event: Event[EventStateChangedData]
):
"""Handle power max changes."""
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
_LOGGER.debug(event)
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if (
new_state is None
or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
or (old_state is not None and new_state.state == old_state.state)
):
return
try:
current_power_max = float(new_state.state)
if math.isnan(current_power_max) or math.isinf(current_power_max):
raise ValueError(f"Sensor has illegal state {new_state.state}")
self._current_max_power = current_power_max
if self._vtherm.preset_mode == PRESET_POWER:
await self._vtherm.async_control_heating()
except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
"""Add some custom attributes"""
extra_state_attributes.update(
{
"power_sensor_entity_id": self._power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
"overpowering_state": self._overpowering_state,
"is_power_configured": self._is_configured,
"device_power": self._device_power,
"power_temp": self._power_temp,
"current_power": self._current_power,
"current_power_max": self._current_max_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'
"""
if not self._is_configured:
return False
if (
self._current_power is None
or self._device_power is None
or self._current_max_power is None
):
_LOGGER.warning(
"%s - power not valued. check_overpowering not available", self
)
return False
_LOGGER.debug(
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
self,
self._current_power,
self._current_max_power,
self._device_power,
)
# issue 407 - power_consumption_max is power we need to add. If already active we don't need to add more power
if self._vtherm.is_device_active:
power_consumption_max = 0
else:
if self._vtherm.is_over_climate:
power_consumption_max = self._device_power
else:
power_consumption_max = max(
self._device_power / self._vtherm.nb_underlying_entities,
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
):
_LOGGER.warning(
"%s - overpowering is detected. Heater preset will be set to 'power'",
self,
)
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)
self._vtherm.send_event(
EventType.POWER_EVENT,
{
"type": "start",
"current_power": self._current_power,
"device_power": self._device_power,
"current_power_max": self._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
):
_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
)
if self._vtherm.is_over_climate:
await self._vtherm.restore_hvac_mode(False)
await self._vtherm.restore_preset_mode()
self._vtherm.send_event(
EventType.POWER_EVENT,
{
"type": "end",
"current_power": self._current_power,
"device_power": self._device_power,
"current_power_max": self._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
@overrides
@property
def is_configured(self) -> bool:
"""Return True of the presence is configured"""
return self._is_configured
@property
def overpowering_state(self) -> str | None:
"""Return the current overpowering state STATE_ON or STATE_OFF
or STATE_UNAVAILABLE if not configured"""
if not self._is_configured:
return STATE_UNAVAILABLE
return self._overpowering_state
@property
def max_power_sensor_entity_id(self) -> bool:
"""Return the power max entity id"""
return self._max_power_sensor_entity_id
@property
def power_sensor_entity_id(self) -> bool:
"""Return the power entity id"""
return self._power_sensor_entity_id
@property
def power_temperature(self) -> bool:
"""Return the power temperature"""
return self._power_temp
@property
def device_power(self) -> bool:
"""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"""
if not self._device_power or not self._vtherm.proportional_algorithm:
return None
return float(
self._device_power * self._vtherm.proportional_algorithm.on_percent
)
def __str__(self):
return f"PresenceManager-{self.name}"

View File

@@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__)
class FeaturePresenceManager(BaseFeatureManager): class FeaturePresenceManager(BaseFeatureManager):
"""A base class for all feature""" """The implementation of the Presence feature"""
def __init__(self, vtherm: Any, hass: HomeAssistant): def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager""" """Init of a featureManager"""
@@ -161,7 +161,7 @@ class FeaturePresenceManager(BaseFeatureManager):
{ {
"presence_sensor_entity_id": self._presence_sensor_entity_id, "presence_sensor_entity_id": self._presence_sensor_entity_id,
"presence_state": self._presence_state, "presence_state": self._presence_state,
"presence_configured": self._is_configured, "is_presence_configured": self._is_configured,
} }
) )

View File

@@ -367,9 +367,6 @@ class CentralConfigTemperatureNumber(
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
"""The unit of measurement""" """The unit of measurement"""
# TODO Kelvin ? It seems not because all internal values are stored in
# ° Celsius but only the render in front can be in °K depending on the
# user configuration.
return self.hass.config.units.temperature_unit return self.hass.config.units.temperature_unit

View File

@@ -165,7 +165,7 @@ class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
if not self.my_climate: if not self.my_climate:
return None return None
if self.my_climate.device_power > THRESHOLD_WATT_KILO: if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
return UnitOfEnergy.WATT_HOUR return UnitOfEnergy.WATT_HOUR
else: else:
return UnitOfEnergy.KILO_WATT_HOUR return UnitOfEnergy.KILO_WATT_HOUR
@@ -190,16 +190,17 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Called when my climate have change""" """Called when my climate have change"""
# _LOGGER.debug("%s - climate state change", self._attr_unique_id) # _LOGGER.debug("%s - climate state change", self._attr_unique_id)
if math.isnan(float(self.my_climate.mean_cycle_power)) or math.isinf( if math.isnan(
self.my_climate.mean_cycle_power float(self.my_climate.power_manager.mean_cycle_power)
): ) or math.isinf(self.my_climate.power_manager.mean_cycle_power):
raise ValueError( raise ValueError(
f"Sensor has illegal state {self.my_climate.mean_cycle_power}" f"Sensor has illegal state {self.my_climate.power_manager.mean_cycle_power}"
) )
old_state = self._attr_native_value old_state = self._attr_native_value
self._attr_native_value = round( self._attr_native_value = round(
self.my_climate.mean_cycle_power, self.suggested_display_precision self.my_climate.power_manager.mean_cycle_power,
self.suggested_display_precision,
) )
if old_state != self._attr_native_value: if old_state != self._attr_native_value:
self.async_write_ha_state() self.async_write_ha_state()
@@ -222,7 +223,7 @@ class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
if not self.my_climate: if not self.my_climate:
return None return None
if self.my_climate.device_power > THRESHOLD_WATT_KILO: if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
return UnitOfPower.WATT return UnitOfPower.WATT
else: else:
return UnitOfPower.KILO_WATT return UnitOfPower.KILO_WATT

View File

@@ -602,13 +602,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]):
if self.hvac_mode == HVACMode.OFF: if self.hvac_mode == HVACMode.OFF:
return return
device_power = self.power_manager.device_power
added_energy = 0 added_energy = 0
if ( if (
self.is_over_climate self.is_over_climate
and self._underlying_climate_delta_t is not None and self._underlying_climate_delta_t is not None
and self._device_power and device_power
): ):
added_energy = self._device_power * self._underlying_climate_delta_t added_energy = device_power * self._underlying_climate_delta_t
if self._total_energy is None: if self._total_energy is None:
self._total_energy = added_energy self._total_energy = added_energy

View File

@@ -182,8 +182,10 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]):
return return
added_energy = 0 added_energy = 0
if not self.is_over_climate and self.mean_cycle_power is not None: if not self.is_over_climate and self.power_manager.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0 added_energy = (
self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0
)
if self._total_energy is None: if self._total_energy is None:
self._total_energy = added_energy self._total_energy = added_energy

View File

@@ -265,8 +265,10 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a
return return
added_energy = 0 added_energy = 0
if not self.is_over_climate and self.mean_cycle_power is not None: if not self.is_over_climate and self.power_manager.mean_cycle_power is not None:
added_energy = self.mean_cycle_power * float(self._cycle_min) / 60.0 added_energy = (
self.power_manager.mean_cycle_power * float(self._cycle_min) / 60.0
)
if self._total_energy is None: if self._total_energy is None:
self._total_energy = added_energy self._total_energy = added_energy

View File

@@ -409,7 +409,7 @@ class UnderlyingSwitch(UnderlyingEntity):
await self.turn_off() await self.turn_off()
return return
if await self._thermostat.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

View File

@@ -746,7 +746,7 @@ async def send_power_change_event(entity: BaseThermostat, new_power, date, sleep
) )
}, },
) )
await entity._async_power_changed(power_event) await entity.power_manager._async_power_sensor_changed(power_event)
if sleep: if sleep:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@@ -772,7 +772,7 @@ async def send_max_power_change_event(
) )
}, },
) )
await entity._async_max_power_changed(power_event) await entity.power_manager._async_max_power_sensor_changed(power_event)
if sleep: if sleep:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)

View File

@@ -159,8 +159,8 @@ 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.check_overpowering() is False assert await entity.power_manager.check_overpowering() is False
assert entity.overpowering_state is None 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()
assert overpowering_binary_sensor.state is STATE_OFF assert overpowering_binary_sensor.state is STATE_OFF
@@ -168,8 +168,8 @@ 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.check_overpowering() is True assert await entity.power_manager.check_overpowering() is True
assert entity.overpowering_state is True assert entity.power_manager.overpowering_state is STATE_ON
# Simulate the event reception # Simulate the event reception
await overpowering_binary_sensor.async_my_climate_changed() await overpowering_binary_sensor.async_my_climate_changed()
@@ -177,8 +177,8 @@ 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.check_overpowering() is False assert await entity.power_manager.check_overpowering() is False
assert entity.overpowering_state is False 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()
assert overpowering_binary_sensor.state == STATE_OFF assert overpowering_binary_sensor.state == STATE_OFF

View File

@@ -334,7 +334,7 @@ async def test_bug_407(hass: HomeAssistant, skip_hass_states_is_state):
await entity.async_set_preset_mode(PRESET_COMFORT) await entity.async_set_preset_mode(PRESET_COMFORT)
assert entity.hvac_mode is HVACMode.HEAT assert entity.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_COMFORT assert entity.preset_mode is PRESET_COMFORT
assert entity.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 18 assert entity.target_temperature == 18
# waits that the heater starts # waits that the heater starts
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@@ -346,10 +346,10 @@ 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.check_overpowering() is False assert await entity.power_manager.check_overpowering() 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.overpowering_state is False assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.is_device_active is True assert entity.is_device_active is True
# 2. An already active heater that switch preset will not switch to overpowering # 2. An already active heater that switch preset will not switch to overpowering
@@ -365,10 +365,10 @@ 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.check_overpowering() is False assert await entity.power_manager.check_overpowering() 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.overpowering_state is False assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert mock_service_call.call_count >= 1 assert mock_service_call.call_count >= 1
@@ -385,10 +385,10 @@ 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.check_overpowering() is True assert await entity.power_manager.check_overpowering() 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.overpowering_state is True assert entity.power_manager.overpowering_state is STATE_ON
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])

View File

@@ -292,8 +292,11 @@ async def test_full_over_switch_wo_central_config(
assert entity._motion_preset == "comfort" assert entity._motion_preset == "comfort"
assert entity._no_motion_preset == "eco" assert entity._no_motion_preset == "eco"
assert entity._power_sensor_entity_id == "sensor.mock_power_sensor" assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor" assert (
entity.power_manager.max_power_sensor_entity_id
== "sensor.mock_max_power_sensor"
)
assert ( assert (
entity._presence_manager.presence_sensor_entity_id entity._presence_manager.presence_sensor_entity_id
@@ -409,8 +412,11 @@ async def test_full_over_switch_with_central_config(
assert entity._motion_preset == "boost" assert entity._motion_preset == "boost"
assert entity._no_motion_preset == "frost" assert entity._no_motion_preset == "frost"
assert entity._power_sensor_entity_id == "sensor.mock_power_sensor" assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
assert entity._max_power_sensor_entity_id == "sensor.mock_max_power_sensor" assert (
entity.power_manager.max_power_sensor_entity_id
== "sensor.mock_max_power_sensor"
)
assert ( assert (
entity._presence_manager.presence_sensor_entity_id entity._presence_manager.presence_sensor_entity_id

View File

@@ -1388,8 +1388,7 @@ async def test_user_config_flow_over_switch_bug_552_tpi(
# @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])
# @pytest.mark.skip
async def test_user_config_flow_over_climate_valve( async def test_user_config_flow_over_climate_valve(
hass: HomeAssistant, skip_hass_states_get hass: HomeAssistant, skip_hass_states_get
): # pylint: disable=unused-argument ): # pylint: disable=unused-argument

View File

@@ -776,17 +776,17 @@ async def test_multiple_switch_power_management(
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.target_temperature == 19 assert entity.target_temperature == 19
# 1. Send power mesurement # 1. Send power mesurement
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.check_overpowering() is False assert await entity.power_manager.check_overpowering() 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.overpowering_state is False assert entity.power_manager.overpowering_state is STATE_OFF
# 2. Send power max mesurement too low and HVACMode is on # 2. Send power max mesurement too low and HVACMode is on
with patch( with patch(
@@ -798,10 +798,10 @@ 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.check_overpowering() is True assert await entity.power_manager.check_overpowering() is True
# All configuration is complete and power is > power_max we switch to POWER preset # 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.overpowering_state is True assert entity.power_manager.overpowering_state is STATE_ON
assert entity.target_temperature == 12 assert entity.target_temperature == 12
assert mock_send_event.call_count == 2 assert mock_send_event.call_count == 2
@@ -831,7 +831,7 @@ async def test_multiple_switch_power_management(
await entity.async_set_preset_mode(PRESET_ECO) await entity.async_set_preset_mode(PRESET_ECO)
assert entity.preset_mode is PRESET_ECO assert entity.preset_mode is PRESET_ECO
# No change # No change
assert entity.overpowering_state is True assert entity.power_manager.overpowering_state is STATE_ON
# 4. Send hugh power max mesurement to release overpowering # 4. Send hugh power max mesurement to release overpowering
with patch( with patch(
@@ -843,10 +843,10 @@ 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.check_overpowering() is False assert await entity.power_manager.check_overpowering() 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.overpowering_state is False assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.target_temperature == 17 assert entity.target_temperature == 17
assert ( assert (

View File

@@ -1,17 +1,240 @@
# pylint: disable=protected-access, unused-argument, line-too-long # pylint: disable=protected-access, unused-argument, line-too-long
""" Test the Power management """ """ Test the Power management """
from unittest.mock import patch, call from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from custom_components.versatile_thermostat.thermostat_switch import ( from custom_components.versatile_thermostat.thermostat_switch import (
ThermostatOverSwitch, ThermostatOverSwitch,
) )
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 from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) 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",
[
# don't switch to overpower (power is enough)
(False, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
# switch to overpower (power is not enough)
(False, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
# don't switch to overpower (power is not enough but device is already on)
(False, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
# Same with a over_climate
# don't switch to overpower (power is enough)
(True, False, 1000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
# switch to overpower (power is not enough)
(True, False, 2000, 3000, STATE_OFF, STATE_ON, 1, True, True),
# don't switch to overpower (power is not enough but device is already on)
(True, True, 2000, 3000, STATE_OFF, STATE_OFF, 0, True, False),
# Leave overpowering state
# switch to not overpower (power is enough)
(False, False, 1000, 3000, STATE_ON, STATE_OFF, 1, True, False),
# don't switch to overpower (power is still not enough)
(False, False, 2000, 3000, STATE_ON, STATE_ON, 0, True, True),
# keep overpower (power is not enough but device is already on)
(False, True, 3000, 3000, STATE_ON, STATE_ON, 0, True, True),
],
)
async def test_power_feature_manager(
hass: HomeAssistant,
is_over_climate,
is_device_active,
power,
max_power,
current_overpowering_state,
overpowering_state,
nb_call,
changed,
check_overpowering_ret,
):
"""Test the FeaturePresenceManager class direclty"""
fake_vtherm = MagicMock(spec=BaseThermostat)
type(fake_vtherm).name = PropertyMock(return_value="the name")
# 1. creation
power_manager = FeaturePowerManager(fake_vtherm, hass)
assert power_manager is not None
assert power_manager.is_configured is False
assert power_manager.overpowering_state == STATE_UNAVAILABLE
assert power_manager.name == "the name"
assert len(power_manager._active_listener) == 0
custom_attributes = {}
power_manager.add_custom_attributes(custom_attributes)
assert custom_attributes["power_sensor_entity_id"] is None
assert custom_attributes["max_power_sensor_entity_id"] is None
assert custom_attributes["overpowering_state"] == STATE_UNAVAILABLE
assert custom_attributes["is_power_configured"] is False
assert custom_attributes["device_power"] is 0
assert custom_attributes["power_temp"] is None
assert custom_attributes["current_power"] is None
assert custom_attributes["current_power_max"] is None
# 2. post_init
power_manager.post_init(
{
CONF_POWER_SENSOR: "sensor.the_power_sensor",
CONF_MAX_POWER_SENSOR: "sensor.the_max_power_sensor",
CONF_USE_POWER_FEATURE: True,
CONF_PRESET_POWER: 10,
CONF_DEVICE_POWER: 1234,
}
)
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
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"] == STATE_UNKNOWN
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"] is None
assert custom_attributes["current_power_max"] is None
# 3. start listening
power_manager.start_listening()
assert power_manager.is_configured is True
assert power_manager.overpowering_state == STATE_UNKNOWN
assert len(power_manager._active_listener) == 2
# 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:
# 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"
type(fake_vtherm).hvac_mode = PropertyMock(return_value=HVACMode.HEAT)
type(fake_vtherm).is_device_active = PropertyMock(return_value=is_device_active)
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)
fake_vtherm.save_hvac_mode = MagicMock()
fake_vtherm.restore_hvac_mode = AsyncMock()
fake_vtherm.save_preset_mode = MagicMock()
fake_vtherm.restore_preset_mode = AsyncMock()
fake_vtherm.async_underlying_entity_turn_off = AsyncMock()
fake_vtherm.async_set_preset_mode_internal = AsyncMock()
fake_vtherm.send_event = MagicMock()
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
# check overpowering
power_manager._overpowering_state = current_overpowering_state
ret2 = await power_manager.check_overpowering()
assert ret2 == check_overpowering_ret
assert power_manager.overpowering_state == overpowering_state
assert mock_get_state.call_count == 2
if 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
assert fake_vtherm.restore_preset_mode.call_count == 1
if is_over_climate:
assert fake_vtherm.restore_hvac_mode.call_count == 1
else:
assert fake_vtherm.restore_hvac_mode.call_count == 0
else:
assert fake_vtherm.update_custom_attributes.call_count == 0
if nb_call == 1:
fake_vtherm.send_event.assert_has_calls(
[
call.fake_vtherm.send_event(
EventType.POWER_EVENT,
{'type': 'end', 'current_power': power, 'device_power': 1234, 'current_power_max': max_power}),
]
)
elif power_manager.overpowering_state == STATE_ON:
if is_over_climate:
assert fake_vtherm.save_hvac_mode.call_count == 1
else:
assert fake_vtherm.save_hvac_mode.call_count == 0
if current_overpowering_state == STATE_OFF:
assert fake_vtherm.save_preset_mode.call_count == 1
assert fake_vtherm.async_underlying_entity_turn_off.call_count == 1
assert fake_vtherm.async_set_preset_mode_internal.call_count == 1
assert fake_vtherm.send_event.call_count == 1
assert fake_vtherm.update_custom_attributes.call_count == 1
else:
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 == 0
assert fake_vtherm.update_custom_attributes.call_count == 0
assert fake_vtherm.restore_hvac_mode.call_count == 0
assert fake_vtherm.restore_preset_mode.call_count == 0
if nb_call == 1:
fake_vtherm.send_event.assert_has_calls(
[
call.fake_vtherm.send_event(
EventType.POWER_EVENT,
{'type': 'start', 'current_power': power, 'device_power': 1234, 'current_power_max': max_power, 'current_power_consumption': 1234.0}),
]
)
fake_vtherm.reset_mock()
# 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["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_power_max"] == max_power
@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(
@@ -63,23 +286,23 @@ async def test_power_management_hvac_off(
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
assert entity.preset_mode is PRESET_BOOST assert entity.preset_mode is PRESET_BOOST
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNKNOWN
assert entity.hvac_mode == HVACMode.OFF assert entity.hvac_mode == HVACMode.OFF
# Send power mesurement # Send power mesurement
await send_power_change_event(entity, 50, datetime.now()) await send_power_change_event(entity, 50, datetime.now())
assert await entity.check_overpowering() is False assert await entity.power_manager.check_overpowering() 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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNKNOWN
# 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.check_overpowering() is False assert await entity.power_manager.check_overpowering() 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.overpowering_state is False assert entity.power_manager.overpowering_state is STATE_OFF
# Send power max mesurement too low but HVACMode is off # Send power max mesurement too low but HVACMode is off
with patch( with patch(
@@ -90,10 +313,10 @@ async def test_power_management_hvac_off(
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off: ) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now()) await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.check_overpowering() is True assert await entity.power_manager.check_overpowering() is True
# All configuration is complete and power is > power_max but we stay in Boost cause thermostat if Off # 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.overpowering_state is True assert entity.power_manager.overpowering_state is STATE_ON
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
@@ -150,17 +373,17 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNKNOWN
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()) 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.check_overpowering() is False assert await entity.power_manager.check_overpowering() 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.overpowering_state is False 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( with patch(
@@ -171,10 +394,10 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off: ) as mock_heater_off:
await send_max_power_change_event(entity, 149, datetime.now()) await send_max_power_change_event(entity, 149, datetime.now())
assert await entity.check_overpowering() is True assert await entity.power_manager.check_overpowering() is True
# All configuration is complete and power is > power_max we switch to POWER preset # 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.overpowering_state is True assert entity.power_manager.overpowering_state is STATE_ON
assert entity.target_temperature == 12 assert entity.target_temperature == 12
assert mock_send_event.call_count == 2 assert mock_send_event.call_count == 2
@@ -206,10 +429,10 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off" "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
) as mock_heater_off: ) as mock_heater_off:
await send_power_change_event(entity, 48, datetime.now()) await send_power_change_event(entity, 48, datetime.now())
assert await entity.check_overpowering() is False assert await entity.power_manager.check_overpowering() 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.overpowering_state is False assert entity.power_manager.overpowering_state is STATE_OFF
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert mock_send_event.call_count == 2 assert mock_send_event.call_count == 2
@@ -303,7 +526,7 @@ async def test_power_management_energy_over_switch(
assert entity.current_temperature == 15 assert entity.current_temperature == 15
assert tpi_algo.on_percent == 1 assert tpi_algo.on_percent == 1
assert entity.device_power == 100.0 assert entity.power_manager.device_power == 100.0
assert mock_send_event.call_count == 2 assert mock_send_event.call_count == 2
assert mock_heater_on.call_count == 1 assert mock_heater_on.call_count == 1
@@ -324,7 +547,7 @@ async def test_power_management_energy_over_switch(
) as mock_heater_off: ) as mock_heater_off:
await send_temperature_change_event(entity, 18, datetime.now()) await send_temperature_change_event(entity, 18, datetime.now())
assert tpi_algo.on_percent == 0.3 assert tpi_algo.on_percent == 0.3
assert entity.mean_cycle_power == 30.0 assert entity.power_manager.mean_cycle_power == 30.0
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
@@ -346,7 +569,7 @@ async def test_power_management_energy_over_switch(
) as mock_heater_off: ) as mock_heater_off:
await send_temperature_change_event(entity, 20, datetime.now()) await send_temperature_change_event(entity, 20, datetime.now())
assert tpi_algo.on_percent == 0.0 assert tpi_algo.on_percent == 0.0
assert entity.mean_cycle_power == 0.0 assert entity.power_manager.mean_cycle_power == 0.0
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
@@ -421,7 +644,7 @@ async def test_power_management_energy_over_climate(
assert entity.current_temperature == 15 assert entity.current_temperature == 15
# Not initialised yet # Not initialised yet
assert entity.mean_cycle_power is None assert entity.power_manager.mean_cycle_power is None
assert entity._underlying_climate_start_hvac_action_date is None assert entity._underlying_climate_start_hvac_action_date is None
# Send a climate_change event with HVACAction=HEATING # Send a climate_change event with HVACAction=HEATING

View File

@@ -7,7 +7,7 @@ from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
# from datetime import timedelta, datetime # from datetime import timedelta, datetime
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.presence_manager import ( from custom_components.versatile_thermostat.feature_presence_manager import (
FeaturePresenceManager, FeaturePresenceManager,
) )
@@ -52,7 +52,7 @@ async def test_presence_feature_manager(
presence_manager.add_custom_attributes(custom_attributes) presence_manager.add_custom_attributes(custom_attributes)
assert custom_attributes["presence_sensor_entity_id"] is None assert custom_attributes["presence_sensor_entity_id"] is None
assert custom_attributes["presence_state"] == STATE_UNAVAILABLE assert custom_attributes["presence_state"] == STATE_UNAVAILABLE
assert custom_attributes["presence_configured"] is False assert custom_attributes["is_presence_configured"] is False
# 2. post_init # 2. post_init
presence_manager.post_init( presence_manager.post_init(
@@ -72,7 +72,7 @@ async def test_presence_feature_manager(
custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor" custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
) )
assert custom_attributes["presence_state"] == STATE_UNKNOWN assert custom_attributes["presence_state"] == STATE_UNKNOWN
assert custom_attributes["presence_configured"] is True assert custom_attributes["is_presence_configured"] is True
# 3. start listening # 3. start listening
presence_manager.start_listening() presence_manager.start_listening()
@@ -124,7 +124,7 @@ async def test_presence_feature_manager(
presence_manager.add_custom_attributes(custom_attributes) presence_manager.add_custom_attributes(custom_attributes)
assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor" assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
assert custom_attributes["presence_state"] == presence_state assert custom_attributes["presence_state"] == presence_state
assert custom_attributes["presence_configured"] is True assert custom_attributes["is_presence_configured"] is True
# 6. test _presence_sensor_changed with the parametrized # 6. test _presence_sensor_changed with the parametrized
fake_vtherm.find_preset_temp.return_value = temp fake_vtherm.find_preset_temp.return_value = temp
@@ -172,4 +172,4 @@ async def test_presence_feature_manager(
presence_manager.add_custom_attributes(custom_attributes) presence_manager.add_custom_attributes(custom_attributes)
assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor" assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
assert custom_attributes["presence_state"] == presence_state assert custom_attributes["presence_state"] == presence_state
assert custom_attributes["presence_configured"] is True assert custom_attributes["is_presence_configured"] is True

View File

@@ -58,7 +58,7 @@ async def test_tpi_calculation(
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300 assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0 assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured assert entity.power_manager.mean_cycle_power is None # no device power configured
tpi_algo.calculate(15, 14, 5, HVACMode.HEAT) tpi_algo.calculate(15, 14, 5, HVACMode.HEAT)
assert tpi_algo.on_percent == 0.4 assert tpi_algo.on_percent == 0.4
@@ -100,7 +100,7 @@ async def test_tpi_calculation(
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 300 assert tpi_algo.on_time_sec == 300
assert tpi_algo.off_time_sec == 0 assert tpi_algo.off_time_sec == 0
assert entity.mean_cycle_power is None # no device power configured assert entity.power_manager.mean_cycle_power is None # no device power configured
tpi_algo.set_security(0.09) tpi_algo.set_security(0.09)
tpi_algo.calculate(25, 30, 35, HVACMode.COOL) tpi_algo.calculate(25, 30, 35, HVACMode.COOL)
@@ -108,7 +108,7 @@ async def test_tpi_calculation(
assert tpi_algo.calculated_on_percent == 1 assert tpi_algo.calculated_on_percent == 1
assert tpi_algo.on_time_sec == 0 assert tpi_algo.on_time_sec == 0
assert tpi_algo.off_time_sec == 300 assert tpi_algo.off_time_sec == 300
assert entity.mean_cycle_power is None # no device power configured assert entity.power_manager.mean_cycle_power is None # no device power configured
tpi_algo.unset_security() tpi_algo.unset_security()
# The calculated values for HVACMode.OFF are the same as for HVACMode.HEAT. # The calculated values for HVACMode.OFF are the same as for HVACMode.HEAT.

View File

@@ -65,7 +65,7 @@ async def test_window_management_time_not_enough(
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -154,7 +154,7 @@ async def test_window_management_time_enough(
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -304,7 +304,7 @@ async def test_window_auto_fast(hass: HomeAssistant, skip_hass_states_is_state):
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -617,7 +617,7 @@ async def test_window_auto_auto_stop(hass: HomeAssistant, skip_hass_states_is_st
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -775,7 +775,7 @@ async def test_window_auto_no_on_percent(
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 20 assert entity.target_temperature == 20
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -891,7 +891,7 @@ async def test_window_bypass(hass: HomeAssistant, skip_hass_states_is_state):
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -1034,7 +1034,7 @@ async def test_window_auto_bypass(hass: HomeAssistant, skip_hass_states_is_state
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -1152,7 +1152,7 @@ async def test_window_bypass_reactivate(hass: HomeAssistant, skip_hass_states_is
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -1591,7 +1591,7 @@ async def test_window_action_eco_temp(hass: HomeAssistant, skip_hass_states_is_s
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF
@@ -1788,7 +1788,7 @@ async def test_window_action_frost_temp(hass: HomeAssistant, skip_hass_states_is
await entity.async_set_preset_mode(PRESET_BOOST) await entity.async_set_preset_mode(PRESET_BOOST)
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.overpowering_state is None assert entity.power_manager.overpowering_state is STATE_UNAVAILABLE
assert entity.target_temperature == 21 assert entity.target_temperature == 21
assert entity.window_state is STATE_OFF assert entity.window_state is STATE_OFF