Compare commits
1 Commits
3.6.0.beta
...
3.6.0.beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f956b85752 |
@@ -1533,6 +1533,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
# Check delay condition
|
# Check delay condition
|
||||||
async def try_motion_condition(_):
|
async def try_motion_condition(_):
|
||||||
try:
|
try:
|
||||||
|
delay = self._motion_delay_sec if new_state.state == STATE_ON else self._motion_off_delay_sec
|
||||||
long_enough = condition.state(
|
long_enough = condition.state(
|
||||||
self.hass,
|
self.hass,
|
||||||
self._motion_sensor_entity_id,
|
self._motion_sensor_entity_id,
|
||||||
@@ -1546,8 +1547,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Motion delay condition is not satisfied. Ignore motion event"
|
"Motion delay condition is not satisfied. Ignore motion event"
|
||||||
)
|
)
|
||||||
return
|
else:
|
||||||
|
|
||||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||||
self._motion_state = new_state.state
|
self._motion_state = new_state.state
|
||||||
if self._attr_preset_mode == PRESET_ACTIVITY:
|
if self._attr_preset_mode == PRESET_ACTIVITY:
|
||||||
@@ -1568,19 +1568,45 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
|||||||
)
|
)
|
||||||
self.recalculate()
|
self.recalculate()
|
||||||
await self._async_control_heating(force=True)
|
await self._async_control_heating(force=True)
|
||||||
|
|
||||||
if self._motion_call_cancel:
|
|
||||||
self._motion_call_cancel()
|
|
||||||
self._motion_call_cancel = None
|
self._motion_call_cancel = None
|
||||||
|
|
||||||
# Delay
|
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
|
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._motion_call_cancel = async_call_later(
|
||||||
self.hass, timedelta(seconds=delay), try_motion_condition
|
self.hass, timedelta(seconds=delay), try_motion_condition
|
||||||
)
|
)
|
||||||
|
|
||||||
# For testing purpose we need to access the inner function
|
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
|
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_switch_initial_state(self):
|
async def _check_switch_initial_state(self):
|
||||||
|
|||||||
@@ -470,3 +470,131 @@ async def test_movement_management_time_enoughand_not_presence(
|
|||||||
assert entity.proportional_algorithm.on_percent == 0.11
|
assert entity.proportional_algorithm.on_percent == 0.11
|
||||||
assert mock_heater_off.call_count == 0
|
assert mock_heater_off.call_count == 0
|
||||||
assert mock_send_event.call_count == 0
|
assert mock_send_event.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("expected_lingering_tasks", [True])
|
||||||
|
@pytest.mark.parametrize("expected_lingering_timers", [True])
|
||||||
|
async def test_movement_management_with_stop_during_condition(
|
||||||
|
hass: HomeAssistant, skip_hass_states_is_state
|
||||||
|
):
|
||||||
|
"""Test the Presence management when the movement sensor switch to off and then to on during the test condition"""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="TheOverSwitchMockName",
|
||||||
|
unique_id="uniqueId",
|
||||||
|
data={
|
||||||
|
CONF_NAME: "TheOverSwitchMockName",
|
||||||
|
CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_SWITCH,
|
||||||
|
CONF_TEMP_SENSOR: "sensor.mock_temp_sensor",
|
||||||
|
CONF_EXTERNAL_TEMP_SENSOR: "sensor.mock_ext_temp_sensor",
|
||||||
|
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_PROP_FUNCTION: PROPORTIONAL_FUNCTION_TPI,
|
||||||
|
CONF_TPI_COEF_INT: 0.3,
|
||||||
|
CONF_TPI_COEF_EXT: 0.01,
|
||||||
|
CONF_MINIMAL_ACTIVATION_DELAY: 30,
|
||||||
|
CONF_SECURITY_DELAY_MIN: 5,
|
||||||
|
CONF_SECURITY_MIN_ON_PERCENT: 0.3,
|
||||||
|
CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor",
|
||||||
|
CONF_MOTION_DELAY: 10,
|
||||||
|
CONF_MOTION_OFF_DELAY: 30,
|
||||||
|
CONF_MOTION_PRESET: "boost",
|
||||||
|
CONF_NO_MOTION_PRESET: "comfort",
|
||||||
|
CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
entity: VersatileThermostat = await create_thermostat(
|
||||||
|
hass, entry, "climate.theoverswitchmockname"
|
||||||
|
)
|
||||||
|
assert entity
|
||||||
|
|
||||||
|
tz = get_tz(hass) # pylint: disable=invalid-name
|
||||||
|
now: datetime = datetime.now(tz=tz)
|
||||||
|
|
||||||
|
# start heating, in boost mode. We block the control_heating to avoid running a cycle
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat._async_control_heating"
|
||||||
|
):
|
||||||
|
await entity.async_set_hvac_mode(HVACMode.HEAT)
|
||||||
|
await entity.async_set_preset_mode(PRESET_ACTIVITY)
|
||||||
|
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
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.presence_state is None
|
||||||
|
|
||||||
|
event_timestamp = now - timedelta(minutes=6)
|
||||||
|
await send_temperature_change_event(entity, 18, event_timestamp)
|
||||||
|
await send_ext_temperature_change_event(entity, 10, event_timestamp)
|
||||||
|
|
||||||
|
await send_presence_change_event(entity, False, True, event_timestamp)
|
||||||
|
assert entity.presence_state is "off"
|
||||||
|
|
||||||
|
# starts detecting motion
|
||||||
|
with patch(
|
||||||
|
"custom_components.versatile_thermostat.climate.VersatileThermostat.send_event"
|
||||||
|
) as mock_send_event, patch(
|
||||||
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on"
|
||||||
|
) as mock_heater_on, patch(
|
||||||
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_off"
|
||||||
|
) as mock_heater_off, patch(
|
||||||
|
"custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active",
|
||||||
|
return_value=True,
|
||||||
|
), patch("homeassistant.helpers.condition.state", return_value=True): # Not needed for this test
|
||||||
|
event_timestamp = now - timedelta(minutes=5)
|
||||||
|
try_condition1 = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||||
|
|
||||||
|
assert try_condition1 is not None
|
||||||
|
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
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.presence_state is "off"
|
||||||
|
|
||||||
|
# Send a stop detection
|
||||||
|
event_timestamp = now - timedelta(minutes=4)
|
||||||
|
try_condition = await send_motion_change_event(entity, False, True, event_timestamp)
|
||||||
|
assert try_condition is None # The timer should not have been stopped
|
||||||
|
|
||||||
|
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.presence_state is "off"
|
||||||
|
|
||||||
|
# Resend a start detection
|
||||||
|
event_timestamp = now - timedelta(minutes=3)
|
||||||
|
try_condition = await send_motion_change_event(entity, True, False, event_timestamp)
|
||||||
|
assert try_condition is None # The timer should not have been restarted (we keep the first one)
|
||||||
|
|
||||||
|
assert entity.hvac_mode is HVACMode.HEAT
|
||||||
|
assert entity.preset_mode is PRESET_ACTIVITY
|
||||||
|
# still no motion detected
|
||||||
|
assert entity.target_temperature == 18
|
||||||
|
assert entity.motion_state is None
|
||||||
|
assert entity.presence_state is "off"
|
||||||
|
|
||||||
|
await try_condition1(None)
|
||||||
|
# We should have switch this time
|
||||||
|
assert entity.target_temperature == 19 # Boost
|
||||||
|
assert entity.motion_state is "on" # switch to movement on
|
||||||
|
assert entity.presence_state is "off" # Non change
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user