diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 6574a4d539660f0084f1c44807cb8e10c6ab9ae9..fdfe56ca62c2af1700bc2bf649a6ef32078a4538 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -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() diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 63bbce2e7c6b08ce519e01311509abd48d8c0e7e..776e79a6827acf6fef01090627b433867545097c 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -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")