From c83c7729013aef6b0be7dfb3fd9035e43e0e26d3 Mon Sep 17 00:00:00 2001 From: Jean-Marc Collin Date: Fri, 27 Dec 2024 11:53:58 +0000 Subject: [PATCH] All tests Window Feature Manager ok. --- .devcontainer/devcontainer.json | 2 +- .../feature_window_manager.py | 47 +- tests/test_window.py | 226 +----- tests/test_window_feature_manager.py | 706 ++++++++++++++++++ 4 files changed, 727 insertions(+), 254 deletions(-) create mode 100644 tests/test_window_feature_manager.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3e842c2..cb4393d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -37,7 +37,7 @@ "yzhang.markdown-all-in-one", "github.vscode-github-actions", "azuretools.vscode-docker", - "huizhou.githd", + "huizhou.githd" ], "settings": { "files.eol": "\n", diff --git a/custom_components/versatile_thermostat/feature_window_manager.py b/custom_components/versatile_thermostat/feature_window_manager.py index 7da6b85..30a657d 100644 --- a/custom_components/versatile_thermostat/feature_window_manager.py +++ b/custom_components/versatile_thermostat/feature_window_manager.py @@ -8,6 +8,7 @@ from datetime import timedelta from homeassistant.const import ( STATE_ON, + STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -199,33 +200,30 @@ class FeatureWindowManager(BaseFeatureManager): _LOGGER.debug( "Window delay condition is not satisfied. Ignore window event" ) - self._window_state = old_state.state == STATE_ON + # TODO Why ? + # self._window_state = old_state.state or STATE_OFF return _LOGGER.debug("%s - Window delay condition is satisfied", self) - if self._window_state == (new_state.state == STATE_ON): + if self._window_state == new_state.state: _LOGGER.debug("%s - no change in window state. Forget the event") return - self._window_state = new_state.state == STATE_ON - _LOGGER.debug("%s - Window ByPass is : %s", self, self._is_window_bypass) if self._is_window_bypass: _LOGGER.info( "%s - Window ByPass is activated. Ignore window event", self ) else: - await self.update_window_state(self._window_state) + await self.update_window_state(new_state.state) self._vtherm.update_custom_attributes() if new_state is None or old_state is None or new_state.state == old_state.state: return try_window_condition - if self._window_call_cancel: - self._window_call_cancel() - self._window_call_cancel = None + self.dearm_window_timer() self._window_call_cancel = async_call_later( self.hass, timedelta(seconds=self._window_delay_sec), try_window_condition ) @@ -249,8 +247,6 @@ class FeatureWindowManager(BaseFeatureManager): self._vtherm.saved_target_temp, ) - self._window_state = new_state - if self._window_action in [ CONF_WINDOW_FROST_TEMP, CONF_WINDOW_ECO_TEMP, @@ -275,6 +271,7 @@ class FeatureWindowManager(BaseFeatureManager): self, self._window_action, ) + return False else: _LOGGER.info( "%s - Window is open. Apply window action %s", self, self._window_action @@ -283,9 +280,9 @@ class FeatureWindowManager(BaseFeatureManager): _LOGGER.debug( "%s is already off. Forget turning off VTherm due to window detection" ) - return + return False - self._window_state = new_state + # self._window_state = new_state if self._vtherm.last_central_mode in [CENTRAL_MODE_AUTO, None]: if self._window_action in [ CONF_WINDOW_TURN_OFF, @@ -306,14 +303,13 @@ class FeatureWindowManager(BaseFeatureManager): elif ( self._window_action == CONF_WINDOW_FROST_TEMP and self._vtherm.is_preset_configured(PRESET_FROST_PROTECTION) - is not None ): await self._vtherm.change_target_temperature( self._vtherm.find_preset_temp(PRESET_FROST_PROTECTION) ) elif ( self._window_action == CONF_WINDOW_ECO_TEMP - and self._vtherm.is_preset_configured(PRESET_ECO) is not None + and self._vtherm.is_preset_configured(PRESET_ECO) ): await self._vtherm.change_target_temperature( self._vtherm.find_preset_temp(PRESET_ECO) @@ -322,6 +318,7 @@ class FeatureWindowManager(BaseFeatureManager): self._vtherm.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION) await self._vtherm.async_set_hvac_mode(HVACMode.OFF) + self._window_state = new_state return True async def manage_window_auto(self, in_cycle=False) -> callable: @@ -345,16 +342,14 @@ class FeatureWindowManager(BaseFeatureManager): {"type": "end", "cause": cause, "curve_slope": slope}, ) # Set attributes - self._window_auto_state = False + self._window_auto_state = STATE_OFF await self.update_window_state(self._window_auto_state) # await self.restore_hvac_mode(True) - if self._window_call_cancel: - self._window_call_cancel() - self._window_call_cancel = None + self.dearm_window_timer() if not self._window_auto_algo: - return + return None if in_cycle: slope = self._window_auto_algo.check_age_last_measurement( @@ -378,11 +373,11 @@ class FeatureWindowManager(BaseFeatureManager): "%s - Window auto event is ignored because bypass is ON or window auto detection is disabled", self, ) - return + return None if ( self._window_auto_algo.is_window_open_detected() - and self._window_auto_state is False + and self._window_auto_state in [STATE_UNKNOWN, STATE_OFF] and self._vtherm.hvac_mode != HVACMode.OFF ): if ( @@ -406,15 +401,11 @@ class FeatureWindowManager(BaseFeatureManager): {"type": "start", "cause": "slope alert", "curve_slope": slope}, ) # Set attributes - self._window_auto_state = True + self._window_auto_state = STATE_ON await self.update_window_state(self._window_auto_state) - # self.save_hvac_mode() - # await self.async_set_hvac_mode(HVACMode.OFF) # Arm the end trigger - if self._window_call_cancel: - self._window_call_cancel() - self._window_call_cancel = None + self.dearm_window_timer() self._window_call_cancel = async_call_later( self.hass, timedelta(minutes=self._window_auto_max_duration), @@ -423,7 +414,7 @@ class FeatureWindowManager(BaseFeatureManager): elif ( self._window_auto_algo.is_window_close_detected() - and self._window_auto_state is True + and self._window_auto_state == STATE_ON ): await deactivate_window_auto(False) diff --git a/tests/test_window.py b/tests/test_window.py index e743a00..6fa2254 100644 --- a/tests/test_window.py +++ b/tests/test_window.py @@ -2,7 +2,7 @@ """ Test the Window management """ import asyncio import logging -from unittest.mock import patch, call, PropertyMock, AsyncMock, MagicMock +from unittest.mock import patch, call, PropertyMock from datetime import datetime, timedelta from custom_components.versatile_thermostat.base_thermostat import BaseThermostat @@ -10,234 +10,10 @@ from custom_components.versatile_thermostat.thermostat_climate import ( ThermostatOverClimate, ) -from custom_components.versatile_thermostat.feature_window_manager import ( - FeatureWindowManager, -) from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import logging.getLogger().setLevel(logging.DEBUG) - -async def test_window_feature_manager_create( - hass: HomeAssistant, -): - """Test the FeatureMotionManager class direclty""" - - fake_vtherm = MagicMock(spec=BaseThermostat) - type(fake_vtherm).name = PropertyMock(return_value="the name") - - # 1. creation - window_manager = FeatureWindowManager(fake_vtherm, hass) - - assert window_manager is not None - assert window_manager.is_configured is False - assert window_manager.is_window_auto_configured is False - assert window_manager.is_window_detected is False - assert window_manager.window_state == STATE_UNAVAILABLE - assert window_manager.name == "the name" - - assert len(window_manager._active_listener) == 0 - - custom_attributes = {} - window_manager.add_custom_attributes(custom_attributes) - assert custom_attributes["window_sensor_entity_id"] is None - assert custom_attributes["window_state"] == STATE_UNAVAILABLE - assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE - assert custom_attributes["is_window_configured"] is False - assert custom_attributes["is_window_auto_configured"] is False - assert custom_attributes["window_delay_sec"] == 0 - assert custom_attributes["window_auto_open_threshold"] == 0 - assert custom_attributes["window_auto_close_threshold"] == 0 - assert custom_attributes["window_auto_max_duration"] == 0 - assert custom_attributes["window_action"] is None - - -@pytest.mark.parametrize( - "use_window_feature, window_sensor_entity_id, window_delay_sec, window_auto_open_threshold, window_auto_close_threshold, window_auto_max_duration, window_action, is_configured, is_auto_configured, window_state, window_auto_state", - [ - # fmt: off - ( True, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_TURN_OFF, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ), - ( False, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), - ( True, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_TURN_OFF, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ), - # delay is missing - ( True, "sensor.the_window_sensor", None, None, None, None, CONF_WINDOW_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), - # action is missing -> defaults to TURN_OFF - ( True, "sensor.the_window_sensor", 10, None, None, None, None, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ), - # With Window auto config complete - ( True, None, None, 1, 2, 3, CONF_WINDOW_TURN_OFF, True, True, STATE_UNKNOWN, STATE_UNKNOWN ), - # With Window auto config not complete -> missing open threshold but defaults to 0 - ( True, None, None, None, 2, 3, CONF_WINDOW_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), - # With Window auto config not complete -> missing close threshold - ( True, None, None, 1, None, 3, CONF_WINDOW_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), - # With Window auto config not complete -> missing max duration threshold but defaults to 0 - ( True, None, None, 1, 2, None, CONF_WINDOW_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), - # fmt: on - ], -) -async def test_window_feature_manager_post_init( - hass: HomeAssistant, - use_window_feature, - window_sensor_entity_id, - window_delay_sec, - window_auto_open_threshold, - window_auto_close_threshold, - window_auto_max_duration, - window_action, - is_configured, - is_auto_configured, - window_state, - window_auto_state, -): - """Test the FeatureMotionManager class direclty""" - - fake_vtherm = MagicMock(spec=BaseThermostat) - type(fake_vtherm).name = PropertyMock(return_value="the name") - - # 1. creation - window_manager = FeatureWindowManager(fake_vtherm, hass) - assert window_manager is not None - - # 2. post_init - window_manager.post_init( - { - CONF_USE_WINDOW_FEATURE: use_window_feature, - CONF_WINDOW_SENSOR: window_sensor_entity_id, - CONF_WINDOW_DELAY: window_delay_sec, - CONF_WINDOW_AUTO_OPEN_THRESHOLD: window_auto_open_threshold, - CONF_WINDOW_AUTO_CLOSE_THRESHOLD: window_auto_close_threshold, - CONF_WINDOW_AUTO_MAX_DURATION: window_auto_max_duration, - CONF_WINDOW_ACTION: window_action, - } - ) - - assert window_manager.is_configured is is_configured - assert window_manager.is_window_auto_configured == is_auto_configured - assert window_manager.window_sensor_entity_id == window_sensor_entity_id - assert window_manager.window_state == window_state - assert window_manager.window_auto_state == window_auto_state - assert window_manager.window_delay_sec == window_delay_sec - assert window_manager.window_auto_open_threshold == window_auto_open_threshold - assert window_manager.window_auto_close_threshold == window_auto_close_threshold - - custom_attributes = {} - window_manager.add_custom_attributes(custom_attributes) - assert custom_attributes["window_sensor_entity_id"] == window_sensor_entity_id - assert custom_attributes["window_state"] == window_state - assert custom_attributes["window_auto_state"] == window_auto_state - assert custom_attributes["is_window_bypass"] is False - assert custom_attributes["is_window_configured"] is is_configured - assert custom_attributes["is_window_auto_configured"] is is_auto_configured - assert custom_attributes["is_window_bypass"] is False - assert custom_attributes["window_delay_sec"] is window_delay_sec - assert custom_attributes["window_auto_open_threshold"] is window_auto_open_threshold - assert ( - custom_attributes["window_auto_close_threshold"] is window_auto_close_threshold - ) - assert custom_attributes["window_auto_max_duration"] is window_auto_max_duration - - -@pytest.mark.parametrize( - "current_state, new_state, nb_call, window_state, is_window_detected, changed", - [ - (STATE_OFF, STATE_ON, 1, STATE_ON, True, True), - (STATE_OFF, STATE_OFF, 0, STATE_OFF, False, False), - ], -) -async def test_window_feature_manager_refresh_sensor( - hass: HomeAssistant, - current_state, - new_state, # new state of motion event - nb_call, - window_state, - is_window_detected, - 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_COMFORT) - - # 1. creation - window_manager = FeatureWindowManager(fake_vtherm, hass) - - # 2. post_init - window_manager.post_init( - { - CONF_WINDOW_SENSOR: "sensor.the_window_sensor", - CONF_USE_WINDOW_FEATURE: True, - CONF_WINDOW_DELAY: 10, - } - ) - - # 3. start listening - window_manager.start_listening() - assert window_manager.is_configured is True - assert window_manager.window_state == STATE_UNKNOWN - assert window_manager.window_auto_state == STATE_UNAVAILABLE - - assert len(window_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.async_set_hvac_mode = AsyncMock() - fake_vtherm.set_hvac_off_reason = MagicMock() - - # force old state for the test - window_manager._window_state = current_state - - ret = await window_manager.refresh_state() - assert ret == changed - assert window_manager.is_configured is True - # in the refresh there is no delay - assert window_manager.window_state == new_state - assert mock_get_state.call_count == 1 - - assert mock_get_state.call_count == 1 - - assert fake_vtherm.async_set_hvac_mode.call_count == nb_call - - assert fake_vtherm.set_hvac_off_reason.call_count == nb_call - - if nb_call == 1: - fake_vtherm.async_set_hvac_mode.assert_has_calls( - [ - call.async_set_hvac_mode(HVACMode.OFF), - ] - ) - - fake_vtherm.set_hvac_off_reason.assert_has_calls( - [ - call.set_hvac_off_reason(HVAC_OFF_REASON_WINDOW_DETECTION), - ] - ) - - fake_vtherm.reset_mock() - - # 5. Check custom_attributes - custom_attributes = {} - window_manager.add_custom_attributes(custom_attributes) - assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor" - assert custom_attributes["window_state"] == new_state - assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE - assert custom_attributes["is_window_bypass"] is False - assert custom_attributes["is_window_configured"] is True - assert custom_attributes["is_window_auto_configured"] is False - assert custom_attributes["is_window_bypass"] is False - assert custom_attributes["window_delay_sec"] is 10 - assert custom_attributes["window_auto_open_threshold"] is None - assert ( - custom_attributes["window_auto_close_threshold"] is None - ) - assert custom_attributes["window_auto_max_duration"] is None - - window_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_window_management_time_not_enough( diff --git a/tests/test_window_feature_manager.py b/tests/test_window_feature_manager.py new file mode 100644 index 0000000..21d9092 --- /dev/null +++ b/tests/test_window_feature_manager.py @@ -0,0 +1,706 @@ +# pylint: disable=unused-argument, line-too-long, protected-access, too-many-lines +""" Test the Window management """ +import logging +from datetime import datetime, timedelta +from unittest.mock import patch, call, PropertyMock, AsyncMock, MagicMock + +from custom_components.versatile_thermostat.base_thermostat import BaseThermostat + +from custom_components.versatile_thermostat.feature_window_manager import ( + FeatureWindowManager, +) +from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import + +logging.getLogger().setLevel(logging.DEBUG) + + +async def test_window_feature_manager_create( + hass: HomeAssistant, +): + """Test the FeatureMotionManager class direclty""" + + fake_vtherm = MagicMock(spec=BaseThermostat) + type(fake_vtherm).name = PropertyMock(return_value="the name") + + # 1. creation + window_manager = FeatureWindowManager(fake_vtherm, hass) + + assert window_manager is not None + assert window_manager.is_configured is False + assert window_manager.is_window_auto_configured is False + assert window_manager.is_window_detected is False + assert window_manager.window_state == STATE_UNAVAILABLE + assert window_manager.name == "the name" + + assert len(window_manager._active_listener) == 0 + + custom_attributes = {} + window_manager.add_custom_attributes(custom_attributes) + assert custom_attributes["window_sensor_entity_id"] is None + assert custom_attributes["window_state"] == STATE_UNAVAILABLE + assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE + assert custom_attributes["is_window_configured"] is False + assert custom_attributes["is_window_auto_configured"] is False + assert custom_attributes["window_delay_sec"] == 0 + assert custom_attributes["window_auto_open_threshold"] == 0 + assert custom_attributes["window_auto_close_threshold"] == 0 + assert custom_attributes["window_auto_max_duration"] == 0 + assert custom_attributes["window_action"] is None + + +@pytest.mark.parametrize( + "use_window_feature, window_sensor_entity_id, window_delay_sec, window_auto_open_threshold, window_auto_close_threshold, window_auto_max_duration, window_action, is_configured, is_auto_configured, window_state, window_auto_state", + [ + # fmt: off + ( True, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_TURN_OFF, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ), + ( False, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_FAN_ONLY, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), + ( True, "sensor.the_window_sensor", 10, None, None, None, CONF_WINDOW_FROST_TEMP, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ), + # delay is missing + ( True, "sensor.the_window_sensor", None, None, None, None, CONF_WINDOW_ECO_TEMP, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), + # action is missing -> defaults to TURN_OFF + ( True, "sensor.the_window_sensor", 10, None, None, None, None, True, False, STATE_UNKNOWN, STATE_UNAVAILABLE ), + # With Window auto config complete + ( True, None, None, 1, 2, 3, CONF_WINDOW_FAN_ONLY, True, True, STATE_UNKNOWN, STATE_UNKNOWN ), + # With Window auto config not complete -> missing open threshold but defaults to 0 + ( True, None, None, None, 2, 3, CONF_WINDOW_FROST_TEMP, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), + # With Window auto config not complete -> missing close threshold + ( True, None, None, 1, None, 3, CONF_WINDOW_ECO_TEMP, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), + # With Window auto config not complete -> missing max duration threshold but defaults to 0 + ( True, None, None, 1, 2, None, CONF_WINDOW_TURN_OFF, False, False, STATE_UNAVAILABLE, STATE_UNAVAILABLE ), + # fmt: on + ], +) +async def test_window_feature_manager_post_init( + hass: HomeAssistant, + use_window_feature, + window_sensor_entity_id, + window_delay_sec, + window_auto_open_threshold, + window_auto_close_threshold, + window_auto_max_duration, + window_action, + is_configured, + is_auto_configured, + window_state, + window_auto_state, +): + """Test the FeatureMotionManager class direclty""" + + fake_vtherm = MagicMock(spec=BaseThermostat) + type(fake_vtherm).name = PropertyMock(return_value="the name") + + # 1. creation + window_manager = FeatureWindowManager(fake_vtherm, hass) + assert window_manager is not None + + # 2. post_init + window_manager.post_init( + { + CONF_USE_WINDOW_FEATURE: use_window_feature, + CONF_WINDOW_SENSOR: window_sensor_entity_id, + CONF_WINDOW_DELAY: window_delay_sec, + CONF_WINDOW_AUTO_OPEN_THRESHOLD: window_auto_open_threshold, + CONF_WINDOW_AUTO_CLOSE_THRESHOLD: window_auto_close_threshold, + CONF_WINDOW_AUTO_MAX_DURATION: window_auto_max_duration, + CONF_WINDOW_ACTION: window_action, + } + ) + + assert window_manager.is_configured is is_configured + assert window_manager.is_window_auto_configured == is_auto_configured + assert window_manager.window_sensor_entity_id == window_sensor_entity_id + assert window_manager.window_state == window_state + assert window_manager.window_auto_state == window_auto_state + assert window_manager.window_delay_sec == window_delay_sec + assert window_manager.window_auto_open_threshold == window_auto_open_threshold + assert window_manager.window_auto_close_threshold == window_auto_close_threshold + + custom_attributes = {} + window_manager.add_custom_attributes(custom_attributes) + assert custom_attributes["window_sensor_entity_id"] == window_sensor_entity_id + assert custom_attributes["window_state"] == window_state + assert custom_attributes["window_auto_state"] == window_auto_state + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["is_window_configured"] is is_configured + assert custom_attributes["is_window_auto_configured"] is is_auto_configured + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["window_delay_sec"] is window_delay_sec + assert custom_attributes["window_auto_open_threshold"] is window_auto_open_threshold + assert ( + custom_attributes["window_auto_close_threshold"] is window_auto_close_threshold + ) + assert custom_attributes["window_auto_max_duration"] is window_auto_max_duration + + +@pytest.mark.parametrize( + "current_state, new_state, nb_call, window_state, is_window_detected, changed", + [ + (STATE_OFF, STATE_ON, 1, STATE_ON, True, True), + (STATE_OFF, STATE_OFF, 0, STATE_OFF, False, False), + (STATE_ON, STATE_OFF, 1, STATE_OFF, False, True), + (STATE_ON, STATE_ON, 0, STATE_ON, True, False), + ], +) +async def test_window_feature_manager_refresh_sensor_action_turn_off( + hass: HomeAssistant, + current_state, + new_state, # new state of motion event + nb_call, + window_state, + is_window_detected, + 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_COMFORT) + + # 1. creation + window_manager = FeatureWindowManager(fake_vtherm, hass) + + # 2. post_init + window_manager.post_init( + { + CONF_WINDOW_SENSOR: "sensor.the_window_sensor", + CONF_USE_WINDOW_FEATURE: True, + CONF_WINDOW_DELAY: 10, + CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF, + } + ) + + # 3. start listening + window_manager.start_listening() + assert window_manager.is_configured is True + assert window_manager.window_state == STATE_UNKNOWN + assert window_manager.window_auto_state == STATE_UNAVAILABLE + + assert len(window_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.async_set_hvac_mode = AsyncMock() + fake_vtherm.set_hvac_off_reason = MagicMock() + fake_vtherm.restore_hvac_mode = AsyncMock() + + # force old state for the test + window_manager._window_state = current_state + if current_state == STATE_ON: + type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=HVAC_OFF_REASON_WINDOW_DETECTION) + else: + type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=None) + + ret = await window_manager.refresh_state() + assert ret == changed + assert window_manager.is_configured is True + # in the refresh there is no delay + assert window_manager.window_state == new_state + assert mock_get_state.call_count == 1 + + assert fake_vtherm.set_hvac_off_reason.call_count == nb_call + + if nb_call == 1: + if new_state == STATE_OFF: + assert fake_vtherm.restore_hvac_mode.call_count == 1 + assert fake_vtherm.async_set_hvac_mode.call_count == 0 + else: + assert fake_vtherm.async_set_hvac_mode.call_count == 1 + fake_vtherm.async_set_hvac_mode.assert_has_calls( + [ + call.async_set_hvac_mode(HVACMode.OFF), + ] + ) + assert fake_vtherm.restore_hvac_mode.call_count == 0 + + reason = None if current_state == STATE_ON and new_state == STATE_OFF else HVAC_OFF_REASON_WINDOW_DETECTION + fake_vtherm.set_hvac_off_reason.assert_has_calls( + [ + call.set_hvac_off_reason(reason), + ] + ) + + else: + assert fake_vtherm.restore_hvac_mode.call_count == 0 + assert fake_vtherm.async_set_hvac_mode.call_count == 0 + + fake_vtherm.reset_mock() + + # 5. Check custom_attributes + custom_attributes = {} + window_manager.add_custom_attributes(custom_attributes) + assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor" + assert custom_attributes["window_state"] == new_state + assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["is_window_configured"] is True + assert custom_attributes["is_window_auto_configured"] is False + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["window_delay_sec"] is 10 + assert custom_attributes["window_auto_open_threshold"] is None + assert ( + custom_attributes["window_auto_close_threshold"] is None + ) + assert custom_attributes["window_auto_max_duration"] is None + + window_manager.stop_listening() + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "current_state, new_state, nb_call, window_state, is_window_detected, changed", + [ + (STATE_OFF, STATE_ON, 1, STATE_ON, True, True), + (STATE_OFF, STATE_OFF, 0, STATE_OFF, False, False), + (STATE_ON, STATE_OFF, 1, STATE_OFF, False, True), + (STATE_ON, STATE_ON, 0, STATE_ON, True, False), + ], +) +async def test_window_feature_manager_refresh_sensor_action_frost_only( + hass: HomeAssistant, + current_state, + new_state, # new state of motion event + nb_call, + window_state, + is_window_detected, + 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_COMFORT) + type(fake_vtherm).last_central_mode = PropertyMock(return_value=None) + + # 1. creation + window_manager = FeatureWindowManager(fake_vtherm, hass) + + # 2. post_init + window_manager.post_init( + { + CONF_WINDOW_SENSOR: "sensor.the_window_sensor", + CONF_USE_WINDOW_FEATURE: True, + CONF_WINDOW_DELAY: 10, + CONF_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP, + } + ) + + # 3. start listening + window_manager.start_listening() + assert window_manager.is_configured is True + assert window_manager.window_state == STATE_UNKNOWN + assert window_manager.window_auto_state == STATE_UNAVAILABLE + + assert len(window_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.save_target_temp = AsyncMock() + fake_vtherm.set_hvac_off_reason = MagicMock() + fake_vtherm.restore_target_temp = AsyncMock() + fake_vtherm.change_target_temperature = AsyncMock() + fake_vtherm.find_preset_temp = MagicMock() + fake_vtherm.find_preset_temp.return_value = 17 + + # force old state for the test + window_manager._window_state = current_state + if current_state == STATE_ON: + type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=HVAC_OFF_REASON_WINDOW_DETECTION) + else: + type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=None) + + ret = await window_manager.refresh_state() + assert ret == changed + assert window_manager.is_configured is True + # in the refresh there is no delay + assert window_manager.window_state == new_state + assert mock_get_state.call_count == 1 + + assert fake_vtherm.set_hvac_off_reason.call_count == 0 + + if nb_call == 1: + if new_state == STATE_OFF: + assert fake_vtherm.restore_target_temp.call_count == 1 + assert fake_vtherm.save_target_temp.call_count == 0 + assert fake_vtherm.change_target_temperature.call_count == 0 + assert fake_vtherm.find_preset_temp.call_count == 0 + else: + assert fake_vtherm.restore_target_temp.call_count == 0 + assert fake_vtherm.save_target_temp.call_count == 1 + assert fake_vtherm.change_target_temperature.call_count == 1 + fake_vtherm.change_target_temperature.assert_has_calls( + [ + call.change_target_temperature(17), + ] + ) + assert fake_vtherm.find_preset_temp.call_count == 1 + else: + assert fake_vtherm.restore_hvac_mode.call_count == 0 + assert fake_vtherm.save_target_temp.call_count == 0 + assert fake_vtherm.change_target_temperature.call_count == 0 + assert fake_vtherm.find_preset_temp.call_count == 0 + + fake_vtherm.reset_mock() + + # 5. Check custom_attributes + custom_attributes = {} + window_manager.add_custom_attributes(custom_attributes) + assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor" + assert custom_attributes["window_state"] == new_state + assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["is_window_configured"] is True + assert custom_attributes["is_window_auto_configured"] is False + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["window_delay_sec"] is 10 + assert custom_attributes["window_auto_open_threshold"] is None + assert ( + custom_attributes["window_auto_close_threshold"] is None + ) + assert custom_attributes["window_auto_max_duration"] is None + + window_manager.stop_listening() + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "current_state, long_enough, new_state, nb_call, window_state, is_window_detected", + [ + (STATE_OFF, True, STATE_ON, 1, STATE_ON, True), + (STATE_OFF, True, STATE_OFF, 0, STATE_OFF, False), + (STATE_ON, True, STATE_OFF, 1, STATE_OFF, False), + (STATE_ON, True, STATE_ON, 0, STATE_ON, True), + (STATE_OFF, False, STATE_ON, 0, STATE_OFF, False), + (STATE_ON, False, STATE_OFF, 0, STATE_ON, True), + ], +) +async def test_window_feature_manager_sensor_event_action_turn_off( + hass: HomeAssistant, + current_state, + long_enough, + new_state, # new state of motion event + nb_call, + window_state, + is_window_detected, +): + """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_COMFORT) + + # 1. creation + window_manager = FeatureWindowManager(fake_vtherm, hass) + + # 2. post_init + window_manager.post_init( + { + CONF_WINDOW_SENSOR: "sensor.the_window_sensor", + CONF_USE_WINDOW_FEATURE: True, + CONF_WINDOW_DELAY: 10, + CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF, + } + ) + + # 3. start listening + window_manager.start_listening() + assert len(window_manager._active_listener) == 1 + + # 4. test refresh with the parametrized + # fmt:off + with patch("homeassistant.helpers.condition.state", return_value=long_enough): + # fmt:on + # Configurer les méthodes mockées + fake_vtherm.async_set_hvac_mode = AsyncMock() + fake_vtherm.set_hvac_off_reason = MagicMock() + fake_vtherm.restore_hvac_mode = AsyncMock() + + # force old state for the test + window_manager._window_state = current_state + if current_state == STATE_ON: + type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=HVAC_OFF_REASON_WINDOW_DETECTION) + else: + type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=None) + + try_window_condition = await window_manager._window_sensor_changed( + event=Event( + event_type=EVENT_STATE_CHANGED, + data={ + "entity_id": "sensor.the_window_sensor", + "new_state": State("sensor.the_window_sensor", new_state), + "old_state": State("sensor.the_window_sensor", current_state), + })) + assert try_window_condition is not None + + await try_window_condition(None) + + # There is change only if long enough + if long_enough: + assert window_manager.window_state == new_state + else: + assert window_manager.window_state == current_state + + assert fake_vtherm.set_hvac_off_reason.call_count == nb_call + + if nb_call == 1: + if new_state == STATE_OFF: + assert fake_vtherm.restore_hvac_mode.call_count == 1 + assert fake_vtherm.async_set_hvac_mode.call_count == 0 + else: + assert fake_vtherm.async_set_hvac_mode.call_count == 1 + fake_vtherm.async_set_hvac_mode.assert_has_calls( + [ + call.async_set_hvac_mode(HVACMode.OFF), + ] + ) + assert fake_vtherm.restore_hvac_mode.call_count == 0 + + reason = None if current_state == STATE_ON and new_state == STATE_OFF else HVAC_OFF_REASON_WINDOW_DETECTION + fake_vtherm.set_hvac_off_reason.assert_has_calls( + [ + call.set_hvac_off_reason(reason), + ] + ) + + else: + assert fake_vtherm.restore_hvac_mode.call_count == 0 + assert fake_vtherm.async_set_hvac_mode.call_count == 0 + + fake_vtherm.reset_mock() + + # 5. Check custom_attributes + custom_attributes = {} + window_manager.add_custom_attributes(custom_attributes) + assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor" + assert custom_attributes["window_state"] == new_state if long_enough else current_state + assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["is_window_configured"] is True + assert custom_attributes["is_window_auto_configured"] is False + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["window_delay_sec"] is 10 + assert custom_attributes["window_auto_open_threshold"] is None + assert ( + custom_attributes["window_auto_close_threshold"] is None + ) + assert custom_attributes["window_auto_max_duration"] is None + + window_manager.stop_listening() + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "current_state, long_enough, new_state, nb_call, window_state, is_window_detected", + [ + (STATE_OFF, True, STATE_ON, 1, STATE_ON, True), + (STATE_OFF, True, STATE_OFF, 0, STATE_OFF, False), + (STATE_ON, True, STATE_OFF, 1, STATE_OFF, False), + (STATE_ON, True, STATE_ON, 0, STATE_ON, True), + (STATE_OFF, False, STATE_ON, 0, STATE_OFF, False), + (STATE_ON, False, STATE_OFF, 0, STATE_ON, True), + ], +) +async def test_window_feature_manager_event_sensor_action_frost_only( + hass: HomeAssistant, + current_state, + long_enough, + new_state, # new state of motion event + nb_call, + window_state, + is_window_detected, +): + """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_COMFORT) + type(fake_vtherm).last_central_mode = PropertyMock(return_value=None) + + # 1. creation + window_manager = FeatureWindowManager(fake_vtherm, hass) + + # 2. post_init + window_manager.post_init( + { + CONF_WINDOW_SENSOR: "sensor.the_window_sensor", + CONF_USE_WINDOW_FEATURE: True, + CONF_WINDOW_DELAY: 10, + CONF_WINDOW_ACTION: CONF_WINDOW_FROST_TEMP, + } + ) + + # 3. start listening + window_manager.start_listening() + + # 4. test refresh with the parametrized + # fmt:off + with patch("homeassistant.helpers.condition.state", return_value=long_enough): + # fmt:on + # Configurer les méthodes mockées + fake_vtherm.save_target_temp = AsyncMock() + fake_vtherm.set_hvac_off_reason = MagicMock() + fake_vtherm.restore_target_temp = AsyncMock() + fake_vtherm.change_target_temperature = AsyncMock() + fake_vtherm.find_preset_temp = MagicMock() + fake_vtherm.find_preset_temp.return_value = 17 + + # force old state for the test + window_manager._window_state = current_state + if current_state == STATE_ON: + type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=HVAC_OFF_REASON_WINDOW_DETECTION) + else: + type(fake_vtherm).hvac_off_reason = PropertyMock(return_value=None) + + try_window_condition = await window_manager._window_sensor_changed( + event=Event( + event_type=EVENT_STATE_CHANGED, + data={ + "entity_id": "sensor.the_window_sensor", + "new_state": State("sensor.the_window_sensor", new_state), + "old_state": State("sensor.the_window_sensor", current_state), + })) + assert try_window_condition is not None + + await try_window_condition(None) + + if long_enough: + assert window_manager.window_state == new_state + else: + assert window_manager.window_state == current_state + + assert fake_vtherm.set_hvac_off_reason.call_count == 0 + + if nb_call == 1: + if new_state == STATE_OFF: + assert fake_vtherm.restore_target_temp.call_count == 1 + assert fake_vtherm.save_target_temp.call_count == 0 + assert fake_vtherm.change_target_temperature.call_count == 0 + assert fake_vtherm.find_preset_temp.call_count == 0 + else: + assert fake_vtherm.restore_target_temp.call_count == 0 + assert fake_vtherm.save_target_temp.call_count == 1 + assert fake_vtherm.change_target_temperature.call_count == 1 + fake_vtherm.change_target_temperature.assert_has_calls( + [ + call.change_target_temperature(17), + ] + ) + assert fake_vtherm.find_preset_temp.call_count == 1 + else: + assert fake_vtherm.restore_hvac_mode.call_count == 0 + assert fake_vtherm.save_target_temp.call_count == 0 + assert fake_vtherm.change_target_temperature.call_count == 0 + assert fake_vtherm.find_preset_temp.call_count == 0 + + fake_vtherm.reset_mock() + + # 5. Check custom_attributes + custom_attributes = {} + window_manager.add_custom_attributes(custom_attributes) + assert custom_attributes["window_sensor_entity_id"] == "sensor.the_window_sensor" + assert custom_attributes["window_state"] == new_state if long_enough else current_state + assert custom_attributes["window_auto_state"] == STATE_UNAVAILABLE + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["is_window_configured"] is True + assert custom_attributes["is_window_auto_configured"] is False + assert custom_attributes["is_window_bypass"] is False + assert custom_attributes["window_delay_sec"] is 10 + assert custom_attributes["window_auto_open_threshold"] is None + assert ( + custom_attributes["window_auto_close_threshold"] is None + ) + assert custom_attributes["window_auto_max_duration"] is None + + window_manager.stop_listening() + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "current_state, in_cycle, new_temp, new_state, nb_call, window_state, is_window_detected", + [ + (STATE_OFF, True, 10, STATE_ON, 1, STATE_ON, True), + (STATE_ON, True, 10, STATE_ON, 0, STATE_ON, True), + (STATE_ON, True, 20, STATE_OFF, 1, STATE_OFF, False), + (STATE_OFF, True, 20, STATE_OFF, 0, STATE_OFF, False), + ], +) +async def test_window_feature_manager_window_auto( + hass: HomeAssistant, + current_state, + in_cycle, + new_temp, + new_state, # new state of motion event + nb_call, + window_state, + is_window_detected, +): + """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_COMFORT) + type(fake_vtherm).hvac_mode = PropertyMock(return_value=HVACMode.HEAT) + type(fake_vtherm).last_central_mode = PropertyMock(return_value=None) + type(fake_vtherm).proportional_algorithm = PropertyMock(return_value=None) + + # 1. creation / post_init / start listening + window_manager = FeatureWindowManager(fake_vtherm, hass) + window_manager.post_init( + { + CONF_USE_WINDOW_FEATURE: True, + CONF_WINDOW_AUTO_OPEN_THRESHOLD: 3, + CONF_WINDOW_AUTO_CLOSE_THRESHOLD: 1, + CONF_WINDOW_AUTO_MAX_DURATION: 10, + CONF_WINDOW_ACTION: CONF_WINDOW_TURN_OFF, + } + ) + assert window_manager.is_window_auto_configured is True + window_manager.start_listening() + + # 2. Call manage window auto + tz = get_tz(hass) # pylint: disable=invalid-name + now: datetime = datetime.now(tz=tz) + + # Add a fake temp point for the window_auto_algo. We need at least 4 points + for i in range(0, 4): + window_manager._window_auto_algo.add_temp_measurement( + 17 + (i * (new_temp - 17) / 4), now, True + ) + now = now + timedelta(minutes=5) + + # fmt:off + with patch("custom_components.versatile_thermostat.feature_window_manager.FeatureWindowManager.update_window_state") as mock_update_window_state: + #fmt: on + now = now + timedelta(minutes=10) + # From 17 to new_temp in 10 minutes + type(fake_vtherm).ema_temp = PropertyMock(return_value=new_temp) + type(fake_vtherm).last_temperature_measure = PropertyMock(return_value=now) + type(fake_vtherm).now = PropertyMock(return_value=now) + fake_vtherm.send_event = MagicMock() + + window_manager._window_auto_state = current_state + + dearm_window_auto = await window_manager.manage_window_auto(in_cycle=in_cycle) + assert dearm_window_auto is not None + + assert mock_update_window_state.call_count == nb_call + if nb_call > 0: + mock_update_window_state.assert_has_calls( + [ + call.update_window_state(new_state), + ] + ) + if new_state == STATE_ON: + assert window_manager._window_call_cancel is not None + + assert window_manager.window_auto_state == new_state + # update_window_state is mocked + # assert window_manager.window_state == new_state + + window_manager.stop_listening() + await hass.async_block_till_done()