Skip to content

Commit

Permalink
Fix movement detection
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Marc Collin committed Oct 20, 2023
1 parent 7574467 commit f956b85
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 30 deletions.
86 changes: 56 additions & 30 deletions custom_components/versatile_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,7 @@ async def _async_motion_changed(self, event):
# 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,
Expand All @@ -1546,41 +1547,66 @@ async def try_motion_condition(_):
_LOGGER.debug(
"Motion delay condition is not satisfied. Ignore motion event"
)
return
else:
_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._async_internal_set_temperature(
self.find_preset_temp(new_preset)
)
self.recalculate()
await self._async_control_heating(force=True)
self._motion_call_cancel = None

_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._async_internal_set_temperature(
self.find_preset_temp(new_preset)
)
self.recalculate()
await self._async_control_heating(force=True)
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 self._motion_call_cancel:
def desarm():
# restart the timer
self._motion_call_cancel()
self._motion_call_cancel = None

# Delay
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
)

# For testing purpose we need to access the inner function
return 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)
desarm()
return None
# Ignore the event
_LOGGER.debug("%s - Event ignored cause i'm already on", self)
return None

@callback
async def _check_switch_initial_state(self):
Expand Down
128 changes: 128 additions & 0 deletions custom_components/versatile_thermostat/tests/test_movement.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,131 @@ async def test_movement_management_time_enoughand_not_presence(
assert entity.proportional_algorithm.on_percent == 0.11
assert mock_heater_off.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

0 comments on commit f956b85

Please sign in to comment.