Updated generic thermostat to respect operation_mode and added away mode (#11445)
* Updated generic thermostat to respect operation_mode and added away mode * Updated tests to include away mode and corrected init problem for sensor state Added more tests to improve coverage and corrected again lint errors Fixed new test by moving to correct package Fixed bug not restoring away mode on restart * Added support for idle on interface through state * Added back initial_operation_mode and modified away_temp to be only one for now * Fixed houndci-bot errors * Added back check for None on restore temperature * Fixed failing tests as well * Removed unused definitions from tests * Added use case for no initial temperature and no previously saved temperature
This commit is contained in:
parent
04de22613c
commit
3436676de2
2 changed files with 149 additions and 57 deletions
|
@ -12,9 +12,9 @@ import voluptuous as vol
|
|||
from homeassistant.core import callback
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
STATE_AUTO, ATTR_OPERATION_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE)
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
|
||||
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
|
@ -30,6 +30,7 @@ DEPENDENCIES = ['switch', 'sensor']
|
|||
|
||||
DEFAULT_TOLERANCE = 0.3
|
||||
DEFAULT_NAME = 'Generic Thermostat'
|
||||
DEFAULT_AWAY_TEMP = 16
|
||||
|
||||
CONF_HEATER = 'heater'
|
||||
CONF_SENSOR = 'target_sensor'
|
||||
|
@ -42,7 +43,9 @@ CONF_COLD_TOLERANCE = 'cold_tolerance'
|
|||
CONF_HOT_TOLERANCE = 'hot_tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
CONF_AWAY_TEMP = 'away_temp'
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HEATER): cv.entity_id,
|
||||
|
@ -60,7 +63,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_KEEP_ALIVE): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_INITIAL_OPERATION_MODE):
|
||||
vol.In([STATE_AUTO, STATE_OFF])
|
||||
vol.In([STATE_AUTO, STATE_OFF]),
|
||||
vol.Optional(CONF_AWAY_TEMP,
|
||||
default=DEFAULT_AWAY_TEMP): vol.Coerce(float)
|
||||
})
|
||||
|
||||
|
||||
|
@ -79,11 +84,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
hot_tolerance = config.get(CONF_HOT_TOLERANCE)
|
||||
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
|
||||
away_temp = config.get(CONF_AWAY_TEMP)
|
||||
|
||||
async_add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, cold_tolerance,
|
||||
hot_tolerance, keep_alive, initial_operation_mode)])
|
||||
hot_tolerance, keep_alive, initial_operation_mode, away_temp)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
|
@ -92,7 +98,7 @@ class GenericThermostat(ClimateDevice):
|
|||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
cold_tolerance, hot_tolerance, keep_alive,
|
||||
initial_operation_mode):
|
||||
initial_operation_mode, away_temp):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
|
@ -103,17 +109,26 @@ class GenericThermostat(ClimateDevice):
|
|||
self._hot_tolerance = hot_tolerance
|
||||
self._keep_alive = keep_alive
|
||||
self._initial_operation_mode = initial_operation_mode
|
||||
self._saved_target_temp = target_temp if target_temp is not None \
|
||||
else away_temp
|
||||
if self.ac_mode:
|
||||
self._current_operation = STATE_COOL
|
||||
self._operation_list = [STATE_COOL, STATE_OFF]
|
||||
else:
|
||||
self._current_operation = STATE_HEAT
|
||||
self._operation_list = [STATE_HEAT, STATE_OFF]
|
||||
if initial_operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
else:
|
||||
self._enabled = True
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = target_temp
|
||||
self._unit = hass.config.units.temperature_unit
|
||||
self._away_temp = away_temp
|
||||
self._is_away = False
|
||||
|
||||
async_track_state_change(
|
||||
hass, sensor_entity_id, self._async_sensor_changed)
|
||||
|
@ -124,10 +139,6 @@ class GenericThermostat(ClimateDevice):
|
|||
async_track_time_interval(
|
||||
hass, self._async_keep_alive, self._keep_alive)
|
||||
|
||||
sensor_state = hass.states.get(sensor_entity_id)
|
||||
if sensor_state:
|
||||
self._async_update_temp(sensor_state)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
|
@ -137,14 +148,37 @@ class GenericThermostat(ClimateDevice):
|
|||
if old_state is not None:
|
||||
# If we have no initial temperature, restore
|
||||
if self._target_temp is None:
|
||||
self._target_temp = float(
|
||||
old_state.attributes[ATTR_TEMPERATURE])
|
||||
|
||||
# If we have no initial operation mode, restore
|
||||
# If we have a previously saved temperature
|
||||
if old_state.attributes[ATTR_TEMPERATURE] is None:
|
||||
if self.ac_mode:
|
||||
self._target_temp = self.max_temp
|
||||
else:
|
||||
self._target_temp = self.min_temp
|
||||
_LOGGER.warning('Undefined target temperature, \
|
||||
falling back to %s', self._target_temp)
|
||||
else:
|
||||
self._target_temp = float(
|
||||
old_state.attributes[ATTR_TEMPERATURE])
|
||||
self._is_away = True if str(
|
||||
old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON else False
|
||||
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
|
||||
self._current_operation = STATE_OFF
|
||||
self._enabled = False
|
||||
if self._initial_operation_mode is None:
|
||||
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
|
||||
self._enabled = False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
if self._is_device_active:
|
||||
return self.current_operation
|
||||
else:
|
||||
if self._enabled:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
|
@ -167,15 +201,8 @@ class GenericThermostat(ClimateDevice):
|
|||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self._enabled:
|
||||
return STATE_OFF
|
||||
if self.ac_mode:
|
||||
cooling = self._active and self._is_device_active
|
||||
return STATE_COOL if cooling else STATE_IDLE
|
||||
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
"""Return current operation."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
|
@ -185,14 +212,20 @@ class GenericThermostat(ClimateDevice):
|
|||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_AUTO, STATE_OFF]
|
||||
return self._operation_list
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_AUTO:
|
||||
if operation_mode == STATE_HEAT:
|
||||
self._current_operation = STATE_HEAT
|
||||
self._enabled = True
|
||||
self._async_control_heating()
|
||||
elif operation_mode == STATE_COOL:
|
||||
self._current_operation = STATE_COOL
|
||||
self._enabled = True
|
||||
self._async_control_heating()
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._current_operation = STATE_OFF
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
self._heater_turn_off()
|
||||
|
@ -252,7 +285,7 @@ class GenericThermostat(ClimateDevice):
|
|||
@callback
|
||||
def _async_keep_alive(self, time):
|
||||
"""Call at constant intervals for keep-alive purposes."""
|
||||
if self.current_operation in [STATE_COOL, STATE_HEAT]:
|
||||
if self._is_device_active:
|
||||
self._heater_turn_on()
|
||||
else:
|
||||
self._heater_turn_off()
|
||||
|
@ -347,3 +380,23 @@ class GenericThermostat(ClimateDevice):
|
|||
data = {ATTR_ENTITY_ID: self.heater_entity_id}
|
||||
self.hass.async_add_job(
|
||||
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on by setting it on away hold indefinitely."""
|
||||
self._is_away = True
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._target_temp = self._away_temp
|
||||
self._async_control_heating()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self._is_away = False
|
||||
self._target_temp = self._saved_target_temp
|
||||
self._async_control_heating()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -29,6 +29,7 @@ from tests.common import (assert_setup_component, get_test_home_assistant,
|
|||
ENTITY = 'climate.test'
|
||||
ENT_SENSOR = 'sensor.test'
|
||||
ENT_SWITCH = 'switch.test'
|
||||
ATTR_AWAY_MODE = 'away_mode'
|
||||
MIN_TEMP = 3.0
|
||||
MAX_TEMP = 65.0
|
||||
TARGET_TEMP = 42.0
|
||||
|
@ -69,22 +70,6 @@ class TestSetupClimateGenericThermostat(unittest.TestCase):
|
|||
}})
|
||||
)
|
||||
|
||||
def test_setup_with_sensor(self):
|
||||
"""Test set up heat_control with sensor to trigger update at init."""
|
||||
self.hass.states.set(ENT_SENSOR, 22.0, {
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS
|
||||
})
|
||||
assert setup_component(self.hass, climate.DOMAIN, {'climate': {
|
||||
'platform': 'generic_thermostat',
|
||||
'name': 'test',
|
||||
'heater': ENT_SWITCH,
|
||||
'target_sensor': ENT_SENSOR
|
||||
}})
|
||||
state = self.hass.states.get(ENTITY)
|
||||
self.assertEqual(
|
||||
TEMP_CELSIUS, state.attributes.get('unit_of_measurement'))
|
||||
self.assertEqual(22.0, state.attributes.get('current_temperature'))
|
||||
|
||||
|
||||
class TestGenericThermostatHeaterSwitching(unittest.TestCase):
|
||||
"""Test the Generic thermostat heater switching.
|
||||
|
@ -197,7 +182,7 @@ class TestClimateGenericThermostat(unittest.TestCase):
|
|||
"""Test that the operation list returns the correct modes."""
|
||||
state = self.hass.states.get(ENTITY)
|
||||
modes = state.attributes.get('operation_list')
|
||||
self.assertEqual([climate.STATE_AUTO, STATE_OFF], modes)
|
||||
self.assertEqual([climate.STATE_HEAT, STATE_OFF], modes)
|
||||
|
||||
def test_set_target_temp(self):
|
||||
"""Test the setting of the target temperature."""
|
||||
|
@ -210,6 +195,31 @@ class TestClimateGenericThermostat(unittest.TestCase):
|
|||
state = self.hass.states.get(ENTITY)
|
||||
self.assertEqual(30.0, state.attributes.get('temperature'))
|
||||
|
||||
def test_set_away_mode(self):
|
||||
"""Test the setting away mode."""
|
||||
climate.set_temperature(self.hass, 23)
|
||||
self.hass.block_till_done()
|
||||
climate.set_away_mode(self.hass, True)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY)
|
||||
self.assertEqual(16, state.attributes.get('temperature'))
|
||||
|
||||
def test_set_away_mode_and_restore_prev_temp(self):
|
||||
"""Test the setting and removing away mode.
|
||||
|
||||
Verify original temperature is restored.
|
||||
"""
|
||||
climate.set_temperature(self.hass, 23)
|
||||
self.hass.block_till_done()
|
||||
climate.set_away_mode(self.hass, True)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY)
|
||||
self.assertEqual(16, state.attributes.get('temperature'))
|
||||
climate.set_away_mode(self.hass, False)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY)
|
||||
self.assertEqual(23, state.attributes.get('temperature'))
|
||||
|
||||
def test_sensor_bad_unit(self):
|
||||
"""Test sensor that have bad unit."""
|
||||
state = self.hass.states.get(ENTITY)
|
||||
|
@ -337,8 +347,8 @@ class TestClimateGenericThermostat(unittest.TestCase):
|
|||
self.hass.block_till_done()
|
||||
self.assertEqual(log_mock.call_count, 1)
|
||||
|
||||
def test_operating_mode_auto(self):
|
||||
"""Test change mode from OFF to AUTO.
|
||||
def test_operating_mode_heat(self):
|
||||
"""Test change mode from OFF to HEAT.
|
||||
|
||||
Switch turns on when temp below setpoint and mode changes.
|
||||
"""
|
||||
|
@ -347,7 +357,7 @@ class TestClimateGenericThermostat(unittest.TestCase):
|
|||
self._setup_sensor(25)
|
||||
self.hass.block_till_done()
|
||||
self._setup_switch(False)
|
||||
climate.set_operation_mode(self.hass, climate.STATE_AUTO)
|
||||
climate.set_operation_mode(self.hass, climate.STATE_HEAT)
|
||||
self.hass.block_till_done()
|
||||
self.assertEqual(1, len(self.calls))
|
||||
call = self.calls[0]
|
||||
|
@ -387,6 +397,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase):
|
|||
'name': 'test',
|
||||
'cold_tolerance': 2,
|
||||
'hot_tolerance': 4,
|
||||
'away_temp': 30,
|
||||
'heater': ENT_SWITCH,
|
||||
'target_sensor': ENT_SENSOR,
|
||||
'ac_mode': True
|
||||
|
@ -409,6 +420,35 @@ class TestClimateGenericThermostatACMode(unittest.TestCase):
|
|||
self.assertEqual(SERVICE_TURN_OFF, call.service)
|
||||
self.assertEqual(ENT_SWITCH, call.data['entity_id'])
|
||||
|
||||
def test_turn_away_mode_on_cooling(self):
|
||||
"""Test the setting away mode when cooling."""
|
||||
self._setup_sensor(25)
|
||||
self.hass.block_till_done()
|
||||
climate.set_temperature(self.hass, 19)
|
||||
self.hass.block_till_done()
|
||||
climate.set_away_mode(self.hass, True)
|
||||
self.hass.block_till_done()
|
||||
state = self.hass.states.get(ENTITY)
|
||||
self.assertEqual(30, state.attributes.get('temperature'))
|
||||
|
||||
def test_operating_mode_cool(self):
|
||||
"""Test change mode from OFF to COOL.
|
||||
|
||||
Switch turns on when temp below setpoint and mode changes.
|
||||
"""
|
||||
climate.set_operation_mode(self.hass, STATE_OFF)
|
||||
climate.set_temperature(self.hass, 25)
|
||||
self._setup_sensor(30)
|
||||
self.hass.block_till_done()
|
||||
self._setup_switch(False)
|
||||
climate.set_operation_mode(self.hass, climate.STATE_COOL)
|
||||
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 test_set_target_temp_ac_on(self):
|
||||
"""Test if target temperature turn ac on."""
|
||||
self._setup_switch(False)
|
||||
|
@ -891,15 +931,13 @@ def test_custom_setup_params(hass):
|
|||
'target_sensor': ENT_SENSOR,
|
||||
'min_temp': MIN_TEMP,
|
||||
'max_temp': MAX_TEMP,
|
||||
'target_temp': TARGET_TEMP,
|
||||
'initial_operation_mode': STATE_OFF,
|
||||
'target_temp': TARGET_TEMP
|
||||
}})
|
||||
assert result
|
||||
state = hass.states.get(ENTITY)
|
||||
assert state.attributes.get('min_temp') == MIN_TEMP
|
||||
assert state.attributes.get('max_temp') == MAX_TEMP
|
||||
assert state.attributes.get('temperature') == TARGET_TEMP
|
||||
assert state.attributes.get(climate.ATTR_OPERATION_MODE) == STATE_OFF
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -907,7 +945,7 @@ def test_restore_state(hass):
|
|||
"""Ensure states are restored on startup."""
|
||||
mock_restore_cache(hass, (
|
||||
State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20",
|
||||
climate.ATTR_OPERATION_MODE: "off"}),
|
||||
climate.ATTR_OPERATION_MODE: "off", ATTR_AWAY_MODE: "on"}),
|
||||
))
|
||||
|
||||
hass.state = CoreState.starting
|
||||
|
@ -927,10 +965,13 @@ def test_restore_state(hass):
|
|||
|
||||
@asyncio.coroutine
|
||||
def test_no_restore_state(hass):
|
||||
"""Ensure states are not restored on startup if not needed."""
|
||||
"""Ensure states are restored on startup if they exist.
|
||||
|
||||
Allows for graceful reboot.
|
||||
"""
|
||||
mock_restore_cache(hass, (
|
||||
State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20",
|
||||
climate.ATTR_OPERATION_MODE: "off"}),
|
||||
climate.ATTR_OPERATION_MODE: "off", ATTR_AWAY_MODE: "on"}),
|
||||
))
|
||||
|
||||
hass.state = CoreState.starting
|
||||
|
@ -941,10 +982,8 @@ def test_no_restore_state(hass):
|
|||
'name': 'test_thermostat',
|
||||
'heater': ENT_SWITCH,
|
||||
'target_sensor': ENT_SENSOR,
|
||||
'target_temp': 22,
|
||||
'initial_operation_mode': 'auto',
|
||||
'target_temp': 22
|
||||
}})
|
||||
|
||||
state = hass.states.get('climate.test_thermostat')
|
||||
assert(state.attributes[ATTR_TEMPERATURE] == 22)
|
||||
assert(state.attributes[climate.ATTR_OPERATION_MODE] != "off")
|
||||
|
|
Loading…
Add table
Reference in a new issue