Add Motion manager. All tests ok
This commit is contained in:
@@ -74,6 +74,7 @@ from .ema import ExponentialMovingAverage
|
||||
from .base_manager import BaseFeatureManager
|
||||
from .feature_presence_manager import FeaturePresenceManager
|
||||
from .feature_power_manager import FeaturePowerManager
|
||||
from .feature_motion_manager import FeatureMotionManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -102,7 +103,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"comfort_away_temp",
|
||||
"power_temp",
|
||||
"ac_mode",
|
||||
"current_power_max",
|
||||
"current_max_power",
|
||||
"saved_preset_mode",
|
||||
"saved_target_temp",
|
||||
"saved_hvac_mode",
|
||||
@@ -112,8 +113,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"last_temperature_datetime",
|
||||
"last_ext_temperature_datetime",
|
||||
"minimal_activation_delay_sec",
|
||||
"device_power",
|
||||
"mean_cycle_power",
|
||||
"last_update_datetime",
|
||||
"timezone",
|
||||
"window_sensor_entity_id",
|
||||
@@ -123,12 +122,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"window_auto_close_threshold",
|
||||
"window_auto_max_duration",
|
||||
"window_action",
|
||||
"motion_sensor_entity_id",
|
||||
"presence_sensor_entity_id",
|
||||
"is_presence_configured",
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
"is_power_configured",
|
||||
"temperature_unit",
|
||||
"is_device_active",
|
||||
"device_actives",
|
||||
@@ -141,6 +134,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
}
|
||||
)
|
||||
)
|
||||
.union(FeaturePresenceManager.unrecorded_attributes)
|
||||
.union(FeaturePowerManager.unrecorded_attributes)
|
||||
.union(FeatureMotionManager.unrecorded_attributes)
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -173,10 +169,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._humidity = None
|
||||
self._swing_mode = None
|
||||
self._window_state = None
|
||||
self._motion_state = None
|
||||
|
||||
self._saved_hvac_mode = None
|
||||
self._window_call_cancel = None
|
||||
self._motion_call_cancel = None
|
||||
|
||||
self._cur_temp = None
|
||||
self._ac_mode = None
|
||||
self._temp_sensor_entity_id = None
|
||||
@@ -249,9 +245,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self, hass
|
||||
)
|
||||
self._power_manager: FeaturePowerManager = FeaturePowerManager(self, hass)
|
||||
self._motion_manager: FeatureMotionManager = FeatureMotionManager(self, hass)
|
||||
|
||||
self.register_manager(self._presence_manager)
|
||||
self.register_manager(self._power_manager)
|
||||
self.register_manager(self._motion_manager)
|
||||
|
||||
self.post_init(entry_infos)
|
||||
|
||||
@@ -343,9 +341,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
if self._window_call_cancel is not None:
|
||||
self._window_call_cancel()
|
||||
self._window_call_cancel = None
|
||||
if self._motion_call_cancel is not None:
|
||||
self._motion_call_cancel()
|
||||
self._motion_call_cancel = None
|
||||
|
||||
self._cycle_min = entry_infos.get(CONF_CYCLE_MIN)
|
||||
|
||||
@@ -382,20 +377,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
end_alert_threshold=self._window_auto_close_threshold,
|
||||
)
|
||||
|
||||
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR)
|
||||
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY)
|
||||
self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY)
|
||||
if not self._motion_off_delay_sec:
|
||||
self._motion_off_delay_sec = self._motion_delay_sec
|
||||
|
||||
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
|
||||
self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
|
||||
self._motion_on = (
|
||||
self._motion_sensor_entity_id is not None
|
||||
and self._motion_preset is not None
|
||||
and self._no_motion_preset is not None
|
||||
)
|
||||
|
||||
self._tpi_coef_int = entry_infos.get(CONF_TPI_COEF_INT)
|
||||
self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT)
|
||||
|
||||
@@ -461,7 +442,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
del self._prop_algorithm
|
||||
|
||||
# Memory synthesis state
|
||||
self._motion_state = None
|
||||
self._window_state = None
|
||||
|
||||
self._total_energy = None
|
||||
@@ -542,14 +522,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._async_windows_changed,
|
||||
)
|
||||
)
|
||||
if self._motion_sensor_entity_id:
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._motion_sensor_entity_id],
|
||||
self._async_motion_changed,
|
||||
)
|
||||
)
|
||||
|
||||
# start listening for all managers
|
||||
for manager in self._managers:
|
||||
@@ -647,23 +619,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
)
|
||||
need_write_state = True
|
||||
|
||||
# try to acquire motion entity state
|
||||
if self._motion_sensor_entity_id:
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
if motion_state and motion_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
self._motion_state = motion_state.state
|
||||
_LOGGER.debug(
|
||||
"%s - Motion state have been retrieved: %s",
|
||||
self,
|
||||
self._motion_state,
|
||||
)
|
||||
# recalculate the right target_temp in activity mode
|
||||
await self._async_update_motion_temp()
|
||||
need_write_state = True
|
||||
|
||||
# refresh states for all managers
|
||||
for manager in self._managers:
|
||||
if await manager.refresh_state():
|
||||
@@ -975,6 +930,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"""Get the presence manager"""
|
||||
return self._presence_manager
|
||||
|
||||
@property
|
||||
def motion_manager(self) -> FeatureMotionManager | None:
|
||||
"""Get the motion manager"""
|
||||
return self._motion_manager
|
||||
|
||||
@property
|
||||
def window_state(self) -> str | None:
|
||||
"""Get the window_state"""
|
||||
@@ -1003,7 +963,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
@property
|
||||
def motion_state(self) -> str | None:
|
||||
"""Get the motion_state"""
|
||||
return self._motion_state
|
||||
return self._motion_manager.motion_state
|
||||
|
||||
@property
|
||||
def presence_state(self) -> str | None:
|
||||
@@ -1248,7 +1208,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
self._saved_preset_mode = preset_mode
|
||||
return
|
||||
|
||||
old_preset_mode = self._attr_preset_mode
|
||||
# Remove this old_preset_mode = self._attr_preset_mode
|
||||
recalculate = True
|
||||
if preset_mode == PRESET_NONE:
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
@@ -1256,7 +1216,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
await self.change_target_temperature(self._saved_target_temp)
|
||||
elif preset_mode == PRESET_ACTIVITY:
|
||||
self._attr_preset_mode = PRESET_ACTIVITY
|
||||
await self._async_update_motion_temp()
|
||||
await self._motion_manager.update_motion(None, False)
|
||||
else:
|
||||
if self._attr_preset_mode == PRESET_NONE:
|
||||
self._saved_target_temp = self._target_temp
|
||||
@@ -1316,18 +1276,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
if preset_mode == PRESET_POWER:
|
||||
return self._power_manager.power_temperature
|
||||
if preset_mode == PRESET_ACTIVITY:
|
||||
motion_preset = self._motion_manager.get_current_motion_preset()
|
||||
if self._ac_mode and self._hvac_mode == HVACMode.COOL:
|
||||
motion_preset = (
|
||||
self._motion_preset + PRESET_AC_SUFFIX
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset + PRESET_AC_SUFFIX
|
||||
)
|
||||
else:
|
||||
motion_preset = (
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
motion_preset = motion_preset + PRESET_AC_SUFFIX
|
||||
|
||||
if motion_preset in self._presets:
|
||||
if self._presence_manager.is_absence_detected:
|
||||
@@ -1559,139 +1510,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
# For testing purpose we need to access the inner function
|
||||
return try_window_condition
|
||||
|
||||
@callback
|
||||
async def _async_motion_changed(self, event):
|
||||
"""Handle motion changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
"%s - Motion changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._attr_preset_mode,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
|
||||
if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
|
||||
return
|
||||
|
||||
# Check delay condition
|
||||
async def try_motion_condition(_):
|
||||
try:
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
long_enough = condition.state(
|
||||
self.hass,
|
||||
self._motion_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=delay),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Motion delay condition is not satisfied (the sensor have change its state during the delay). Check motion sensor state"
|
||||
)
|
||||
# Get sensor current state
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
_LOGGER.debug(
|
||||
"%s - motion_state=%s, new_state.state=%s",
|
||||
self,
|
||||
motion_state.state,
|
||||
new_state.state,
|
||||
)
|
||||
if (
|
||||
motion_state.state == new_state.state
|
||||
and new_state.state == STATE_ON
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the motion sensor is finally 'on' after the delay", self
|
||||
)
|
||||
long_enough = True
|
||||
else:
|
||||
long_enough = False
|
||||
|
||||
if long_enough:
|
||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||
self._motion_state = new_state.state
|
||||
if self._attr_preset_mode == PRESET_ACTIVITY:
|
||||
|
||||
new_preset = (
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - Motion condition have changes. New preset temp will be %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
|
||||
# We take the presence into account
|
||||
|
||||
await self.change_target_temperature(
|
||||
self.find_preset_temp(new_preset)
|
||||
)
|
||||
self.recalculate()
|
||||
await self.async_control_heating(force=True)
|
||||
else:
|
||||
self._motion_state = (
|
||||
STATE_ON if new_state.state == STATE_OFF else STATE_OFF
|
||||
)
|
||||
|
||||
self._motion_call_cancel = None
|
||||
|
||||
im_on = self._motion_state == STATE_ON
|
||||
delay_running = self._motion_call_cancel is not None
|
||||
event_on = new_state.state == STATE_ON
|
||||
|
||||
def arm():
|
||||
"""Arm the timer"""
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
self._motion_call_cancel = async_call_later(
|
||||
self.hass, timedelta(seconds=delay), try_motion_condition
|
||||
)
|
||||
|
||||
def desarm():
|
||||
# restart the timer
|
||||
self._motion_call_cancel()
|
||||
self._motion_call_cancel = None
|
||||
|
||||
# if I'm off
|
||||
if not im_on:
|
||||
if event_on and not delay_running:
|
||||
_LOGGER.debug(
|
||||
"%s - Arm delay cause i'm off and event is on and no delay is running",
|
||||
self,
|
||||
)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
# Ignore the event
|
||||
_LOGGER.debug("%s - Event ignored cause i'm already off", self)
|
||||
return None
|
||||
else: # I'm On
|
||||
if not event_on and not delay_running:
|
||||
_LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
if event_on and delay_running:
|
||||
_LOGGER.debug(
|
||||
"%s - Desarm off delay cause i'm on and event is on and a delay is running",
|
||||
self,
|
||||
)
|
||||
desarm()
|
||||
return None
|
||||
# Ignore the event
|
||||
_LOGGER.debug("%s - Event ignored cause i'm already on", self)
|
||||
return None
|
||||
|
||||
@callback
|
||||
async def _check_initial_state(self):
|
||||
"""Prevent the device from keep running if HVAC_MODE_OFF."""
|
||||
@@ -1771,41 +1589,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update external temperature from sensor: %s", ex)
|
||||
|
||||
async def _async_update_motion_temp(self):
|
||||
"""Update the temperature considering the ACTIVITY preset and current motion state"""
|
||||
_LOGGER.debug(
|
||||
"%s - Calling _update_motion_temp preset_mode=%s, motion_state=%s",
|
||||
self,
|
||||
self._attr_preset_mode,
|
||||
self._motion_state,
|
||||
)
|
||||
if (
|
||||
self._motion_sensor_entity_id is None
|
||||
or self._attr_preset_mode != PRESET_ACTIVITY
|
||||
):
|
||||
return
|
||||
|
||||
new_preset = (
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - Motion condition have changes. New preset temp will be %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
|
||||
# We take the presence into account
|
||||
|
||||
await self.change_target_temperature(self.find_preset_temp(new_preset))
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - regarding motion, target_temp have been set to %.2f",
|
||||
self,
|
||||
self._target_temp,
|
||||
)
|
||||
|
||||
async def async_underlying_entity_turn_off(self):
|
||||
"""Turn heater toggleable device off. Used by Window, overpowering, control_heating to turn all off"""
|
||||
|
||||
@@ -2377,8 +2160,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
"saved_preset_mode": self._saved_preset_mode,
|
||||
"saved_target_temp": self._saved_target_temp,
|
||||
"saved_hvac_mode": self._saved_hvac_mode,
|
||||
"motion_sensor_entity_id": self._motion_sensor_entity_id,
|
||||
"motion_state": self._motion_state,
|
||||
"window_state": self.window_state,
|
||||
"window_auto_state": self.window_auto_state,
|
||||
"window_bypass_state": self._window_bypass_state,
|
||||
@@ -2400,7 +2181,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
).isoformat(),
|
||||
"security_state": self._security_state,
|
||||
"minimal_activation_delay_sec": self._minimal_activation_delay,
|
||||
ATTR_MEAN_POWER_CYCLE: self._power_manager.mean_cycle_power,
|
||||
ATTR_TOTAL_ENERGY: self.total_energy,
|
||||
"last_update_datetime": self.now.isoformat(),
|
||||
"timezone": str(self._current_tz),
|
||||
@@ -2633,7 +2413,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
|
||||
else:
|
||||
_LOGGER.debug("No preset_modes")
|
||||
|
||||
if self._motion_on:
|
||||
if self._motion_manager.is_configured:
|
||||
self._attr_preset_modes.append(PRESET_ACTIVITY)
|
||||
|
||||
# Re-applicate the last preset if any to take change into account
|
||||
|
||||
345
custom_components/versatile_thermostat/feature_motion_manager.py
Normal file
345
custom_components/versatile_thermostat/feature_motion_manager.py
Normal file
@@ -0,0 +1,345 @@
|
||||
""" Implements the Motion Feature Manager """
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from datetime import timedelta
|
||||
|
||||
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,
|
||||
async_call_later,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import ConditionError
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
from .const import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
from .commons import ConfigData
|
||||
|
||||
from .base_manager import BaseFeatureManager
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureMotionManager(BaseFeatureManager):
|
||||
"""The implementation of the Presence feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"motion_sensor_entity_id",
|
||||
"is_motion_configured",
|
||||
"motion_delay_sec",
|
||||
"motion_off_delay_sec",
|
||||
"motion_preset",
|
||||
"no_motion_preset",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
self._motion_state: str = STATE_UNAVAILABLE
|
||||
self._motion_sensor_entity_id: str = None
|
||||
self._motion_delay_sec: int | None = 0
|
||||
self._motion_off_delay_sec: int | None = 0
|
||||
self._motion_preset: str | None = None
|
||||
self._no_motion_preset: str | None = None
|
||||
self._is_configured: bool = False
|
||||
self._motion_call_cancel: callable = None
|
||||
|
||||
@overrides
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
if self._motion_call_cancel is not None:
|
||||
self._motion_call_cancel() # pylint: disable="not-callable"
|
||||
self._motion_call_cancel = None
|
||||
|
||||
self._motion_sensor_entity_id = entry_infos.get(CONF_MOTION_SENSOR, None)
|
||||
self._motion_delay_sec = entry_infos.get(CONF_MOTION_DELAY, 0)
|
||||
self._motion_off_delay_sec = entry_infos.get(CONF_MOTION_OFF_DELAY, None)
|
||||
if not self._motion_off_delay_sec:
|
||||
self._motion_off_delay_sec = self._motion_delay_sec
|
||||
|
||||
self._motion_preset = entry_infos.get(CONF_MOTION_PRESET)
|
||||
self._no_motion_preset = entry_infos.get(CONF_NO_MOTION_PRESET)
|
||||
if (
|
||||
self._motion_sensor_entity_id is not None
|
||||
and self._motion_preset is not None
|
||||
and self._no_motion_preset is not None
|
||||
):
|
||||
self._is_configured = True
|
||||
self._motion_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
def start_listening(self):
|
||||
"""Start listening the underlying entity"""
|
||||
if self._is_configured:
|
||||
self.stop_listening()
|
||||
self.add_listener(
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
[self._motion_sensor_entity_id],
|
||||
self._motion_sensor_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@overrides
|
||||
def stop_listening(self):
|
||||
"""Stop listening and remove the eventual timer still running"""
|
||||
self.dearm_motion_timer()
|
||||
super().stop_listening()
|
||||
|
||||
def dearm_motion_timer(self):
|
||||
"""Dearm the eventual motion time running"""
|
||||
if self._motion_call_cancel:
|
||||
self._motion_call_cancel()
|
||||
self._motion_call_cancel = None
|
||||
|
||||
@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:
|
||||
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
if motion_state and motion_state.state not in (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - Motion state have been retrieved: %s",
|
||||
self,
|
||||
self._motion_state,
|
||||
)
|
||||
# recalculate the right target_temp in activity mode
|
||||
ret = await self.update_motion(motion_state.state, False)
|
||||
|
||||
return ret
|
||||
|
||||
@callback
|
||||
async def _motion_sensor_changed(self, event: Event[EventStateChangedData]):
|
||||
"""Handle motion sensor changes."""
|
||||
new_state = event.data.get("new_state")
|
||||
_LOGGER.info(
|
||||
"%s - Motion changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
|
||||
self,
|
||||
new_state,
|
||||
self._vtherm.preset_mode,
|
||||
PRESET_ACTIVITY,
|
||||
)
|
||||
|
||||
if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
|
||||
return
|
||||
|
||||
# Check delay condition
|
||||
async def try_motion_condition(_):
|
||||
self.dearm_motion_timer()
|
||||
|
||||
try:
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
long_enough = condition.state(
|
||||
self.hass,
|
||||
self._motion_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=delay),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Motion delay condition is not satisfied (the sensor have change its state during the delay). Check motion sensor state"
|
||||
)
|
||||
# Get sensor current state
|
||||
motion_state = self.hass.states.get(self._motion_sensor_entity_id)
|
||||
_LOGGER.debug(
|
||||
"%s - motion_state=%s, new_state.state=%s",
|
||||
self,
|
||||
motion_state.state,
|
||||
new_state.state,
|
||||
)
|
||||
if (
|
||||
motion_state.state == new_state.state
|
||||
and new_state.state == STATE_ON
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - the motion sensor is finally 'on' after the delay", self
|
||||
)
|
||||
long_enough = True
|
||||
else:
|
||||
long_enough = False
|
||||
|
||||
if long_enough:
|
||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||
await self.update_motion(new_state.state)
|
||||
else:
|
||||
await self.update_motion(
|
||||
STATE_ON if new_state.state == STATE_OFF else STATE_OFF
|
||||
)
|
||||
|
||||
im_on = self._motion_state == STATE_ON
|
||||
delay_running = self._motion_call_cancel is not None
|
||||
event_on = new_state.state == STATE_ON
|
||||
|
||||
def arm():
|
||||
"""Arm the timer"""
|
||||
delay = (
|
||||
self._motion_delay_sec
|
||||
if new_state.state == STATE_ON
|
||||
else self._motion_off_delay_sec
|
||||
)
|
||||
self._motion_call_cancel = async_call_later(
|
||||
self.hass, timedelta(seconds=delay), try_motion_condition
|
||||
)
|
||||
|
||||
# if I'm off
|
||||
if not im_on:
|
||||
if event_on and not delay_running:
|
||||
_LOGGER.debug(
|
||||
"%s - Arm delay cause i'm off and event is on and no delay is running",
|
||||
self,
|
||||
)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
# Ignore the event
|
||||
_LOGGER.debug("%s - Event ignored cause i'm already off", self)
|
||||
return None
|
||||
else: # I'm On
|
||||
if not event_on and not delay_running:
|
||||
_LOGGER.info("%s - Arm delay cause i'm on and event is off", self)
|
||||
arm()
|
||||
return try_motion_condition
|
||||
if event_on and delay_running:
|
||||
_LOGGER.debug(
|
||||
"%s - Desarm off delay cause i'm on and event is on and a delay is running",
|
||||
self,
|
||||
)
|
||||
self.dearm_motion_timer()
|
||||
return None
|
||||
# Ignore the event
|
||||
_LOGGER.debug("%s - Event ignored cause i'm already on", self)
|
||||
return None
|
||||
|
||||
async def update_motion(
|
||||
self, new_state: str = None, recalculate: bool = True
|
||||
) -> bool:
|
||||
"""Update the value of the presence sensor and update the VTherm state accordingly
|
||||
Return true if a change has been made"""
|
||||
|
||||
_LOGGER.info("%s - Updating motion state. New state is %s", self, new_state)
|
||||
old_motion_state = self._motion_state
|
||||
if new_state is not None:
|
||||
self._motion_state = STATE_ON if new_state == STATE_ON else STATE_OFF
|
||||
|
||||
if self._vtherm.preset_mode == PRESET_ACTIVITY:
|
||||
new_preset = self.get_current_motion_preset()
|
||||
_LOGGER.info(
|
||||
"%s - Motion condition have changes. New preset temp will be %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
# We do not change the preset which is kept to ACTIVITY but only the target_temperature
|
||||
# We take the presence into account
|
||||
new_temp = self._vtherm.find_preset_temp(new_preset)
|
||||
old_temp = self._vtherm.target_temperature
|
||||
if new_temp != old_temp:
|
||||
await self._vtherm.change_target_temperature(new_temp)
|
||||
|
||||
if new_temp != old_temp and recalculate:
|
||||
self._vtherm.recalculate()
|
||||
await self._vtherm.async_control_heating(force=True)
|
||||
|
||||
return old_motion_state != self._motion_state
|
||||
|
||||
def get_current_motion_preset(self) -> str:
|
||||
"""Calculate and return the current motion preset"""
|
||||
return (
|
||||
self._motion_preset
|
||||
if self._motion_state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
|
||||
def add_custom_attributes(self, extra_state_attributes: dict[str, Any]):
|
||||
"""Add some custom attributes"""
|
||||
extra_state_attributes.update(
|
||||
{
|
||||
"motion_sensor_entity_id": self._motion_sensor_entity_id,
|
||||
"motion_state": self._motion_state,
|
||||
"is_motion_configured": self._is_configured,
|
||||
"motion_delay_sec": self._motion_delay_sec,
|
||||
"motion_off_delay_sec": self._motion_off_delay_sec,
|
||||
"motion_preset": self._motion_preset,
|
||||
"no_motion_preset": self._no_motion_preset,
|
||||
}
|
||||
)
|
||||
|
||||
@overrides
|
||||
@property
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True of the presence is configured"""
|
||||
return self._is_configured
|
||||
|
||||
@property
|
||||
def motion_state(self) -> str | None:
|
||||
"""Return the current presence state STATE_ON or STATE_OFF
|
||||
or STATE_UNAVAILABLE if not configured"""
|
||||
if not self._is_configured:
|
||||
return STATE_UNAVAILABLE
|
||||
return self._motion_state
|
||||
|
||||
@property
|
||||
def is_motion_detected(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._is_configured and self._motion_state in [
|
||||
STATE_ON,
|
||||
]
|
||||
|
||||
@property
|
||||
def motion_sensor_entity_id(self) -> bool:
|
||||
"""Return true if the presence is configured and presence sensor is OFF"""
|
||||
return self._motion_sensor_entity_id
|
||||
|
||||
@property
|
||||
def motion_delay_sec(self) -> bool:
|
||||
"""Return the motion delay"""
|
||||
return self._motion_delay_sec
|
||||
|
||||
@property
|
||||
def motion_off_delay_sec(self) -> bool:
|
||||
"""Return motion delay off"""
|
||||
return self._motion_off_delay_sec
|
||||
|
||||
@property
|
||||
def motion_preset(self) -> bool:
|
||||
"""Return motion preset"""
|
||||
return self._motion_preset
|
||||
|
||||
@property
|
||||
def no_motion_preset(self) -> bool:
|
||||
"""Return no motion preset"""
|
||||
return self._no_motion_preset
|
||||
|
||||
def __str__(self):
|
||||
return f"MotionManager-{self.name}"
|
||||
@@ -35,6 +35,18 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class FeaturePowerManager(BaseFeatureManager):
|
||||
"""The implementation of the Power feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"power_sensor_entity_id",
|
||||
"max_power_sensor_entity_id",
|
||||
"is_power_configured",
|
||||
"device_power",
|
||||
"power_temp",
|
||||
"current_power",
|
||||
"current_max_power",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
@@ -55,7 +67,6 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
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
|
||||
@@ -68,6 +79,7 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
and self._device_power
|
||||
):
|
||||
self._is_configured = True
|
||||
self._overpowering_state = STATE_UNKNOWN
|
||||
else:
|
||||
_LOGGER.info("%s - Power management is not fully configured", self)
|
||||
|
||||
@@ -197,7 +209,8 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
"device_power": self._device_power,
|
||||
"power_temp": self._power_temp,
|
||||
"current_power": self._current_power,
|
||||
"current_power_max": self._current_max_power,
|
||||
"current_max_power": self._current_max_power,
|
||||
"mean_cycle_power": self.mean_cycle_power,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -261,7 +274,7 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
"type": "start",
|
||||
"current_power": self._current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_power_max": self._current_max_power,
|
||||
"current_max_power": self._current_max_power,
|
||||
"current_power_consumption": power_consumption_max,
|
||||
},
|
||||
)
|
||||
@@ -286,7 +299,7 @@ class FeaturePowerManager(BaseFeatureManager):
|
||||
"type": "end",
|
||||
"current_power": self._current_power,
|
||||
"device_power": self._device_power,
|
||||
"current_power_max": self._current_max_power,
|
||||
"current_max_power": self._current_max_power,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -41,6 +41,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class FeaturePresenceManager(BaseFeatureManager):
|
||||
"""The implementation of the Presence feature"""
|
||||
|
||||
unrecorded_attributes = frozenset(
|
||||
{
|
||||
"presence_sensor_entity_id",
|
||||
"is_presence_configured",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, vtherm: Any, hass: HomeAssistant):
|
||||
"""Init of a featureManager"""
|
||||
super().__init__(vtherm, hass)
|
||||
@@ -52,11 +59,12 @@ class FeaturePresenceManager(BaseFeatureManager):
|
||||
def post_init(self, entry_infos: ConfigData):
|
||||
"""Reinit of the manager"""
|
||||
self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
|
||||
self._is_configured = (
|
||||
if (
|
||||
entry_infos.get(CONF_USE_PRESENCE_FEATURE, False)
|
||||
and self._presence_sensor_entity_id is not None
|
||||
)
|
||||
self._presence_state = STATE_UNKNOWN
|
||||
):
|
||||
self._is_configured = True
|
||||
self._presence_state = STATE_UNKNOWN
|
||||
|
||||
@overrides
|
||||
def start_listening(self):
|
||||
|
||||
@@ -125,15 +125,10 @@ class VersatileThermostatAPI(dict):
|
||||
):
|
||||
"""register the two number entities needed for boiler activation"""
|
||||
self._threshold_number_entity = threshold_number_entity
|
||||
# If sensor and threshold number are initialized, reload the listener
|
||||
# if self._nb_active_number_entity and self._central_boiler_entity:
|
||||
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
|
||||
|
||||
def register_nb_device_active_boiler(self, nb_active_number_entity):
|
||||
"""register the two number entities needed for boiler activation"""
|
||||
self._nb_active_number_entity = nb_active_number_entity
|
||||
# if self._threshold_number_entity and self._central_boiler_entity:
|
||||
# self._hass.async_add_job(self.reload_central_boiler_binary_listener)
|
||||
|
||||
def register_temperature_number(
|
||||
self,
|
||||
@@ -172,13 +167,6 @@ class VersatileThermostatAPI(dict):
|
||||
)
|
||||
if component:
|
||||
for entity in component.entities:
|
||||
# if hasattr(entity, "init_presets"):
|
||||
# if (
|
||||
# only_use_central is False
|
||||
# or entity.use_central_config_temperature
|
||||
# ):
|
||||
# await entity.init_presets(self.find_central_configuration())
|
||||
|
||||
# A little hack to test if the climate is a VTherm. Cannot use isinstance
|
||||
# due to circular dependency of BaseThermostat
|
||||
if (
|
||||
|
||||
@@ -599,12 +599,7 @@ async def create_thermostat(
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# We should reload the VTherm links
|
||||
# vtherm_api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api()
|
||||
# central_config = vtherm_api.find_central_configuration()
|
||||
entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
|
||||
# if entity and hasattr(entity, "init_presets")::
|
||||
# await entity.init_presets(central_config)
|
||||
|
||||
return entity
|
||||
|
||||
@@ -839,7 +834,7 @@ async def send_motion_change_event(
|
||||
),
|
||||
},
|
||||
)
|
||||
ret = await entity._async_motion_changed(motion_event)
|
||||
ret = await entity.motion_manager._motion_sensor_changed(motion_event)
|
||||
if sleep:
|
||||
await asyncio.sleep(0.1)
|
||||
return ret
|
||||
@@ -1009,7 +1004,7 @@ async def set_climate_preset_temp(
|
||||
await temp_entity.async_set_native_value(temp)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"commons tests set_cliamte_preset_temp: cannot find number entity with entity_id '%s'",
|
||||
"commons tests set_climate_preset_temp: cannot find number entity with entity_id '%s'",
|
||||
number_entity_id,
|
||||
)
|
||||
|
||||
@@ -1071,9 +1066,14 @@ async def set_all_climate_preset_temp(
|
||||
NUMBER_DOMAIN,
|
||||
)
|
||||
assert temp_entity
|
||||
if not temp_entity:
|
||||
raise ConfigurationNotCompleteError(
|
||||
f"'{number_entity_name}' don't exists as number entity"
|
||||
)
|
||||
# Because set_value is not implemented in Number class (really don't understand why...)
|
||||
assert temp_entity.state == value
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
#
|
||||
# Side effects management
|
||||
|
||||
@@ -310,6 +310,8 @@ async def test_motion_binary_sensors(
|
||||
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||
CONF_MOTION_DELAY: 0, # important to not been obliged to wait
|
||||
CONF_MOTION_PRESET: PRESET_BOOST,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -329,7 +331,7 @@ async def test_motion_binary_sensors(
|
||||
await entity.async_set_preset_mode(PRESET_COMFORT)
|
||||
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||
await send_temperature_change_event(entity, 15, now)
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
|
||||
await motion_binary_sensor.async_my_climate_changed()
|
||||
assert motion_binary_sensor.state is STATE_OFF
|
||||
|
||||
@@ -286,11 +286,14 @@ async def test_full_over_switch_wo_central_config(
|
||||
assert entity._window_auto_open_threshold == 3
|
||||
assert entity._window_auto_max_duration == 5
|
||||
|
||||
assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor"
|
||||
assert entity._motion_delay_sec == 10
|
||||
assert entity._motion_off_delay_sec == 29
|
||||
assert entity._motion_preset == "comfort"
|
||||
assert entity._no_motion_preset == "eco"
|
||||
assert (
|
||||
entity.motion_manager.motion_sensor_entity_id
|
||||
== "binary_sensor.mock_motion_sensor"
|
||||
)
|
||||
assert entity.motion_manager.motion_delay_sec == 10
|
||||
assert entity.motion_manager.motion_off_delay_sec == 29
|
||||
assert entity.motion_manager.motion_preset == "comfort"
|
||||
assert entity.motion_manager.no_motion_preset == "eco"
|
||||
|
||||
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
|
||||
assert (
|
||||
@@ -406,11 +409,14 @@ async def test_full_over_switch_with_central_config(
|
||||
assert entity._window_auto_open_threshold == 4
|
||||
assert entity._window_auto_max_duration == 31
|
||||
|
||||
assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor"
|
||||
assert entity._motion_delay_sec == 31
|
||||
assert entity._motion_off_delay_sec == 301
|
||||
assert entity._motion_preset == "boost"
|
||||
assert entity._no_motion_preset == "frost"
|
||||
assert (
|
||||
entity.motion_manager.motion_sensor_entity_id
|
||||
== "binary_sensor.mock_motion_sensor"
|
||||
)
|
||||
assert entity.motion_manager.motion_delay_sec == 31
|
||||
assert entity.motion_manager.motion_off_delay_sec == 301
|
||||
assert entity.motion_manager.motion_preset == "boost"
|
||||
assert entity.motion_manager.no_motion_preset == "frost"
|
||||
|
||||
assert entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
|
||||
assert (
|
||||
|
||||
@@ -3,20 +3,286 @@
|
||||
""" Test the Window management """
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, call, AsyncMock, MagicMock, PropertyMock
|
||||
|
||||
from custom_components.versatile_thermostat.base_thermostat import BaseThermostat
|
||||
from custom_components.versatile_thermostat.feature_motion_manager import (
|
||||
FeatureMotionManager,
|
||||
)
|
||||
|
||||
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
|
||||
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"current_state, new_state, temp, nb_call, motion_state, is_motion_detected, preset_refresh, changed",
|
||||
[
|
||||
(STATE_OFF, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
# motion is ON. So is_motion_detected is true and preset is BOOST
|
||||
(STATE_OFF, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
# current_state is ON and motion is OFF. So is_motion_detected is false and preset is ECO
|
||||
(STATE_ON, STATE_OFF, 17, 1, STATE_OFF, False, PRESET_ECO, True),
|
||||
],
|
||||
)
|
||||
async def test_motion_feature_manager_refresh(
|
||||
hass: HomeAssistant,
|
||||
current_state,
|
||||
new_state, # new state of motion event
|
||||
temp,
|
||||
nb_call,
|
||||
motion_state,
|
||||
is_motion_detected,
|
||||
preset_refresh,
|
||||
changed,
|
||||
):
|
||||
"""Test the FeatureMotionManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_ACTIVITY)
|
||||
|
||||
# 1. creation
|
||||
motion_manager = FeatureMotionManager(fake_vtherm, hass)
|
||||
|
||||
assert motion_manager is not None
|
||||
assert motion_manager.is_configured is False
|
||||
assert motion_manager.is_motion_detected is False
|
||||
assert motion_manager.motion_state == STATE_UNAVAILABLE
|
||||
assert motion_manager.name == "the name"
|
||||
|
||||
assert len(motion_manager._active_listener) == 0
|
||||
|
||||
custom_attributes = {}
|
||||
motion_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["motion_sensor_entity_id"] is None
|
||||
assert custom_attributes["motion_state"] == STATE_UNAVAILABLE
|
||||
assert custom_attributes["is_motion_configured"] is False
|
||||
assert custom_attributes["motion_preset"] is None
|
||||
assert custom_attributes["no_motion_preset"] is None
|
||||
assert custom_attributes["motion_delay_sec"] == 0
|
||||
assert custom_attributes["motion_off_delay_sec"] == 0
|
||||
|
||||
# 2. post_init
|
||||
motion_manager.post_init(
|
||||
{
|
||||
CONF_MOTION_SENSOR: "sensor.the_motion_sensor",
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: PRESET_BOOST,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
}
|
||||
)
|
||||
|
||||
assert motion_manager.is_configured is True
|
||||
assert motion_manager.motion_state == STATE_UNKNOWN
|
||||
assert motion_manager.is_motion_detected is False
|
||||
|
||||
custom_attributes = {}
|
||||
motion_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
|
||||
assert custom_attributes["motion_state"] == STATE_UNKNOWN
|
||||
assert custom_attributes["is_motion_configured"] is True
|
||||
assert custom_attributes["motion_preset"] is PRESET_BOOST
|
||||
assert custom_attributes["no_motion_preset"] is PRESET_ECO
|
||||
assert custom_attributes["motion_delay_sec"] == 10
|
||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||
|
||||
# 3. start listening
|
||||
motion_manager.start_listening()
|
||||
assert motion_manager.is_configured is True
|
||||
assert motion_manager.motion_state == STATE_UNKNOWN
|
||||
assert motion_manager.is_motion_detected is False
|
||||
|
||||
assert len(motion_manager._active_listener) == 1
|
||||
|
||||
# 4. test refresh with the parametrized
|
||||
# fmt:off
|
||||
with patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)) as mock_get_state:
|
||||
# fmt:on
|
||||
# Configurer les méthodes mockées
|
||||
fake_vtherm.find_preset_temp.return_value = temp
|
||||
fake_vtherm.change_target_temperature = AsyncMock()
|
||||
fake_vtherm.async_control_heating = AsyncMock()
|
||||
fake_vtherm.recalculate = MagicMock()
|
||||
|
||||
# force old state for the test
|
||||
motion_manager._motion_state = current_state
|
||||
|
||||
ret = await motion_manager.refresh_state()
|
||||
assert ret == changed
|
||||
assert motion_manager.is_configured is True
|
||||
# in the refresh there is no delay
|
||||
assert motion_manager.motion_state == new_state
|
||||
assert motion_manager.is_motion_detected is is_motion_detected
|
||||
|
||||
assert mock_get_state.call_count == 1
|
||||
|
||||
assert fake_vtherm.find_preset_temp.call_count == nb_call
|
||||
|
||||
if nb_call == 1:
|
||||
fake_vtherm.find_preset_temp.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(preset_refresh),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.change_target_temperature.call_count == nb_call
|
||||
fake_vtherm.change_target_temperature.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(temp),
|
||||
]
|
||||
)
|
||||
|
||||
# We do not call control_heating at startup
|
||||
assert fake_vtherm.recalculate.call_count == 0
|
||||
assert fake_vtherm.async_control_heating.call_count == 0
|
||||
|
||||
fake_vtherm.reset_mock()
|
||||
|
||||
# 5. Check custom_attributes
|
||||
custom_attributes = {}
|
||||
motion_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
|
||||
assert custom_attributes["motion_state"] == new_state
|
||||
assert custom_attributes["is_motion_configured"] is True
|
||||
assert custom_attributes["motion_preset"] is PRESET_BOOST
|
||||
assert custom_attributes["no_motion_preset"] is PRESET_ECO
|
||||
assert custom_attributes["motion_delay_sec"] == 10
|
||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||
|
||||
motion_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"current_state, long_enough, new_state, temp, nb_call, motion_state, is_motion_detected, preset_event, changed",
|
||||
[
|
||||
(STATE_OFF, True, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
# motion is ON but for not enough time but sensor is on at the end. So is_motion_detected is true and preset is BOOST
|
||||
(STATE_OFF, False, STATE_ON, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
# motion is OFF for enough time. So is_motion_detected is false and preset is ECO
|
||||
(STATE_ON, True, STATE_OFF, 17, 1, STATE_OFF, False, PRESET_ECO, True),
|
||||
# motion is OFF for not enough time. So is_motion_detected is false and preset is ECO
|
||||
(STATE_ON, False, STATE_OFF, 21, 1, STATE_ON, True, PRESET_BOOST, True),
|
||||
],
|
||||
)
|
||||
async def test_motion_feature_manager_event(
|
||||
hass: HomeAssistant,
|
||||
current_state,
|
||||
long_enough,
|
||||
new_state, # new state of motion event
|
||||
temp,
|
||||
nb_call,
|
||||
motion_state,
|
||||
is_motion_detected,
|
||||
preset_event,
|
||||
changed,
|
||||
):
|
||||
"""Test the FeatureMotionManager class direclty"""
|
||||
|
||||
fake_vtherm = MagicMock(spec=BaseThermostat)
|
||||
type(fake_vtherm).name = PropertyMock(return_value="the name")
|
||||
type(fake_vtherm).preset_mode = PropertyMock(return_value=PRESET_ACTIVITY)
|
||||
|
||||
# 1. iniitialization creation, post_init, start_listening
|
||||
motion_manager = FeatureMotionManager(fake_vtherm, hass)
|
||||
motion_manager.post_init(
|
||||
{
|
||||
CONF_MOTION_SENSOR: "sensor.the_motion_sensor",
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_MOTION_DELAY: 10,
|
||||
CONF_MOTION_OFF_DELAY: 30,
|
||||
CONF_MOTION_PRESET: PRESET_BOOST,
|
||||
CONF_NO_MOTION_PRESET: PRESET_ECO,
|
||||
}
|
||||
)
|
||||
motion_manager.start_listening()
|
||||
|
||||
# 2. test _motion_sensor_changed with the parametrized
|
||||
# fmt: off
|
||||
with patch("homeassistant.helpers.condition.state", return_value=long_enough), \
|
||||
patch("homeassistant.core.StateMachine.get", return_value=State("sensor.the_motion_sensor", new_state)):
|
||||
# fmt: on
|
||||
fake_vtherm.find_preset_temp.return_value = temp
|
||||
fake_vtherm.change_target_temperature = AsyncMock()
|
||||
fake_vtherm.async_control_heating = AsyncMock()
|
||||
fake_vtherm.recalculate = MagicMock()
|
||||
|
||||
# force old state for the test
|
||||
motion_manager._motion_state = current_state
|
||||
|
||||
delay = await motion_manager._motion_sensor_changed(
|
||||
event=Event(
|
||||
event_type=EVENT_STATE_CHANGED,
|
||||
data={
|
||||
"entity_id": "sensor.the_motion_sensor",
|
||||
"new_state": State("sensor.the_motion_sensor", new_state),
|
||||
"old_state": State("sensor.the_motion_sensor", STATE_UNAVAILABLE),
|
||||
}))
|
||||
assert delay is not None
|
||||
|
||||
await delay(None)
|
||||
assert motion_manager.is_configured is True
|
||||
assert motion_manager.motion_state == motion_state
|
||||
assert motion_manager.is_motion_detected is is_motion_detected
|
||||
|
||||
assert fake_vtherm.find_preset_temp.call_count == nb_call
|
||||
|
||||
if nb_call == 1:
|
||||
fake_vtherm.find_preset_temp.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(preset_event),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.change_target_temperature.call_count == nb_call
|
||||
fake_vtherm.change_target_temperature.assert_has_calls(
|
||||
[
|
||||
call.find_preset_temp(temp),
|
||||
]
|
||||
)
|
||||
|
||||
assert fake_vtherm.recalculate.call_count == 1
|
||||
assert fake_vtherm.async_control_heating.call_count == 1
|
||||
fake_vtherm.async_control_heating.assert_has_calls([
|
||||
call.async_control_heating(force=True)
|
||||
])
|
||||
|
||||
fake_vtherm.reset_mock()
|
||||
|
||||
# 3. Check custom_attributes
|
||||
custom_attributes = {}
|
||||
motion_manager.add_custom_attributes(custom_attributes)
|
||||
assert custom_attributes["motion_sensor_entity_id"] == "sensor.the_motion_sensor"
|
||||
assert custom_attributes["motion_state"] == motion_state
|
||||
assert custom_attributes["is_motion_configured"] is True
|
||||
assert custom_attributes["motion_preset"] is PRESET_BOOST
|
||||
assert custom_attributes["no_motion_preset"] is PRESET_ECO
|
||||
assert custom_attributes["motion_delay_sec"] == 10
|
||||
assert custom_attributes["motion_off_delay_sec"] == 30
|
||||
|
||||
motion_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_not_enough(
|
||||
async def test_motion_management_time_not_enough(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
temps = {
|
||||
"frost": 10,
|
||||
"eco": 17,
|
||||
"comfort": 18,
|
||||
"boost": 19,
|
||||
"frost_away": 10,
|
||||
"eco_away": 17,
|
||||
"comfort_away": 18,
|
||||
"boost_away": 19,
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -30,17 +296,11 @@ async def test_movement_management_time_not_enough(
|
||||
CONF_CYCLE_MIN: 5,
|
||||
CONF_TEMP_MIN: 15,
|
||||
CONF_TEMP_MAX: 30,
|
||||
"eco_temp": 17,
|
||||
"comfort_temp": 18,
|
||||
"boost_temp": 19,
|
||||
"eco_away_temp": 17,
|
||||
"comfort_away_temp": 18,
|
||||
"boost_away_temp": 19,
|
||||
CONF_USE_WINDOW_FEATURE: False,
|
||||
CONF_USE_MOTION_FEATURE: True,
|
||||
CONF_USE_POWER_FEATURE: False,
|
||||
CONF_USE_PRESENCE_FEATURE: True,
|
||||
CONF_HEATER: "switch.mock_switch",
|
||||
CONF_UNDERLYING_LIST: ["switch.mock_switch"],
|
||||
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||
CONF_TPI_COEF_INT: 0.3,
|
||||
CONF_TPI_COEF_EXT: 0.01,
|
||||
@@ -60,11 +320,12 @@ async def test_movement_management_time_not_enough(
|
||||
hass, entry, "climate.theoverswitchmockname"
|
||||
)
|
||||
assert entity
|
||||
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
|
||||
|
||||
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||
now: datetime = datetime.now(tz=tz)
|
||||
|
||||
# start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
|
||||
# 1. start heating, in boost mode, when someone is present. We block the control_heating to avoid running a cycle
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating"
|
||||
):
|
||||
@@ -75,7 +336,7 @@ async def test_movement_management_time_not_enough(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=5)
|
||||
@@ -83,9 +344,9 @@ async def test_movement_management_time_not_enough(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, True, False, event_timestamp)
|
||||
assert entity.presence_state == "on"
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
# starts detecting motion with time not enough
|
||||
# 2. starts detecting motion with time not enough
|
||||
with patch(
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event, patch(
|
||||
@@ -104,7 +365,9 @@ async def test_movement_management_time_not_enough(
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
# Will return False -> we will stay on movement False
|
||||
await try_condition(None)
|
||||
@@ -137,7 +400,9 @@ async def test_movement_management_time_not_enough(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition:
|
||||
event_timestamp = now - timedelta(minutes=3)
|
||||
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, True, False, event_timestamp
|
||||
)
|
||||
|
||||
# Will return True -> we will switch to movement On
|
||||
await try_condition(None)
|
||||
@@ -168,7 +433,9 @@ async def test_movement_management_time_not_enough(
|
||||
),
|
||||
):
|
||||
event_timestamp = now - timedelta(minutes=2)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
|
||||
# Will return False -> we will stay to movement On
|
||||
await try_condition(None)
|
||||
@@ -200,7 +467,9 @@ async def test_movement_management_time_not_enough(
|
||||
"homeassistant.helpers.condition.state", return_value=True
|
||||
) as mock_condition:
|
||||
event_timestamp = now - timedelta(minutes=1)
|
||||
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||
try_condition = await send_motion_change_event(
|
||||
entity, False, True, event_timestamp
|
||||
)
|
||||
|
||||
# Will return True -> we will switch to movement Off
|
||||
await try_condition(None)
|
||||
@@ -221,7 +490,7 @@ async def test_movement_management_time_not_enough(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_enough_and_presence(
|
||||
async def test_motion_management_time_enough_and_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Motion management when time is not enough"""
|
||||
@@ -282,7 +551,7 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
@@ -312,7 +581,7 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 19
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.motion_state == STATE_ON
|
||||
assert entity.presence_state == STATE_ON
|
||||
assert mock_send_event.call_count == 0
|
||||
# Change is confirmed. Heater should be started
|
||||
@@ -340,7 +609,7 @@ async def test_movement_management_time_enough_and_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.motion_state == STATE_OFF
|
||||
assert entity.presence_state == STATE_ON
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
@@ -352,7 +621,7 @@ async def test_movement_management_time_enough_and_presence(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_time_enoughand_not_presence(
|
||||
async def test_motion_management_time_enough_and_not_presence(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Presence management when time is not enough"""
|
||||
@@ -413,7 +682,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet and presence is unknown
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
@@ -421,7 +690,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state == "off"
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -443,7 +712,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost away mode
|
||||
assert entity.target_temperature == 19.1
|
||||
assert entity.motion_state == "on"
|
||||
assert entity.motion_state == STATE_ON
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
assert mock_send_event.call_count == 0
|
||||
@@ -472,7 +741,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18.1
|
||||
assert entity.motion_state == "off"
|
||||
assert entity.motion_state == STATE_OFF
|
||||
assert entity.presence_state == STATE_OFF
|
||||
assert mock_send_event.call_count == 0
|
||||
# 18.1 starts heating with a low on_percent
|
||||
@@ -484,7 +753,7 @@ async def test_movement_management_time_enoughand_not_presence(
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_with_stop_during_condition(
|
||||
async def test_motion_management_with_stop_during_condition(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
|
||||
@@ -546,7 +815,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
@@ -554,7 +823,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||
|
||||
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||
assert entity.presence_state == "off"
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
# starts detecting motion
|
||||
with patch(
|
||||
@@ -580,7 +849,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because motion is detected yet -> switch to Boost mode
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state == STATE_OFF
|
||||
# Send a stop detection
|
||||
event_timestamp = now - timedelta(minutes=4)
|
||||
@@ -592,7 +861,7 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.hvac_mode is HVACMode.HEAT
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
# Resend a start detection
|
||||
@@ -608,19 +877,19 @@ async def test_movement_management_with_stop_during_condition(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# still no motion detected
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state == STATE_OFF
|
||||
|
||||
await try_condition1(None)
|
||||
# We should have switch this time
|
||||
assert entity.target_temperature == 19 # Boost
|
||||
assert entity.motion_state == "on" # switch to movement on
|
||||
assert entity.motion_state == STATE_ON # switch to movement on
|
||||
assert entity.presence_state == STATE_OFF # Non change
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||
async def test_movement_management_with_stop_during_condition_last_state_on(
|
||||
async def test_motion_management_with_stop_during_condition_last_state_on(
|
||||
hass: HomeAssistant, skip_hass_states_is_state
|
||||
):
|
||||
"""Test the Motion management when the movement sensor switch to off and then to on during the test condition"""
|
||||
@@ -681,7 +950,7 @@ async def test_movement_management_with_stop_during_condition_last_state_on(
|
||||
assert entity.preset_mode is PRESET_ACTIVITY
|
||||
# because no motion is detected yet
|
||||
assert entity.target_temperature == 18
|
||||
assert entity.motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
|
||||
event_timestamp = now - timedelta(minutes=6)
|
||||
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||
@@ -814,7 +814,7 @@ async def test_multiple_switch_power_management(
|
||||
"type": "start",
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_power_max": 74,
|
||||
"current_max_power": 74,
|
||||
"current_power_consumption": 25.0,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -113,7 +113,7 @@ async def test_over_climate_valve_mono(hass: HomeAssistant, skip_hass_states_get
|
||||
assert vtherm.preset_mode is PRESET_NONE
|
||||
assert vtherm._security_state is False
|
||||
assert vtherm._window_state is None
|
||||
assert vtherm._motion_state is None
|
||||
assert vtherm.motion_state is STATE_UNAVAILABLE
|
||||
assert vtherm.presence_state is STATE_UNAVAILABLE
|
||||
|
||||
assert vtherm.is_device_active is False
|
||||
|
||||
@@ -77,7 +77,7 @@ async def test_power_feature_manager(
|
||||
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
|
||||
assert custom_attributes["current_max_power"] is None
|
||||
|
||||
# 2. post_init
|
||||
power_manager.post_init(
|
||||
@@ -104,7 +104,7 @@ async def test_power_feature_manager(
|
||||
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
|
||||
assert custom_attributes["current_max_power"] is None
|
||||
|
||||
# 3. start listening
|
||||
power_manager.start_listening()
|
||||
@@ -183,7 +183,7 @@ async def test_power_feature_manager(
|
||||
[
|
||||
call.fake_vtherm.send_event(
|
||||
EventType.POWER_EVENT,
|
||||
{'type': 'end', 'current_power': power, 'device_power': 1234, 'current_power_max': max_power}),
|
||||
{'type': 'end', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power}),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -214,7 +214,7 @@ async def test_power_feature_manager(
|
||||
[
|
||||
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}),
|
||||
{'type': 'start', 'current_power': power, 'device_power': 1234, 'current_max_power': max_power, 'current_power_consumption': 1234.0}),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -232,7 +232,10 @@ async def test_power_feature_manager(
|
||||
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
|
||||
assert custom_attributes["current_max_power"] == max_power
|
||||
|
||||
power_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||
@@ -410,7 +413,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
"type": "start",
|
||||
"current_power": 50,
|
||||
"device_power": 100,
|
||||
"current_power_max": 149,
|
||||
"current_max_power": 149,
|
||||
"current_power_consumption": 100.0,
|
||||
},
|
||||
),
|
||||
@@ -445,7 +448,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
|
||||
"type": "end",
|
||||
"current_power": 48,
|
||||
"device_power": 100,
|
||||
"current_power_max": 149,
|
||||
"current_max_power": 149,
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -173,3 +173,6 @@ async def test_presence_feature_manager(
|
||||
assert custom_attributes["presence_sensor_entity_id"] == "sensor.the_presence_sensor"
|
||||
assert custom_attributes["presence_state"] == presence_state
|
||||
assert custom_attributes["is_presence_configured"] is True
|
||||
|
||||
presence_manager.stop_listening()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -55,7 +55,7 @@ async def test_over_switch_full_start(hass: HomeAssistant, skip_hass_states_is_s
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity._prop_algorithm is not None
|
||||
assert entity.have_valve_regulation is False
|
||||
@@ -114,7 +114,7 @@ async def test_over_climate_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity.motion_state is STATE_UNAVAILABLE
|
||||
assert entity.presence_state is STATE_UNAVAILABLE
|
||||
assert entity.have_valve_regulation is False
|
||||
|
||||
@@ -151,18 +151,6 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
|
||||
) as mock_send_event:
|
||||
entity = await create_thermostat(hass, entry, "climate.theover4switchmockname")
|
||||
# entry.add_to_hass(hass)
|
||||
# await hass.config_entries.async_setup(entry.entry_id)
|
||||
# assert entry.state is ConfigEntryState.LOADED
|
||||
#
|
||||
# def find_my_entity(entity_id) -> ClimateEntity:
|
||||
# """Find my new entity"""
|
||||
# component: EntityComponent[ClimateEntity] = hass.data[CLIMATE_DOMAIN]
|
||||
# for entity in component.entities:
|
||||
# if entity.entity_id == entity_id:
|
||||
# return entity
|
||||
#
|
||||
# entity: BaseThermostat = find_my_entity("climate.theover4switchmockname")
|
||||
|
||||
assert entity
|
||||
|
||||
@@ -182,7 +170,7 @@ async def test_over_4switch_full_start(hass: HomeAssistant, skip_hass_states_is_
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False
|
||||
assert entity._window_state is None
|
||||
assert entity._motion_state is None
|
||||
assert entity.motion_state is STATE_UNKNOWN
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity._prop_algorithm is not None
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ async def test_over_switch_ac_full_start(
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False # pylint: disable=protected-access
|
||||
assert entity._window_state is None # pylint: disable=protected-access
|
||||
assert entity._motion_state is None # pylint: disable=protected-access
|
||||
assert entity.motion_state is STATE_UNKNOWN # pylint: disable=protected-access
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity._prop_algorithm is not None # pylint: disable=protected-access
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ async def test_over_valve_full_start(
|
||||
assert entity.preset_mode is PRESET_NONE
|
||||
assert entity._security_state is False # pylint: disable=protected-access
|
||||
assert entity._window_state is None # pylint: disable=protected-access
|
||||
assert entity._motion_state is None # pylint: disable=protected-access
|
||||
assert entity.motion_state is STATE_UNKNOWN # pylint: disable=protected-access
|
||||
assert entity.presence_state is STATE_UNKNOWN
|
||||
assert entity._prop_algorithm is not None # pylint: disable=protected-access
|
||||
assert entity.have_valve_regulation is False
|
||||
|
||||
Reference in New Issue
Block a user