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 .base_manager import BaseFeatureManager
from .feature_presence_manager import FeaturePresenceManager from .feature_presence_manager import FeaturePresenceManager
from .feature_power_manager import FeaturePowerManager from .feature_power_manager import FeaturePowerManager
from .feature_motion_manager import FeatureMotionManager
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -102,7 +103,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"comfort_away_temp", "comfort_away_temp",
"power_temp", "power_temp",
"ac_mode", "ac_mode",
"current_power_max", "current_max_power",
"saved_preset_mode", "saved_preset_mode",
"saved_target_temp", "saved_target_temp",
"saved_hvac_mode", "saved_hvac_mode",
@@ -112,8 +113,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"last_temperature_datetime", "last_temperature_datetime",
"last_ext_temperature_datetime", "last_ext_temperature_datetime",
"minimal_activation_delay_sec", "minimal_activation_delay_sec",
"device_power",
"mean_cycle_power",
"last_update_datetime", "last_update_datetime",
"timezone", "timezone",
"window_sensor_entity_id", "window_sensor_entity_id",
@@ -123,12 +122,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"window_auto_close_threshold", "window_auto_close_threshold",
"window_auto_max_duration", "window_auto_max_duration",
"window_action", "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", "temperature_unit",
"is_device_active", "is_device_active",
"device_actives", "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__( def __init__(
@@ -173,10 +169,10 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._humidity = None self._humidity = None
self._swing_mode = None self._swing_mode = None
self._window_state = None self._window_state = None
self._motion_state = None
self._saved_hvac_mode = None self._saved_hvac_mode = None
self._window_call_cancel = None self._window_call_cancel = None
self._motion_call_cancel = None
self._cur_temp = None self._cur_temp = None
self._ac_mode = None self._ac_mode = None
self._temp_sensor_entity_id = None self._temp_sensor_entity_id = None
@@ -249,9 +245,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self, hass self, hass
) )
self._power_manager: FeaturePowerManager = FeaturePowerManager(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._presence_manager)
self.register_manager(self._power_manager) self.register_manager(self._power_manager)
self.register_manager(self._motion_manager)
self.post_init(entry_infos) self.post_init(entry_infos)
@@ -343,9 +341,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if self._window_call_cancel is not None: if self._window_call_cancel is not None:
self._window_call_cancel() self._window_call_cancel()
self._window_call_cancel = None 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) 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, 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_int = entry_infos.get(CONF_TPI_COEF_INT)
self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT) self._tpi_coef_ext = entry_infos.get(CONF_TPI_COEF_EXT)
@@ -461,7 +442,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
del self._prop_algorithm del self._prop_algorithm
# Memory synthesis state # Memory synthesis state
self._motion_state = None
self._window_state = None self._window_state = None
self._total_energy = None self._total_energy = None
@@ -542,14 +522,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._async_windows_changed, 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 # start listening for all managers
for manager in self._managers: for manager in self._managers:
@@ -647,23 +619,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
) )
need_write_state = True 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 # refresh states for all managers
for manager in self._managers: for manager in self._managers:
if await manager.refresh_state(): if await manager.refresh_state():
@@ -975,6 +930,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"""Get the presence manager""" """Get the presence manager"""
return self._presence_manager return self._presence_manager
@property
def motion_manager(self) -> FeatureMotionManager | None:
"""Get the motion manager"""
return self._motion_manager
@property @property
def window_state(self) -> str | None: def window_state(self) -> str | None:
"""Get the window_state""" """Get the window_state"""
@@ -1003,7 +963,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
@property @property
def motion_state(self) -> str | None: def motion_state(self) -> str | None:
"""Get the motion_state""" """Get the motion_state"""
return self._motion_state return self._motion_manager.motion_state
@property @property
def presence_state(self) -> str | None: def presence_state(self) -> str | None:
@@ -1248,7 +1208,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
self._saved_preset_mode = preset_mode self._saved_preset_mode = preset_mode
return return
old_preset_mode = self._attr_preset_mode # Remove this old_preset_mode = self._attr_preset_mode
recalculate = True recalculate = True
if preset_mode == PRESET_NONE: if preset_mode == PRESET_NONE:
self._attr_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) await self.change_target_temperature(self._saved_target_temp)
elif preset_mode == PRESET_ACTIVITY: elif preset_mode == PRESET_ACTIVITY:
self._attr_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: else:
if self._attr_preset_mode == PRESET_NONE: if self._attr_preset_mode == PRESET_NONE:
self._saved_target_temp = self._target_temp self._saved_target_temp = self._target_temp
@@ -1316,18 +1276,9 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
if preset_mode == PRESET_POWER: if preset_mode == PRESET_POWER:
return self._power_manager.power_temperature return self._power_manager.power_temperature
if preset_mode == PRESET_ACTIVITY: if preset_mode == PRESET_ACTIVITY:
motion_preset = self._motion_manager.get_current_motion_preset()
if self._ac_mode and self._hvac_mode == HVACMode.COOL: if self._ac_mode and self._hvac_mode == HVACMode.COOL:
motion_preset = ( motion_preset = motion_preset + PRESET_AC_SUFFIX
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
)
if motion_preset in self._presets: if motion_preset in self._presets:
if self._presence_manager.is_absence_detected: 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 # For testing purpose we need to access the inner function
return try_window_condition 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 @callback
async def _check_initial_state(self): async def _check_initial_state(self):
"""Prevent the device from keep running if HVAC_MODE_OFF.""" """Prevent the device from keep running if HVAC_MODE_OFF."""
@@ -1771,41 +1589,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
except ValueError as ex: except ValueError as ex:
_LOGGER.error("Unable to update external temperature from sensor: %s", ex) _LOGGER.error("Unable to update external temperature from sensor: %s", ex)
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): async def async_underlying_entity_turn_off(self):
"""Turn heater toggleable device off. Used by Window, overpowering, control_heating to turn all off""" """Turn heater toggleable device off. Used by Window, overpowering, control_heating to turn all off"""
@@ -2377,8 +2160,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
"saved_preset_mode": self._saved_preset_mode, "saved_preset_mode": self._saved_preset_mode,
"saved_target_temp": self._saved_target_temp, "saved_target_temp": self._saved_target_temp,
"saved_hvac_mode": self._saved_hvac_mode, "saved_hvac_mode": self._saved_hvac_mode,
"motion_sensor_entity_id": self._motion_sensor_entity_id,
"motion_state": self._motion_state,
"window_state": self.window_state, "window_state": self.window_state,
"window_auto_state": self.window_auto_state, "window_auto_state": self.window_auto_state,
"window_bypass_state": self._window_bypass_state, "window_bypass_state": self._window_bypass_state,
@@ -2400,7 +2181,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
).isoformat(), ).isoformat(),
"security_state": self._security_state, "security_state": self._security_state,
"minimal_activation_delay_sec": self._minimal_activation_delay, "minimal_activation_delay_sec": self._minimal_activation_delay,
ATTR_MEAN_POWER_CYCLE: self._power_manager.mean_cycle_power,
ATTR_TOTAL_ENERGY: self.total_energy, ATTR_TOTAL_ENERGY: self.total_energy,
"last_update_datetime": self.now.isoformat(), "last_update_datetime": self.now.isoformat(),
"timezone": str(self._current_tz), "timezone": str(self._current_tz),
@@ -2633,7 +2413,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]):
else: else:
_LOGGER.debug("No preset_modes") _LOGGER.debug("No preset_modes")
if self._motion_on: if self._motion_manager.is_configured:
self._attr_preset_modes.append(PRESET_ACTIVITY) self._attr_preset_modes.append(PRESET_ACTIVITY)
# Re-applicate the last preset if any to take change into account # 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): class FeaturePowerManager(BaseFeatureManager):
"""The implementation of the Power feature""" """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): def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager""" """Init of a featureManager"""
super().__init__(vtherm, hass) super().__init__(vtherm, hass)
@@ -55,7 +67,6 @@ class FeaturePowerManager(BaseFeatureManager):
self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR) self._power_sensor_entity_id = entry_infos.get(CONF_POWER_SENSOR)
self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR) self._max_power_sensor_entity_id = entry_infos.get(CONF_MAX_POWER_SENSOR)
self._power_temp = entry_infos.get(CONF_PRESET_POWER) 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._device_power = entry_infos.get(CONF_DEVICE_POWER) or 0
self._is_configured = False self._is_configured = False
@@ -68,6 +79,7 @@ class FeaturePowerManager(BaseFeatureManager):
and self._device_power and self._device_power
): ):
self._is_configured = True self._is_configured = True
self._overpowering_state = STATE_UNKNOWN
else: else:
_LOGGER.info("%s - Power management is not fully configured", self) _LOGGER.info("%s - Power management is not fully configured", self)
@@ -197,7 +209,8 @@ class FeaturePowerManager(BaseFeatureManager):
"device_power": self._device_power, "device_power": self._device_power,
"power_temp": self._power_temp, "power_temp": self._power_temp,
"current_power": self._current_power, "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", "type": "start",
"current_power": self._current_power, "current_power": self._current_power,
"device_power": self._device_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, "current_power_consumption": power_consumption_max,
}, },
) )
@@ -286,7 +299,7 @@ class FeaturePowerManager(BaseFeatureManager):
"type": "end", "type": "end",
"current_power": self._current_power, "current_power": self._current_power,
"device_power": self._device_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): class FeaturePresenceManager(BaseFeatureManager):
"""The implementation of the Presence feature""" """The implementation of the Presence feature"""
unrecorded_attributes = frozenset(
{
"presence_sensor_entity_id",
"is_presence_configured",
}
)
def __init__(self, vtherm: Any, hass: HomeAssistant): def __init__(self, vtherm: Any, hass: HomeAssistant):
"""Init of a featureManager""" """Init of a featureManager"""
super().__init__(vtherm, hass) super().__init__(vtherm, hass)
@@ -52,10 +59,11 @@ class FeaturePresenceManager(BaseFeatureManager):
def post_init(self, entry_infos: ConfigData): def post_init(self, entry_infos: ConfigData):
"""Reinit of the manager""" """Reinit of the manager"""
self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR) self._presence_sensor_entity_id = entry_infos.get(CONF_PRESENCE_SENSOR)
self._is_configured = ( if (
entry_infos.get(CONF_USE_PRESENCE_FEATURE, False) entry_infos.get(CONF_USE_PRESENCE_FEATURE, False)
and self._presence_sensor_entity_id is not None and self._presence_sensor_entity_id is not None
) ):
self._is_configured = True
self._presence_state = STATE_UNKNOWN self._presence_state = STATE_UNKNOWN
@overrides @overrides

