Add option to heat_control component to set min cycle duration
This commit adds a new config option to the heat_control thermostat component, min_cycle_duration. Some heaters and/or ACs don't like being constantly cycled on and off. Prior to this patch the heat_control component can end up cycling the switch quite frequently. (depending on how quickly the temperature changes) The new option added is used for setting a minimum duration that must have elapsed in either the on or off state before the thermostat will send the service call to cycle the switch. This should enable users to hand tune how frequently heat_control can switch the device on or off to best suit the device being used.
This commit is contained in:
parent
792154a6a7
commit
496972a587
2 changed files with 205 additions and 3 deletions
|
@ -8,12 +8,14 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import condition
|
||||
import homeassistant.util as util
|
||||
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, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
||||
DEPENDENCIES = ['switch', 'sensor']
|
||||
|
@ -28,6 +30,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__)
|
||||
|
||||
|
@ -40,6 +43,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),
|
||||
})
|
||||
|
||||
|
||||
|
@ -52,9 +56,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
|
||||
|
@ -63,12 +69,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
|
||||
|
@ -187,6 +194,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
|
||||
|
|
|
@ -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 (
|
||||
|
@ -296,3 +299,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)
|
||||
|
|
Loading…
Add table
Reference in a new issue