From 02b46e2ba3732d1b223ebb30c2a98fe2c41e9c77 Mon Sep 17 00:00:00 2001 From: Lev Aronsky Date: Thu, 1 Nov 2018 22:25:50 +0200 Subject: [PATCH] Ignore min_cycle_duration when manually controlling the thermostat. (#16128) * Ignore min_cycle_duration when manually controlling the thermostat. * style * Generic thermostat: add minimum cycle duration to keep-alive tests. There was a bug in previous versions of the code, that would not execute the keep-alive action if the minimum cycle duration hasn't passed. This test verifies that the keep-alive action is executed correctly. * Generic thermostat: added tests to verify that changing the thermostat mode manually triggers the switch, regardless of minimum cycle duration. * Updated tests to use `common` module instead of the deprecated `climate` --- .../components/climate/generic_thermostat.py | 37 ++++++----- .../climate/test_generic_thermostat.py | 66 +++++++++++++++++++ 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index ad8875462fd..d421157c2ec 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -232,11 +232,11 @@ class GenericThermostat(ClimateDevice): if operation_mode == STATE_HEAT: self._current_operation = STATE_HEAT self._enabled = True - await self._async_control_heating() + await self._async_control_heating(force=True) elif operation_mode == STATE_COOL: self._current_operation = STATE_COOL self._enabled = True - await self._async_control_heating() + await self._async_control_heating(force=True) elif operation_mode == STATE_OFF: self._current_operation = STATE_OFF self._enabled = False @@ -262,7 +262,7 @@ class GenericThermostat(ClimateDevice): if temperature is None: return self._target_temp = temperature - await self._async_control_heating() + await self._async_control_heating(force=True) await self.async_update_ha_state() @property @@ -307,7 +307,7 @@ class GenericThermostat(ClimateDevice): except ValueError as ex: _LOGGER.error("Unable to update from sensor: %s", ex) - async def _async_control_heating(self, time=None): + async def _async_control_heating(self, time=None, force=False): """Check if we need to turn heating on or off.""" async with self._temp_lock: if not self._active and None not in (self._cur_temp, @@ -320,16 +320,21 @@ class GenericThermostat(ClimateDevice): if not self._active or not self._enabled: return - if self.min_cycle_duration: - if self._is_device_active: - current_state = STATE_ON - else: - current_state = STATE_OFF - long_enough = condition.state( - self.hass, self.heater_entity_id, current_state, - self.min_cycle_duration) - if not long_enough: - return + if not force and time is None: + # If the `force` argument is True, we + # ignore `min_cycle_duration`. + # If the `time` argument is not none, we were invoked for + # keep-alive purposes, and `min_cycle_duration` is irrelevant. + if self.min_cycle_duration: + if self._is_device_active: + current_state = STATE_ON + else: + current_state = STATE_OFF + long_enough = condition.state( + self.hass, self.heater_entity_id, current_state, + self.min_cycle_duration) + if not long_enough: + return too_cold = \ self._target_temp - self._cur_temp >= self._cold_tolerance @@ -385,7 +390,7 @@ class GenericThermostat(ClimateDevice): self._is_away = True self._saved_target_temp = self._target_temp self._target_temp = self._away_temp - await self._async_control_heating() + await self._async_control_heating(force=True) await self.async_update_ha_state() async def async_turn_away_mode_off(self): @@ -394,5 +399,5 @@ class GenericThermostat(ClimateDevice): return self._is_away = False self._target_temp = self._saved_target_temp - await self._async_control_heating() + await self._async_control_heating(force=True) await self.async_update_ha_state() diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index f87a5371773..6bdbc58e011 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -623,6 +623,38 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): assert SERVICE_TURN_OFF == call.service assert ENT_SWITCH == call.data['entity_id'] + def test_mode_change_ac_trigger_off_not_long_enough(self): + """Test if mode change turns ac off despite minimum cycle.""" + self._setup_switch(True) + common.set_temperature(self.hass, 30) + self.hass.block_till_done() + self._setup_sensor(25) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + common.set_operation_mode(self.hass, climate.STATE_OFF) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('homeassistant', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + + def test_mode_change_ac_trigger_on_not_long_enough(self): + """Test if mode change turns ac on despite minimum cycle.""" + self._setup_switch(False) + common.set_temperature(self.hass, 25) + self.hass.block_till_done() + self._setup_sensor(30) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + common.set_operation_mode(self.hass, climate.STATE_HEAT) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('homeassistant', call.domain) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def _setup_sensor(self, temp): """Set up the test sensor.""" self.hass.states.set(ENT_SENSOR, temp) @@ -714,6 +746,38 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): assert SERVICE_TURN_OFF == call.service assert ENT_SWITCH == call.data['entity_id'] + def test_mode_change_heater_trigger_off_not_long_enough(self): + """Test if mode change turns heater off despite minimum cycle.""" + self._setup_switch(True) + common.set_temperature(self.hass, 25) + self.hass.block_till_done() + self._setup_sensor(30) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + common.set_operation_mode(self.hass, climate.STATE_OFF) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('homeassistant', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + + def test_mode_change_heater_trigger_on_not_long_enough(self): + """Test if mode change turns heater on despite minimum cycle.""" + self._setup_switch(False) + common.set_temperature(self.hass, 30) + self.hass.block_till_done() + self._setup_sensor(25) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + common.set_operation_mode(self.hass, climate.STATE_HEAT) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('homeassistant', call.domain) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + def _setup_sensor(self, temp): """Set up the test sensor.""" self.hass.states.set(ENT_SENSOR, temp) @@ -748,6 +812,7 @@ class TestClimateGenericThermostatACKeepAlive(unittest.TestCase): 'target_temp': 25, 'target_sensor': ENT_SENSOR, 'ac_mode': True, + 'min_cycle_duration': datetime.timedelta(minutes=15), 'keep_alive': datetime.timedelta(minutes=10) }}) @@ -838,6 +903,7 @@ class TestClimateGenericThermostatKeepAlive(unittest.TestCase): 'target_temp': 25, 'heater': ENT_SWITCH, 'target_sensor': ENT_SENSOR, + 'min_cycle_duration': datetime.timedelta(minutes=15), 'keep_alive': datetime.timedelta(minutes=10) }})