View File

@@ -125,15 +125,10 @@ class VersatileThermostatAPI(dict):
): ):
"""register the two number entities needed for boiler activation""" """register the two number entities needed for boiler activation"""
self._threshold_number_entity = threshold_number_entity 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): def register_nb_device_active_boiler(self, nb_active_number_entity):
"""register the two number entities needed for boiler activation""" """register the two number entities needed for boiler activation"""
self._nb_active_number_entity = nb_active_number_entity 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( def register_temperature_number(
self, self,
@@ -172,13 +167,6 @@ class VersatileThermostatAPI(dict):
) )
if component: if component:
for entity in component.entities: 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 # A little hack to test if the climate is a VTherm. Cannot use isinstance
# due to circular dependency of BaseThermostat # due to circular dependency of BaseThermostat
if ( if (

View File

@@ -599,12 +599,7 @@ async def create_thermostat(
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED 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) entity = search_entity(hass, entity_id, CLIMATE_DOMAIN)
# if entity and hasattr(entity, "init_presets")::
# await entity.init_presets(central_config)
return entity 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: if sleep:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
return ret return ret
@@ -1009,7 +1004,7 @@ async def set_climate_preset_temp(
await temp_entity.async_set_native_value(temp) await temp_entity.async_set_native_value(temp)
else: else:
_LOGGER.warning( _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, number_entity_id,
) )
@@ -1071,9 +1066,14 @@ async def set_all_climate_preset_temp(
NUMBER_DOMAIN, NUMBER_DOMAIN,
) )
assert temp_entity 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...) # Because set_value is not implemented in Number class (really don't understand why...)
assert temp_entity.state == value assert temp_entity.state == value
await hass.async_block_till_done()
# #
# Side effects management # Side effects management

