# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long, unused-variable, too-many-lines """ Test the Window management """ from datetime import datetime, timedelta import logging 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_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, 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, CONF_USE_WINDOW_FEATURE: False, CONF_USE_MOTION_FEATURE: True, CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: True, CONF_UNDERLYING_LIST: ["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_SAFETY_DELAY_MIN: 10, CONF_SAFETY_MIN_ON_PERCENT: 0.3, CONF_MOTION_SENSOR: "binary_sensor.mock_motion_sensor", CONF_MOTION_DELAY: 10, # important to not been obliged to wait CONF_MOTION_OFF_DELAY: 30, CONF_MOTION_PRESET: "boost", CONF_NO_MOTION_PRESET: "comfort", CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor", }, ) entity: BaseThermostat = await create_thermostat( 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) # 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" ): 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 STATE_UNKNOWN assert entity.presence_state is STATE_UNKNOWN event_timestamp = now - timedelta(minutes=5) 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, True, False, event_timestamp) assert entity.presence_state == STATE_ON # 2. starts detecting motion with time not enough with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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=False, ), patch( "homeassistant.helpers.condition.state", return_value=False ) as mock_condition, patch( "homeassistant.core.StateMachine.get", return_value=State( entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF ), ): event_timestamp = now - timedelta(minutes=4) 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) 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 # state is not changed if time is not enough assert entity.motion_state is STATE_OFF assert entity.presence_state == STATE_ON assert mock_send_event.call_count == 0 # Change is not confirmed assert mock_heater_on.call_count == 0 assert mock_heater_off.call_count == 0 assert mock_send_event.call_count == 0 # starts detecting motion with time enough this time with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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=False, ), patch( "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 ) # Will return True -> we will switch to movement On await try_condition(None) assert entity.hvac_mode is HVACMode.HEAT assert entity.preset_mode is PRESET_ACTIVITY # because motion is detected yet assert entity.target_temperature == 19 assert entity.motion_state == STATE_ON assert entity.presence_state == STATE_ON # stop detecting motion with off delay too low with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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, ) as mock_device_active, patch( "homeassistant.helpers.condition.state", return_value=False ) as mock_condition, patch( "homeassistant.core.StateMachine.get", return_value=State( entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF ), ): event_timestamp = now - timedelta(minutes=2) 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) assert entity.hvac_mode is HVACMode.HEAT assert entity.preset_mode is PRESET_ACTIVITY # because no motion is detected yet assert entity.target_temperature == 19 assert entity.motion_state == STATE_ON assert entity.presence_state == STATE_ON assert mock_send_event.call_count == 0 # The heater must heat now assert mock_heater_on.call_count == 1 assert mock_heater_off.call_count == 0 assert mock_send_event.call_count == 0 # stop detecting motion with off delay enough long with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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, ) as mock_device_active, patch( "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 ) # Will return True -> we will switch to movement Off await try_condition(None) 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 == STATE_OFF assert entity.presence_state == STATE_ON assert mock_send_event.call_count == 0 # The heater must stop heating now assert mock_heater_on.call_count == 0 assert mock_heater_off.call_count == 1 assert mock_send_event.call_count == 0 @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) 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""" 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_SAFETY_DELAY_MIN: 5, CONF_SAFETY_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: "boost", CONF_NO_MOTION_PRESET: "comfort", CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor", }, ) entity: BaseThermostat = 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.base_thermostat.BaseThermostat.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 STATE_UNKNOWN assert entity.presence_state is STATE_UNKNOWN event_timestamp = now - timedelta(minutes=4) 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, True, False, event_timestamp) assert entity.presence_state == "on" # starts detecting motion with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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=False, ), patch( "homeassistant.helpers.condition.state", return_value=True ): event_timestamp = now - timedelta(minutes=3) await send_motion_change_event(entity, True, False, event_timestamp) 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 == 19 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 assert mock_heater_on.call_count == 1 assert mock_heater_off.call_count == 0 assert mock_send_event.call_count == 0 # stop detecting motion with confirmation of stop with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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 ): event_timestamp = now - timedelta(minutes=2) await send_motion_change_event(entity, False, True, event_timestamp) 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 == STATE_OFF assert entity.presence_state == STATE_ON assert mock_send_event.call_count == 0 assert mock_heater_on.call_count == 0 # Because heating is no more necessary assert mock_heater_off.call_count == 1 assert mock_send_event.call_count == 0 @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) 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""" 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.1, "comfort_away_temp": 18.1, "boost_away_temp": 19.1, 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_SAFETY_DELAY_MIN: 5, CONF_SAFETY_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: "boost", CONF_NO_MOTION_PRESET: "comfort", CONF_PRESENCE_SENSOR: "binary_sensor.mock_presence_sensor", }, ) entity: BaseThermostat = 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.base_thermostat.BaseThermostat.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 and presence is unknown assert entity.target_temperature == 18 assert entity.motion_state is STATE_UNKNOWN assert entity.presence_state is STATE_UNKNOWN event_timestamp = now - timedelta(minutes=4) 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 == STATE_OFF # starts detecting motion with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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=False, ), patch( "homeassistant.helpers.condition.state", return_value=True ): event_timestamp = now - timedelta(minutes=3) await send_motion_change_event(entity, True, False, event_timestamp) assert entity.hvac_mode is HVACMode.HEAT 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 == STATE_ON assert entity.presence_state == STATE_OFF assert mock_send_event.call_count == 0 # Change is confirmed. Heater should be started assert mock_heater_on.call_count == 1 assert mock_heater_off.call_count == 0 assert mock_send_event.call_count == 0 # stop detecting motion with confirmation of stop with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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 ): event_timestamp = now - timedelta(minutes=2) await send_motion_change_event(entity, False, True, event_timestamp) 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.1 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 assert mock_heater_on.call_count == 1 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_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""" 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_SAFETY_DELAY_MIN: 5, CONF_SAFETY_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: BaseThermostat = 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.base_thermostat.BaseThermostat.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 STATE_UNKNOWN assert entity.presence_state is STATE_UNKNOWN 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 == STATE_OFF # starts detecting motion with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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 STATE_UNKNOWN assert entity.presence_state == STATE_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 STATE_UNKNOWN assert entity.presence_state == STATE_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 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 == 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_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""" 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: False, 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_SAFETY_DELAY_MIN: 5, CONF_SAFETY_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", }, ) entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) # 0. start heating, in boost mode. We block the control_heating to avoid running a cycle with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.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 STATE_UNKNOWN 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) # 1. starts detecting motion but the sensor is off with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", return_value=True, ), patch("homeassistant.helpers.condition.state", return_value=False), patch( "homeassistant.core.StateMachine.get", return_value=State( entity_id="binary_sensor.mock_motion_sensor", state=STATE_OFF ), ): event_timestamp = now - timedelta(minutes=5) try_condition1 = await send_motion_change_event( entity, True, False, event_timestamp ) assert try_condition1 is not None await try_condition1(None) # because no motion is detected yet -> condition.state is False and sensor is not active assert entity.target_temperature == 18 assert entity.motion_state is STATE_OFF # 2. starts detecting motion but the sensor is on with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", return_value=True, ), patch("homeassistant.helpers.condition.state", return_value=False), patch( "homeassistant.core.StateMachine.get", return_value=State( entity_id="binary_sensor.mock_motion_sensor", state=STATE_ON ), ): event_timestamp = now - timedelta(minutes=5) try_condition1 = await send_motion_change_event( entity, True, False, event_timestamp ) assert try_condition1 is not None await try_condition1(None) # because no motion is detected yet -> condition.state is False and sensor is not active assert entity.target_temperature == 19 assert entity.motion_state is STATE_ON