diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 9f70c90ced1..faf4059f891 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -11,7 +11,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components import switch from homeassistant.components.thermostat import ( STATE_HEAT, STATE_COOL, STATE_IDLE, ThermostatDevice) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF +from homeassistant.helpers import condition from homeassistant.helpers.event import track_state_change DEPENDENCIES = ['switch', 'sensor'] @@ -26,6 +27,7 @@ CONF_MIN_TEMP = 'min_temp' CONF_MAX_TEMP = 'max_temp' CONF_TARGET_TEMP = 'target_temp' CONF_AC_MODE = 'ac_mode' +CONF_MIN_DUR = 'min_cycle_duration' _LOGGER = logging.getLogger(__name__) @@ -38,6 +40,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), vol.Optional(CONF_AC_MODE): vol.Coerce(bool), + vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), }) @@ -50,9 +53,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): max_temp = config.get(CONF_MAX_TEMP) target_temp = config.get(CONF_TARGET_TEMP) ac_mode = config.get(CONF_AC_MODE) + min_cycle_duration = config.get(CONF_MIN_DUR) add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id, - min_temp, max_temp, target_temp, ac_mode)]) + min_temp, max_temp, target_temp, ac_mode, + min_cycle_duration)]) # pylint: disable=too-many-instance-attributes, abstract-method @@ -61,12 +66,13 @@ class HeatControl(ThermostatDevice): # pylint: disable=too-many-arguments def __init__(self, hass, name, heater_entity_id, sensor_entity_id, - min_temp, max_temp, target_temp, ac_mode): + min_temp, max_temp, target_temp, ac_mode, min_cycle_duration): """Initialize the thermostat.""" self.hass = hass self._name = name self.heater_entity_id = heater_entity_id self.ac_mode = ac_mode + self.min_cycle_duration = min_cycle_duration self._active = False self._cur_temp = None @@ -172,6 +178,17 @@ class HeatControl(ThermostatDevice): if not self._active: 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 self.ac_mode: too_hot = self._cur_temp - self._target_temp > TOL_TEMP is_cooling = self._is_device_active diff --git a/tests/components/thermostat/test_heat_control.py b/tests/components/thermostat/test_heat_control.py index 7c14d2b1490..1ccc3fb2903 100644 --- a/tests/components/thermostat/test_heat_control.py +++ b/tests/components/thermostat/test_heat_control.py @@ -1,5 +1,8 @@ """The tests for the heat control thermostat.""" +import datetime import unittest +from unittest import mock + from homeassistant.bootstrap import _setup_component from homeassistant.const import ( @@ -307,3 +310,184 @@ class TestThermostatHeatControlACMode(unittest.TestCase): self.hass.services.register('switch', SERVICE_TURN_ON, log_call) self.hass.services.register('switch', SERVICE_TURN_OFF, log_call) + + +class TestThermostatHeatControlACModeMinCycle(unittest.TestCase): + """Test the Heat Control thermostat.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.temperature_unit = TEMP_CELSIUS + thermostat.setup(self.hass, {'thermostat': { + 'platform': 'heat_control', + 'name': 'test', + 'heater': ENT_SWITCH, + 'target_sensor': ENT_SENSOR, + 'ac_mode': True, + 'min_cycle_duration': datetime.timedelta(minutes=10) + }}) + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_temp_change_ac_trigger_on_not_long_enough(self): + """Test if temperature change turn ac on.""" + self._setup_switch(False) + thermostat.set_temperature(self.hass, 25) + self.hass.pool.block_till_done() + self._setup_sensor(30) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_temp_change_ac_trigger_on_long_enough(self): + """Test if temperature change turn ac on.""" + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, + tzinfo=datetime.timezone.utc) + with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=fake_changed): + self._setup_switch(False) + thermostat.set_temperature(self.hass, 25) + self.hass.pool.block_till_done() + self._setup_sensor(30) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + + def test_temp_change_ac_trigger_off_not_long_enough(self): + """Test if temperature change turn ac on.""" + self._setup_switch(True) + thermostat.set_temperature(self.hass, 30) + self.hass.pool.block_till_done() + self._setup_sensor(25) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_temp_change_ac_trigger_off_long_enough(self): + """Test if temperature change turn ac on.""" + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, + tzinfo=datetime.timezone.utc) + with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=fake_changed): + self._setup_switch(True) + thermostat.set_temperature(self.hass, 30) + self.hass.pool.block_till_done() + self._setup_sensor(25) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + + def _setup_sensor(self, temp, unit=TEMP_CELSIUS): + """Setup the test sensor.""" + self.hass.states.set(ENT_SENSOR, temp, { + ATTR_UNIT_OF_MEASUREMENT: unit + }) + + def _setup_switch(self, is_on): + """Setup the test switch.""" + self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) + self.calls = [] + + def log_call(call): + """Log service calls.""" + self.calls.append(call) + + self.hass.services.register('switch', SERVICE_TURN_ON, log_call) + self.hass.services.register('switch', SERVICE_TURN_OFF, log_call) + + +class TestThermostatHeatControlMinCycle(unittest.TestCase): + """Test the Heat Control thermostat.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.temperature_unit = TEMP_CELSIUS + thermostat.setup(self.hass, {'thermostat': { + 'platform': 'heat_control', + 'name': 'test', + 'heater': ENT_SWITCH, + 'target_sensor': ENT_SENSOR, + 'min_cycle_duration': datetime.timedelta(minutes=10) + }}) + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_temp_change_heater_trigger_off_not_long_enough(self): + """Test if temp change doesn't turn heater off because of time.""" + self._setup_switch(True) + thermostat.set_temperature(self.hass, 25) + self.hass.pool.block_till_done() + self._setup_sensor(30) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_temp_change_heater_trigger_on_not_long_enough(self): + """Test if temp change doesn't turn heater on because of time.""" + self._setup_switch(False) + thermostat.set_temperature(self.hass, 30) + self.hass.pool.block_till_done() + self._setup_sensor(25) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_temp_change_heater_trigger_on_long_enough(self): + """Test if temperature change turn heater on after min cycle.""" + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, + tzinfo=datetime.timezone.utc) + with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=fake_changed): + self._setup_switch(False) + thermostat.set_temperature(self.hass, 30) + self.hass.pool.block_till_done() + self._setup_sensor(25) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_ON, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + + def test_temp_change_heater_trigger_off_long_enough(self): + """Test if temperature change turn heater off after min cycle.""" + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, + tzinfo=datetime.timezone.utc) + with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=fake_changed): + self._setup_switch(True) + thermostat.set_temperature(self.hass, 25) + self.hass.pool.block_till_done() + self._setup_sensor(30) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + call = self.calls[0] + self.assertEqual('switch', call.domain) + self.assertEqual(SERVICE_TURN_OFF, call.service) + self.assertEqual(ENT_SWITCH, call.data['entity_id']) + + def _setup_sensor(self, temp, unit=TEMP_CELSIUS): + """Setup the test sensor.""" + self.hass.states.set(ENT_SENSOR, temp, { + ATTR_UNIT_OF_MEASUREMENT: unit + }) + + def _setup_switch(self, is_on): + """Setup the test switch.""" + self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF) + self.calls = [] + + def log_call(call): + """Log service calls.""" + self.calls.append(call) + + self.hass.services.register('switch', SERVICE_TURN_ON, log_call) + self.hass.services.register('switch', SERVICE_TURN_OFF, log_call)