# pylint: disable=wildcard-import, unused-wildcard-import, protected-access, unused-argument, line-too-long """ Test the Window management """ from unittest.mock import patch, call from datetime import datetime, timedelta import logging from .commons import * logging.getLogger().setLevel(logging.DEBUG) @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_bug_56( hass: HomeAssistant, skip_hass_states_is_state, skip_turn_on_off_heater, skip_send_event, ): """Test that in over_climate mode there is no error when underlying climate is not available""" the_mock_underlying = MagicMockClimate() with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", return_value=None, # dont find the underlying climate ): entry = MockConfigEntry( domain=DOMAIN, title="TheOverClimateMockName", unique_id="uniqueId", data={ CONF_NAME: "TheOverClimateMockName", CONF_THERMOSTAT_TYPE: CONF_THERMOSTAT_CLIMATE, 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, CONF_USE_WINDOW_FEATURE: False, CONF_USE_MOTION_FEATURE: False, CONF_USE_POWER_FEATURE: False, CONF_USE_PRESENCE_FEATURE: False, CONF_CLIMATE: "climate.mock_climate", CONF_MINIMAL_ACTIVATION_DELAY: 30, CONF_SECURITY_DELAY_MIN: 5, CONF_SECURITY_MIN_ON_PERCENT: 0.3, }, ) entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverclimatemockname" ) assert entity # cause the underlying climate was not found assert entity.is_over_climate is True assert entity.underlying_entity(0)._underlying_climate is None # Should not failed entity.update_custom_attributes() # try to call async_control_heating try: ret = await entity.async_control_heating() # an exception should be send assert ret is False except Exception: # pylint: disable=broad-exception-caught assert False # This time the underlying will be found with patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", return_value=the_mock_underlying, # dont find the underlying climate ): # try to call async_control_heating try: await entity.async_control_heating() except UnknownEntity: assert False except Exception: # pylint: disable=broad-exception-caught assert False # Should not failed entity.update_custom_attributes() @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_bug_63( hass: HomeAssistant, skip_hass_states_is_state, skip_turn_on_off_heater, skip_send_event, ): """Test that it should be possible to set the security_default_on_percent to 0""" 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, CONF_USE_WINDOW_FEATURE: False, CONF_USE_MOTION_FEATURE: False, 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_SECURITY_DELAY_MIN: 5, CONF_SECURITY_MIN_ON_PERCENT: 0.0, # !! here CONF_SECURITY_DEFAULT_ON_PERCENT: 0.0, # !! here CONF_DEVICE_POWER: 200, }, ) entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity assert entity._security_min_on_percent == 0 assert entity._security_default_on_percent == 0 # Waiting for answer in https://github.com/jmcollin78/versatile_thermostat/issues/64 # Repro case not evident @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_bug_64( hass: HomeAssistant, skip_hass_states_is_state, skip_turn_on_off_heater, skip_send_event, ): """Test that it should be possible to set the security_default_on_percent to 0""" 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, CONF_USE_WINDOW_FEATURE: False, CONF_USE_MOTION_FEATURE: False, 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_SECURITY_DELAY_MIN: 5, CONF_SECURITY_MIN_ON_PERCENT: 0.5, CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here CONF_DEVICE_POWER: 200, }, ) entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_bug_66( hass: HomeAssistant, skip_hass_states_is_state, skip_turn_on_off_heater, skip_send_event, ): """Test that it should be possible to open/close the window rapidly without side effect""" tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) 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, CONF_USE_WINDOW_FEATURE: True, CONF_USE_MOTION_FEATURE: False, 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_SECURITY_DELAY_MIN: 5, CONF_SECURITY_MIN_ON_PERCENT: 0.5, CONF_SECURITY_DEFAULT_ON_PERCENT: 0.1, # !! here CONF_DEVICE_POWER: 200, CONF_WINDOW_SENSOR: "binary_sensor.mock_window_sensor", CONF_WINDOW_DELAY: 0, # important to not been obliged to wait }, ) entity: BaseThermostat = await create_thermostat( hass, entry, "climate.theoverswitchmockname" ) assert entity await entity.async_set_hvac_mode(HVACMode.HEAT) await entity.async_set_preset_mode(PRESET_BOOST) assert entity.hvac_mode is HVACMode.HEAT assert entity.preset_mode is PRESET_BOOST assert entity.target_temperature == 19 assert entity.window_state is STATE_OFF # Open the window and let the thermostat shut down 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( "homeassistant.helpers.condition.state", return_value=True ) as mock_condition, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", return_value=True, ): await send_temperature_change_event(entity, 15, now) try_window_condition = await send_window_change_event( entity, True, False, now, False ) # simulate the call to try_window_condition await try_window_condition(None) assert mock_send_event.call_count == 1 assert mock_heater_on.call_count == 1 assert mock_heater_off.call_count >= 1 assert mock_condition.call_count == 1 assert entity.window_state == STATE_ON # Close the window but too shortly 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( "homeassistant.helpers.condition.state", return_value=False ) as mock_condition, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", return_value=False, ): event_timestamp = now + timedelta(minutes=1) try_window_condition = await send_window_change_event( entity, False, True, event_timestamp ) # simulate the call to try_window_condition await try_window_condition(None) # window state should not have change assert entity.window_state == STATE_ON # Reopen immediatly with sufficient 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( "homeassistant.helpers.condition.state", return_value=True ) as mock_condition, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", return_value=False, ): try_window_condition = await send_window_change_event( entity, True, False, event_timestamp ) # simulate the call to try_window_condition await try_window_condition(None) # still no change assert entity.window_state == STATE_ON assert entity.hvac_mode == HVACMode.OFF # Close the window but with sufficient time 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( "homeassistant.helpers.condition.state", return_value=True ) as mock_condition, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.is_device_active", return_value=False, ): event_timestamp = now + timedelta(minutes=2) try_window_condition = await send_window_change_event( entity, False, True, event_timestamp ) # simulate the call to try_window_condition await try_window_condition(None) # window state should be Off this time and old state should have been restored assert entity.window_state == STATE_OFF assert entity.hvac_mode is HVACMode.HEAT assert entity.preset_mode is PRESET_BOOST @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_bug_82( hass: HomeAssistant, skip_hass_states_is_state, skip_turn_on_off_heater, skip_send_event, ): """Test that when a underlying climate is not available the VTherm doesn't go into security mode""" tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) entry = MockConfigEntry( domain=DOMAIN, title="TheOverClimateMockName", unique_id="uniqueId", data=PARTIAL_CLIMATE_CONFIG, # 5 minutes security delay ) fake_underlying_climate = MockUnavailableClimate(hass, "mockUniqueId", "MockClimateName", {}) with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", return_value=fake_underlying_climate, ) as mock_find_climate: 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 = find_my_entity("climate.theoverclimatemockname") assert entity assert entity.name == "TheOverClimateMockName" assert entity.is_over_climate is True # assert entity.hvac_action is HVACAction.OFF assert entity.hvac_mode is HVACMode.OFF # assert entity.hvac_mode is None assert entity.target_temperature == entity.min_temp assert entity.preset_modes == [ PRESET_NONE, PRESET_ECO, PRESET_COMFORT, PRESET_BOOST, ] assert entity.preset_mode is PRESET_NONE assert entity._security_state is False # should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT assert mock_send_event.call_count == 2 mock_send_event.assert_has_calls( [ call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}), call.send_event( EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}, ), ] ) assert mock_find_climate.call_count == 1 assert mock_find_climate.mock_calls[0] == call() mock_find_climate.assert_has_calls([call.find_underlying_entity()]) # Force security mode assert entity._last_ext_temperature_mesure is not None assert entity._last_temperature_mesure is not None assert (entity._last_temperature_mesure.astimezone(tz) - now).total_seconds() < 1 assert ( entity._last_ext_temperature_mesure.astimezone(tz) - now ).total_seconds() < 1 # Tries to turns on the Thermostat await entity.async_set_hvac_mode(HVACMode.HEAT) assert entity.hvac_mode == HVACMode.HEAT # 2. activate security feature when date is expired with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingSwitch.turn_on" ): event_timestamp = now - timedelta(minutes=6) # set temperature to 15 so that on_percent will be > security_min_on_percent (0.2) await send_temperature_change_event(entity, 15, event_timestamp) # Should stay False assert entity.security_state is False assert entity.preset_mode == 'none' assert entity._saved_preset_mode == 'none' @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize("expected_lingering_timers", [True]) async def test_bug_101( hass: HomeAssistant, skip_hass_states_is_state, skip_turn_on_off_heater, skip_send_event, ): """Test that when a underlying climate target temp is changed, the VTherm change its own temperature target and switch to manual""" tz = get_tz(hass) # pylint: disable=invalid-name now: datetime = datetime.now(tz=tz) entry = MockConfigEntry( domain=DOMAIN, title="TheOverClimateMockName", unique_id="uniqueId", data=PARTIAL_CLIMATE_NOT_REGULATED_CONFIG, # 5 minutes security delay ) # Underlying is in HEAT mode but should be shutdown at startup fake_underlying_climate = MockClimate(hass, "mockUniqueId", "MockClimateName", {}, HVACMode.HEAT, HVACAction.HEATING) with patch( "custom_components.versatile_thermostat.base_thermostat.BaseThermostat.send_event" ) as mock_send_event, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.find_underlying_climate", return_value=fake_underlying_climate, ) as mock_find_climate, patch( "custom_components.versatile_thermostat.underlyings.UnderlyingClimate.set_hvac_mode" ) as mock_underlying_set_hvac_mode: 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 = find_my_entity("climate.theoverclimatemockname") assert entity assert entity.name == "TheOverClimateMockName" assert entity.is_over_climate is True assert entity.hvac_mode is HVACMode.OFF # because in MockClimate HVACAction is HEATING if hvac_mode is not set assert entity.hvac_action is HVACAction.HEATING # Underlying should have been shutdown assert mock_underlying_set_hvac_mode.call_count == 1 mock_underlying_set_hvac_mode.assert_has_calls( [ call.set_hvac_mode(HVACMode.OFF), ] ) assert entity.target_temperature == entity.min_temp assert entity.preset_mode is PRESET_NONE # should have been called with EventType.PRESET_EVENT and EventType.HVAC_MODE_EVENT assert mock_send_event.call_count == 2 mock_send_event.assert_has_calls( [ call.send_event(EventType.PRESET_EVENT, {"preset": PRESET_NONE}), call.send_event( EventType.HVAC_MODE_EVENT, {"hvac_mode": HVACMode.OFF}, ), ] ) assert mock_find_climate.call_count == 1 assert mock_find_climate.mock_calls[0] == call() mock_find_climate.assert_has_calls([call.find_underlying_entity()]) # Force preset mode await entity.async_set_hvac_mode(HVACMode.HEAT) assert entity.hvac_mode == HVACMode.HEAT await entity.async_set_preset_mode(PRESET_COMFORT) assert entity.preset_mode == PRESET_COMFORT # 2. Change the target temp of underlying thermostat at now -> the event will be disgarded because to fast (to avoid loop cf issue 121) await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, now, 12.75) # Should NOT have been switched to Manual preset assert entity.target_temperature == 17 assert entity.preset_mode is PRESET_COMFORT # 2. Change the target temp of underlying thermostat at 11 sec later -> the event will be taken # Wait 11 sec event_timestamp = now + timedelta(seconds=11) assert entity.is_regulated is False await send_climate_change_event_with_temperature(entity, HVACMode.HEAT, HVACMode.HEAT, HVACAction.OFF, HVACAction.OFF, event_timestamp, 12.75) assert entity.target_temperature == 12.75 assert entity.preset_mode is PRESET_NONE