Refactor power feature
This commit is contained in:
2
.devcontainer/pytest.ini
Normal file
2
.devcontainer/pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[pytest]
|
||||||
|
asyncio_default_fixture_loop_scope = function
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
355
custom_components/versatile_thermostat/feature_power_manager.py
Normal file
355
custom_components/versatile_thermostat/feature_power_manager.py
Normal 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}"
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user