Add Motion manager. All tests ok

This commit is contained in:
Jean-Marc Collin
2024-12-26 15:42:29 +00:00
parent 887d59a08f
commit a11eaef9f7
16 changed files with 743 additions and 338 deletions

View File

@@ -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

View 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}"

View File

@@ -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,
},
)

View File

@@ -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):

View File

@@ -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 (

View File

@@ -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

View File

@@ -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

View File

@@ -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 (

View File

@@ -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)

View File

@@ -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,
},
),

View File

@@ -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

View File

@@ -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,
},
),
],

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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