View File

@@ -310,6 +310,8 @@ async def test_motion_binary_sensors(
CONF_SECURITY_MIN_ON_PERCENT: 0.3, CONF_SECURITY_MIN_ON_PERCENT: 0.3,
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor", CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
CONF_MOTION_DELAY: 0, # important to not been obliged to wait 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_preset_mode(PRESET_COMFORT)
await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_hvac_mode(HVACMode.HEAT)
await send_temperature_change_event(entity, 15, now) await send_temperature_change_event(entity, 15, now)
assert entity.motion_state is None assert entity.motion_state is STATE_UNKNOWN
await motion_binary_sensor.async_my_climate_changed() await motion_binary_sensor.async_my_climate_changed()
assert motion_binary_sensor.state is STATE_OFF 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_open_threshold == 3
assert entity._window_auto_max_duration == 5 assert entity._window_auto_max_duration == 5
assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor" assert (
assert entity._motion_delay_sec == 10 entity.motion_manager.motion_sensor_entity_id
assert entity._motion_off_delay_sec == 29 == "binary_sensor.mock_motion_sensor"
assert entity._motion_preset == "comfort" )
assert entity._no_motion_preset == "eco" 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 entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
assert ( 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_open_threshold == 4
assert entity._window_auto_max_duration == 31 assert entity._window_auto_max_duration == 31
assert entity._motion_sensor_entity_id == "binary_sensor.mock_motion_sensor" assert (
assert entity._motion_delay_sec == 31 entity.motion_manager.motion_sensor_entity_id
assert entity._motion_off_delay_sec == 301 == "binary_sensor.mock_motion_sensor"
assert entity._motion_preset == "boost" )
assert entity._no_motion_preset == "frost" 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 entity.power_manager.power_sensor_entity_id == "sensor.mock_power_sensor"
assert ( assert (

View File

@@ -3,20 +3,286 @@
""" Test the Window management """ """ Test the Window management """
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging 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.base_thermostat import BaseThermostat
from custom_components.versatile_thermostat.feature_motion_manager import (
FeatureMotionManager,
)
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
@pytest.mark.parametrize(
"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_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [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 hass: HomeAssistant, skip_hass_states_is_state
): ):
"""Test the Presence management when time is not enough""" """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( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@@ -30,17 +296,11 @@ async def test_movement_management_time_not_enough(
CONF_CYCLE_MIN: 5, CONF_CYCLE_MIN: 5,
CONF_TEMP_MIN: 15, CONF_TEMP_MIN: 15,
CONF_TEMP_MAX: 30, 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_WINDOW_FEATURE: False,
CONF_USE_MOTION_FEATURE: True, CONF_USE_MOTION_FEATURE: True,
CONF_USE_POWER_FEATURE: False, CONF_USE_POWER_FEATURE: False,
CONF_USE_PRESENCE_FEATURE: True, CONF_USE_PRESENCE_FEATURE: True,
CONF_HEATER: "switch.mock_switch", CONF_UNDERLYING_LIST: ["switch.mock_switch"],
CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI, CONF_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
CONF_TPI_COEF_INT: 0.3, CONF_TPI_COEF_INT: 0.3,
CONF_TPI_COEF_EXT: 0.01, CONF_TPI_COEF_EXT: 0.01,
@@ -60,11 +320,12 @@ async def test_movement_management_time_not_enough(
hass, entry, "climate.theoverswitchmockname" hass, entry, "climate.theoverswitchmockname"
) )
assert entity assert entity
await set_all_climate_preset_temp(hass, entity, temps, "theoverswitchmockname")
tz = get_tz(hass) # pylint: disable=invalid-name tz = get_tz(hass) # pylint: disable=invalid-name
now: datetime = datetime.now(tz=tz) 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( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.async_control_heating" "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 assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18 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 assert entity.presence_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=5) 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_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, True, False, 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( with patch(
"custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event, patch( ) as mock_send_event, patch(
@@ -104,7 +365,9 @@ async def test_movement_management_time_not_enough(
), ),
): ):
event_timestamp = now - timedelta(minutes=4) 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 # Will return False -> we will stay on movement False
await try_condition(None) await try_condition(None)
@@ -137,7 +400,9 @@ async def test_movement_management_time_not_enough(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition: ) as mock_condition:
event_timestamp = now - timedelta(minutes=3) 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 # Will return True -> we will switch to movement On
await try_condition(None) await try_condition(None)
@@ -168,7 +433,9 @@ async def test_movement_management_time_not_enough(
), ),
): ):
event_timestamp = now - timedelta(minutes=2) 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 # Will return False -> we will stay to movement On
await try_condition(None) await try_condition(None)
@@ -200,7 +467,9 @@ async def test_movement_management_time_not_enough(
"homeassistant.helpers.condition.state", return_value=True "homeassistant.helpers.condition.state", return_value=True
) as mock_condition: ) as mock_condition:
event_timestamp = now - timedelta(minutes=1) 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 # Will return True -> we will switch to movement Off
await try_condition(None) 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_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [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 hass: HomeAssistant, skip_hass_states_is_state
): ):
"""Test the Motion management when time is not enough""" """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 assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18 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 assert entity.presence_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=4) 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 assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode # because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 19 assert entity.target_temperature == 19
assert entity.motion_state == "on" assert entity.motion_state == STATE_ON
assert entity.presence_state == STATE_ON assert entity.presence_state == STATE_ON
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# Change is confirmed. Heater should be started # 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 assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state == "off" assert entity.motion_state == STATE_OFF
assert entity.presence_state == STATE_ON assert entity.presence_state == STATE_ON
assert mock_send_event.call_count == 0 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_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [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 hass: HomeAssistant, skip_hass_states_is_state
): ):
"""Test the Presence management when time is not enough""" """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 assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet and presence is unknown # because no motion is detected yet and presence is unknown
assert entity.target_temperature == 18 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 assert entity.presence_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=4) 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_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, 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 # starts detecting motion
with patch( with patch(
@@ -443,7 +712,7 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost away mode # because motion is detected yet -> switch to Boost away mode
assert entity.target_temperature == 19.1 assert entity.target_temperature == 19.1
assert entity.motion_state == "on" assert entity.motion_state == STATE_ON
assert entity.presence_state == STATE_OFF assert entity.presence_state == STATE_OFF
assert mock_send_event.call_count == 0 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 assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18.1 assert entity.target_temperature == 18.1
assert entity.motion_state == "off" assert entity.motion_state == STATE_OFF
assert entity.presence_state == STATE_OFF assert entity.presence_state == STATE_OFF
assert mock_send_event.call_count == 0 assert mock_send_event.call_count == 0
# 18.1 starts heating with a low on_percent # 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_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [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 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""" """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 assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18 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 assert entity.presence_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=6) 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_ext_temperature_change_event(entity, 10, event_timestamp)
await send_presence_change_event(entity, False, True, 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 # starts detecting motion
with patch( with patch(
@@ -580,7 +849,7 @@ async def test_movement_management_with_stop_during_condition(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# because motion is detected yet -> switch to Boost mode # because motion is detected yet -> switch to Boost mode
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is None assert entity.motion_state is STATE_UNKNOWN
assert entity.presence_state == STATE_OFF assert entity.presence_state == STATE_OFF
# Send a stop detection # Send a stop detection
event_timestamp = now - timedelta(minutes=4) 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.hvac_mode is HVACMode.HEAT
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is None assert entity.motion_state is STATE_UNKNOWN
assert entity.presence_state == STATE_OFF assert entity.presence_state == STATE_OFF
# Resend a start detection # Resend a start detection
@@ -608,19 +877,19 @@ async def test_movement_management_with_stop_during_condition(
assert entity.preset_mode is PRESET_ACTIVITY assert entity.preset_mode is PRESET_ACTIVITY
# still no motion detected # still no motion detected
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is None assert entity.motion_state is STATE_UNKNOWN
assert entity.presence_state == STATE_OFF assert entity.presence_state == STATE_OFF
await try_condition1(None) await try_condition1(None)
# We should have switch this time # We should have switch this time
assert entity.target_temperature == 19 # Boost 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 assert entity.presence_state == STATE_OFF # Non change
@pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_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 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""" """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 assert entity.preset_mode is PRESET_ACTIVITY
# because no motion is detected yet # because no motion is detected yet
assert entity.target_temperature == 18 assert entity.target_temperature == 18
assert entity.motion_state is None assert entity.motion_state is STATE_UNKNOWN
event_timestamp = now - timedelta(minutes=6) event_timestamp = now - timedelta(minutes=6)
await send_temperature_change_event(entity, 18, event_timestamp) await send_temperature_change_event(entity, 18, event_timestamp)

View File

@@ -814,7 +814,7 @@ async def test_multiple_switch_power_management(
"type": "start", "type": "start",
"current_power": 50, "current_power": 50,
"device_power": 100, "device_power": 100,
"current_power_max": 74, "current_max_power": 74,
"current_power_consumption": 25.0, "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.preset_mode is PRESET_NONE
assert vtherm._security_state is False assert vtherm._security_state is False
assert vtherm._window_state is None 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.presence_state is STATE_UNAVAILABLE
assert vtherm.is_device_active is False 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["device_power"] is 0
assert custom_attributes["power_temp"] is None assert custom_attributes["power_temp"] is None
assert custom_attributes["current_power"] 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 # 2. post_init
power_manager.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["device_power"] == 1234
assert custom_attributes["power_temp"] == 10 assert custom_attributes["power_temp"] == 10
assert custom_attributes["current_power"] 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
# 3. start listening # 3. start listening
power_manager.start_listening() power_manager.start_listening()
@@ -183,7 +183,7 @@ async def test_power_feature_manager(
[ [
call.fake_vtherm.send_event( call.fake_vtherm.send_event(
EventType.POWER_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( call.fake_vtherm.send_event(
EventType.POWER_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["device_power"] == 1234
assert custom_attributes["power_temp"] == 10 assert custom_attributes["power_temp"] == 10
assert custom_attributes["current_power"] == power 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]) @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", "type": "start",
"current_power": 50, "current_power": 50,
"device_power": 100, "device_power": 100,
"current_power_max": 149, "current_max_power": 149,
"current_power_consumption": 100.0, "current_power_consumption": 100.0,
}, },
), ),
@@ -445,7 +448,7 @@ async def test_power_management_hvac_on(hass: HomeAssistant, skip_hass_states_is
"type": "end", "type": "end",
"current_power": 48, "current_power": 48,
"device_power": 100, "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_sensor_entity_id"] == "sensor.the_presence_sensor"
assert custom_attributes["presence_state"] == presence_state assert custom_attributes["presence_state"] == presence_state
assert custom_attributes["is_presence_configured"] is True 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.preset_mode is PRESET_NONE
assert entity._security_state is False assert entity._security_state is False
assert entity._window_state is None 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.presence_state is STATE_UNKNOWN
assert entity._prop_algorithm is not None assert entity._prop_algorithm is not None
assert entity.have_valve_regulation is False 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.preset_mode is PRESET_NONE
assert entity._security_state is False assert entity._security_state is False
assert entity._window_state is None 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.presence_state is STATE_UNAVAILABLE
assert entity.have_valve_regulation is False 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" "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event"
) as mock_send_event: ) as mock_send_event:
entity = await create_thermostat(hass, entry, "climate.theover4switchmockname") 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 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.preset_mode is PRESET_NONE
assert entity._security_state is False assert entity._security_state is False
assert entity._window_state is None 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.presence_state is STATE_UNKNOWN
assert entity._prop_algorithm is not None 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.preset_mode is PRESET_NONE
assert entity._security_state is False # pylint: disable=protected-access assert entity._security_state is False # pylint: disable=protected-access
assert entity._window_state is None # 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.presence_state is STATE_UNKNOWN
assert entity._prop_algorithm is not None # pylint: disable=protected-access 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.preset_mode is PRESET_NONE
assert entity._security_state is False # pylint: disable=protected-access assert entity._security_state is False # pylint: disable=protected-access
assert entity._window_state is None # 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.presence_state is STATE_UNKNOWN
assert entity._prop_algorithm is not None # pylint: disable=protected-access assert entity._prop_algorithm is not None # pylint: disable=protected-access
assert entity.have_valve_regulation is False assert entity.have_valve_regulation is False