Add keep-alive feature to the generic thermostat (#6040)
* Add keep-alive feature to the generic thermostat * Comply with maximum line lengths * Added tests for the keep-alive functionalitypull/6402/head
parent
aac9f972cf
commit
f396a4593e
|
@ -16,7 +16,8 @@ from homeassistant.components.climate import (
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
|
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
|
||||||
from homeassistant.helpers import condition
|
from homeassistant.helpers import condition
|
||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import (
|
||||||
|
async_track_state_change, async_track_time_interval)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -35,6 +36,7 @@ CONF_TARGET_TEMP = 'target_temp'
|
||||||
CONF_AC_MODE = 'ac_mode'
|
CONF_AC_MODE = 'ac_mode'
|
||||||
CONF_MIN_DUR = 'min_cycle_duration'
|
CONF_MIN_DUR = 'min_cycle_duration'
|
||||||
CONF_TOLERANCE = 'tolerance'
|
CONF_TOLERANCE = 'tolerance'
|
||||||
|
CONF_KEEP_ALIVE = 'keep_alive'
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
@ -47,6 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
||||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_KEEP_ALIVE): vol.All(
|
||||||
|
cv.time_period, cv.positive_timedelta),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,10 +66,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
ac_mode = config.get(CONF_AC_MODE)
|
ac_mode = config.get(CONF_AC_MODE)
|
||||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||||
tolerance = config.get(CONF_TOLERANCE)
|
tolerance = config.get(CONF_TOLERANCE)
|
||||||
|
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||||
|
|
||||||
async_add_devices([GenericThermostat(
|
async_add_devices([GenericThermostat(
|
||||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||||
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
target_temp, ac_mode, min_cycle_duration, tolerance, keep_alive)])
|
||||||
|
|
||||||
|
|
||||||
class GenericThermostat(ClimateDevice):
|
class GenericThermostat(ClimateDevice):
|
||||||
|
@ -73,7 +78,7 @@ class GenericThermostat(ClimateDevice):
|
||||||
|
|
||||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||||
tolerance):
|
tolerance, keep_alive):
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
|
@ -81,6 +86,7 @@ class GenericThermostat(ClimateDevice):
|
||||||
self.ac_mode = ac_mode
|
self.ac_mode = ac_mode
|
||||||
self.min_cycle_duration = min_cycle_duration
|
self.min_cycle_duration = min_cycle_duration
|
||||||
self._tolerance = tolerance
|
self._tolerance = tolerance
|
||||||
|
self._keep_alive = keep_alive
|
||||||
|
|
||||||
self._active = False
|
self._active = False
|
||||||
self._cur_temp = None
|
self._cur_temp = None
|
||||||
|
@ -94,6 +100,10 @@ class GenericThermostat(ClimateDevice):
|
||||||
async_track_state_change(
|
async_track_state_change(
|
||||||
hass, heater_entity_id, self._async_switch_changed)
|
hass, heater_entity_id, self._async_switch_changed)
|
||||||
|
|
||||||
|
if self._keep_alive:
|
||||||
|
async_track_time_interval(
|
||||||
|
hass, self._async_keep_alive, self._keep_alive)
|
||||||
|
|
||||||
sensor_state = hass.states.get(sensor_entity_id)
|
sensor_state = hass.states.get(sensor_entity_id)
|
||||||
if sensor_state:
|
if sensor_state:
|
||||||
self._async_update_temp(sensor_state)
|
self._async_update_temp(sensor_state)
|
||||||
|
@ -180,6 +190,14 @@ class GenericThermostat(ClimateDevice):
|
||||||
return
|
return
|
||||||
self.hass.async_add_job(self.async_update_ha_state())
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_keep_alive(self, time):
|
||||||
|
"""Called at constant intervals for keep-alive purposes."""
|
||||||
|
if self.current_operation in [STATE_COOL, STATE_HEAT]:
|
||||||
|
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||||
|
else:
|
||||||
|
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_temp(self, state):
|
def _async_update_temp(self, state):
|
||||||
"""Update thermostat with latest state from sensor."""
|
"""Update thermostat with latest state from sensor."""
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""The tests for the generic_thermostat."""
|
"""The tests for the generic_thermostat."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
import pytz
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import homeassistant.core as ha
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.bootstrap import setup_component, async_setup_component
|
from homeassistant.bootstrap import setup_component, async_setup_component
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -524,6 +526,187 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase):
|
||||||
self.hass.services.register('switch', SERVICE_TURN_OFF, log_call)
|
self.hass.services.register('switch', SERVICE_TURN_OFF, log_call)
|
||||||
|
|
||||||
|
|
||||||
|
class TestClimateGenericThermostatACKeepAlive(unittest.TestCase):
|
||||||
|
"""Test the Generic 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
|
||||||
|
assert setup_component(self.hass, climate.DOMAIN, {'climate': {
|
||||||
|
'platform': 'generic_thermostat',
|
||||||
|
'name': 'test',
|
||||||
|
'tolerance': 0.3,
|
||||||
|
'heater': ENT_SWITCH,
|
||||||
|
'target_sensor': ENT_SENSOR,
|
||||||
|
'ac_mode': True,
|
||||||
|
'keep_alive': 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_long_enough(self):
|
||||||
|
"""Test if turn on signal is sent at keep-alive intervals."""
|
||||||
|
self._setup_switch(True)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self._setup_sensor(30)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
climate.set_temperature(self.hass, 25)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
test_time = datetime.datetime.now(pytz.UTC)
|
||||||
|
self._send_time_changed(test_time)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
self._send_time_changed(test_time + datetime.timedelta(minutes=5))
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
self._send_time_changed(test_time + datetime.timedelta(minutes=10))
|
||||||
|
self.hass.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_long_enough(self):
|
||||||
|
"""Test if turn on signal is sent at keep-alive intervals."""
|
||||||
|
self._setup_switch(False)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self._setup_sensor(20)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
climate.set_temperature(self.hass, 25)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
test_time = datetime.datetime.now(pytz.UTC)
|
||||||
|
self._send_time_changed(test_time)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
self._send_time_changed(test_time + datetime.timedelta(minutes=5))
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
self._send_time_changed(test_time + datetime.timedelta(minutes=10))
|
||||||
|
self.hass.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 _send_time_changed(self, now):
|
||||||
|
"""Send a time changed event."""
|
||||||
|
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
@callback
|
||||||
|
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 TestClimateGenericThermostatKeepAlive(unittest.TestCase):
|
||||||
|
"""Test the Generic 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
|
||||||
|
assert setup_component(self.hass, climate.DOMAIN, {'climate': {
|
||||||
|
'platform': 'generic_thermostat',
|
||||||
|
'name': 'test',
|
||||||
|
'tolerance': 0.3,
|
||||||
|
'heater': ENT_SWITCH,
|
||||||
|
'target_sensor': ENT_SENSOR,
|
||||||
|
'keep_alive': 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_on_long_enough(self):
|
||||||
|
"""Test if turn on signal is sent at keep-alive intervals."""
|
||||||
|
self._setup_switch(True)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self._setup_sensor(20)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
climate.set_temperature(self.hass, 25)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
test_time = datetime.datetime.now(pytz.UTC)
|
||||||
|
self._send_time_changed(test_time)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
self._send_time_changed(test_time + datetime.timedelta(minutes=5))
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
self._send_time_changed(test_time + datetime.timedelta(minutes=10))
|
||||||
|
self.hass.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 turn on signal is sent at keep-alive intervals."""
|
||||||
|
self._setup_switch(False)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self._setup_sensor(30)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
climate.set_temperature(self.hass, 25)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
test_time = datetime.datetime.now(pytz.UTC)
|
||||||
|
self._send_time_changed(test_time)
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
self._send_time_changed(test_time + datetime.timedelta(minutes=5))
|
||||||
|
self.hass.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
self._send_time_changed(test_time + datetime.timedelta(minutes=10))
|
||||||
|
self.hass.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 _send_time_changed(self, now):
|
||||||
|
"""Send a time changed event."""
|
||||||
|
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
@callback
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_custom_setup_params(hass):
|
def test_custom_setup_params(hass):
|
||||||
"""Test the setup with custom parameters."""
|
"""Test the setup with custom parameters."""
|
||||||
|
|
Loading…
Reference in New Issue