diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 3058258c75a..0cd9bbe17d3 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -32,6 +32,7 @@ SERVICE_SET_AWAY_MODE = "set_away_mode" SERVICE_SET_AUX_HEAT = "set_aux_heat" SERVICE_SET_TEMPERATURE = "set_temperature" SERVICE_SET_FAN_MODE = "set_fan_mode" +SERVICE_SET_HOLD_MODE = "set_hold_mode" SERVICE_SET_OPERATION_MODE = "set_operation_mode" SERVICE_SET_SWING_MODE = "set_swing_mode" SERVICE_SET_HUMIDITY = "set_humidity" @@ -56,6 +57,7 @@ ATTR_CURRENT_HUMIDITY = "current_humidity" ATTR_HUMIDITY = "humidity" ATTR_MAX_HUMIDITY = "max_humidity" ATTR_MIN_HUMIDITY = "min_humidity" +ATTR_HOLD_MODE = "hold_mode" ATTR_OPERATION_MODE = "operation_mode" ATTR_OPERATION_LIST = "operation_list" ATTR_SWING_MODE = "swing_mode" @@ -93,6 +95,10 @@ SET_FAN_MODE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_FAN_MODE): cv.string, }) +SET_HOLD_MODE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_HOLD_MODE): cv.string, +}) SET_OPERATION_MODE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_OPERATION_MODE): cv.string, @@ -116,9 +122,23 @@ def set_away_mode(hass, away_mode, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id + _LOGGER.warning( + 'This service has been deprecated; use climate.set_hold_mode') hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) +def set_hold_mode(hass, hold_mode, entity_id=None): + """Set new hold mode.""" + data = { + ATTR_HOLD_MODE: hold_mode + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data) + + def set_aux_heat(hass, aux_heat, entity_id=None): """Turn all or specified climate devices auxillary heater on.""" data = { @@ -229,6 +249,8 @@ def async_setup(hass, config): SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE) return + _LOGGER.warning( + 'This service has been deprecated; use climate.set_hold_mode') for climate in target_climate: if away_mode: yield from climate.async_turn_away_mode_on() @@ -242,6 +264,23 @@ def async_setup(hass, config): descriptions.get(SERVICE_SET_AWAY_MODE), schema=SET_AWAY_MODE_SCHEMA) + @asyncio.coroutine + def async_hold_mode_set_service(service): + """Set hold mode on target climate devices.""" + target_climate = component.async_extract_from_service(service) + + hold_mode = service.data.get(ATTR_HOLD_MODE) + + for climate in target_climate: + yield from climate.async_set_hold_mode(hold_mode) + + yield from _async_update_climate(target_climate) + + hass.services.async_register( + DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service, + descriptions.get(SERVICE_SET_HOLD_MODE), + schema=SET_HOLD_MODE_SCHEMA) + @asyncio.coroutine def async_aux_heat_set_service(service): """Set auxillary heater on target climate devices.""" @@ -446,6 +485,10 @@ class ClimateDevice(Entity): if self.operation_list: data[ATTR_OPERATION_LIST] = self.operation_list + is_hold = self.current_hold_mode + if is_hold is not None: + data[ATTR_HOLD_MODE] = is_hold + swing_mode = self.current_swing_mode if swing_mode is not None: data[ATTR_SWING_MODE] = swing_mode @@ -517,6 +560,11 @@ class ClimateDevice(Entity): """Return true if away mode is on.""" return None + @property + def current_hold_mode(self): + """Return the current hold mode, e.g., home, away, temp.""" + return None + @property def is_aux_heat_on(self): """Return true if aux heater.""" @@ -626,6 +674,18 @@ class ClimateDevice(Entity): return self.hass.loop.run_in_executor( None, self.turn_away_mode_off) + def set_hold_mode(self, hold_mode): + """Set new target hold mode.""" + raise NotImplementedError() + + def async_set_hold_mode(self, hold_mode): + """Set new target hold mode. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, self.set_hold_mode, hold_mode) + def turn_aux_heat_on(self): """Turn auxillary heater on.""" raise NotImplementedError() diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 04053febf90..a66873cbc63 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -12,11 +12,11 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Demo climate devices.""" add_devices([ - DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low", - None, None, "Auto", "heat", None, None, None), - DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High", + DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, None, 77, + "Auto Low", None, None, "Auto", "heat", None, None, None), + DemoClimate("Hvac", 21, TEMP_CELSIUS, True, None, 22, "On High", 67, 54, "Off", "cool", False, None, None), - DemoClimate("Ecobee", None, TEMP_CELSIUS, None, 23, "Auto Low", + DemoClimate("Ecobee", None, TEMP_CELSIUS, None, None, 23, "Auto Low", None, None, "Auto", "auto", None, 24, 21) ]) @@ -25,7 +25,7 @@ class DemoClimate(ClimateDevice): """Representation of a demo climate device.""" def __init__(self, name, target_temperature, unit_of_measurement, - away, current_temperature, current_fan_mode, + away, hold, current_temperature, current_fan_mode, target_humidity, current_humidity, current_swing_mode, current_operation, aux, target_temp_high, target_temp_low): """Initialize the climate device.""" @@ -34,6 +34,7 @@ class DemoClimate(ClimateDevice): self._target_humidity = target_humidity self._unit_of_measurement = unit_of_measurement self._away = away + self._hold = hold self._current_temperature = current_temperature self._current_humidity = current_humidity self._current_fan_mode = current_fan_mode @@ -106,6 +107,11 @@ class DemoClimate(ClimateDevice): """Return if away mode is on.""" return self._away + @property + def current_hold_mode(self): + """Return hold mode setting.""" + return self._hold + @property def is_aux_heat_on(self): """Return true if away mode is on.""" @@ -171,6 +177,11 @@ class DemoClimate(ClimateDevice): self._away = False self.update_ha_state() + def set_hold_mode(self, hold): + """Update hold mode on.""" + self._hold = hold + self.update_ha_state() + def turn_aux_heat_on(self): """Turn away auxillary heater on.""" self._aux = True diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index f820d69754d..dcee6d9ce31 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -183,6 +183,19 @@ class Thermostat(ClimateDevice): else: return STATE_OFF + @property + def current_hold_mode(self): + """Return current hold mode.""" + if self.is_away_mode_on: + hold = 'away' + elif self.is_home_mode_on: + hold = 'home' + elif self.is_temp_hold_on(): + hold = 'temp' + else: + hold = None + return hold + @property def current_operation(self): """Return current operation.""" @@ -236,30 +249,94 @@ class Thermostat(ClimateDevice): "fan_min_on_time": self.fan_min_on_time } + def is_vacation_on(self): + """Return true if vacation mode is on.""" + events = self.thermostat['events'] + return any(event['type'] == 'vacation' and event['running'] + for event in events) + + def is_temp_hold_on(self): + """Return true if temperature hold is on.""" + events = self.thermostat['events'] + return any(event['type'] == 'hold' and event['running'] + for event in events) + @property def is_away_mode_on(self): """Return true if away mode is on.""" - mode = self.mode events = self.thermostat['events'] - for event in events: - if event['holdClimateRef'] == 'away' or \ - event['type'] == 'autoAway': - mode = "away" - break - return 'away' in mode + return any(event['holdClimateRef'] == 'away' or + event['type'] == 'autoAway' + for event in events) def turn_away_mode_on(self): """Turn away on.""" - if self.hold_temp: - self.data.ecobee.set_climate_hold(self.thermostat_index, - "away", "indefinite") - else: - self.data.ecobee.set_climate_hold(self.thermostat_index, "away") + self.data.ecobee.set_climate_hold(self.thermostat_index, + "away", self.hold_preference()) self.update_without_throttle = True def turn_away_mode_off(self): """Turn away off.""" - self.data.ecobee.resume_program(self.thermostat_index) + self.set_hold_mode(None) + + @property + def is_home_mode_on(self): + """Return true if home mode is on.""" + events = self.thermostat['events'] + return any(event['holdClimateRef'] == 'home' or + event['type'] == 'autoHome' + for event in events) + + def turn_home_mode_on(self): + """Turn home on.""" + self.data.ecobee.set_climate_hold(self.thermostat_index, + "home", self.hold_preference()) + self.update_without_throttle = True + + def set_hold_mode(self, hold_mode): + """Set hold mode (away, home, temp).""" + hold = self.current_hold_mode + + if hold == hold_mode: + return + elif hold_mode == 'away': + self.turn_away_mode_on() + elif hold_mode == 'home': + self.turn_home_mode_on() + elif hold_mode == 'temp': + self.set_temp_hold(int(self.current_temperature)) + else: + self.data.ecobee.resume_program(self.thermostat_index) + self.update_without_throttle = True + + def set_auto_temp_hold(self, heat_temp, cool_temp): + """Set temperature hold in auto mode.""" + self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp, + heat_temp, self.hold_preference()) + _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " + "cool=%s, is=%s", heat_temp, isinstance( + heat_temp, (int, float)), cool_temp, + isinstance(cool_temp, (int, float))) + + self.update_without_throttle = True + + def set_temp_hold(self, temp): + """Set temperature hold in modes other than auto.""" + # Set arbitrary range when not in auto mode + if self.current_operation == STATE_HEAT: + heat_temp = temp + cool_temp = temp + 20 + elif self.current_operation == STATE_COOL: + heat_temp = temp - 20 + cool_temp = temp + + self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp, + heat_temp, self.hold_preference()) + _LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, " + "cool=%s, is=%s", heat_temp, isinstance( + heat_temp, (int, float)), cool_temp, + isinstance(cool_temp, (int, float))) + self.update_without_throttle = True def set_temperature(self, **kwargs): @@ -268,33 +345,14 @@ class Thermostat(ClimateDevice): high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) temp = kwargs.get(ATTR_TEMPERATURE) - if self.current_operation == STATE_HEAT and temp is not None: - low_temp = temp - high_temp = temp + 20 - elif self.current_operation == STATE_COOL and temp is not None: - low_temp = temp - 20 - high_temp = temp - if low_temp is None and high_temp is None: + if self.current_operation == STATE_AUTO and low_temp is not None \ + and high_temp is not None: + self.set_auto_temp_hold(int(low_temp), int(high_temp)) + elif temp is not None: + self.set_temp_hold(int(temp)) + else: _LOGGER.error( 'Missing valid arguments for set_temperature in %s', kwargs) - return - - low_temp = int(low_temp) - high_temp = int(high_temp) - - if self.hold_temp: - self.data.ecobee.set_hold_temp( - self.thermostat_index, high_temp, low_temp, "indefinite") - else: - self.data.ecobee.set_hold_temp( - self.thermostat_index, high_temp, low_temp) - - _LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, " - "high=%s, is=%s", low_temp, isinstance( - low_temp, (int, float)), high_temp, - isinstance(high_temp, (int, float))) - - self.update_without_throttle = True def set_operation_mode(self, operation_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" @@ -313,15 +371,19 @@ class Thermostat(ClimateDevice): str(resume_all).lower()) self.update_without_throttle = True - # Home and Sleep mode aren't used in UI yet: + def hold_preference(self): + """Return user preference setting for hold time.""" + # Values returned from thermostat are 'useEndTime4hour', + # 'useEndTime2hour', 'nextTransition', 'indefinite', 'askMe' + default = self.thermostat['settings']['holdAction'] + if default == 'nextTransition': + return default + elif default == 'indefinite': + return default + else: + return 'nextTransition' - # def turn_home_mode_on(self): - # """ Turns home mode on. """ - # self.data.ecobee.set_climate_hold(self.thermostat_index, "home") - - # def turn_home_mode_off(self): - # """ Turns home mode off. """ - # self.data.ecobee.resume_program(self.thermostat_index) + # Sleep mode isn't used in UI yet: # def turn_sleep_mode_on(self): # """ Turns sleep mode on. """ diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 801052b31ff..899a3dcfe33 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -22,6 +22,18 @@ set_away_mode: description: New value of away mode example: true +set_hold_mode: + description: Turn hold mode for climate device + + fields: + entity_id: + description: Name(s) of entities to change + example: 'climate.kitchen' + + hold_mode: + description: New value of hold mode + example: 'away' + set_temperature: description: Set target temperature of climate device diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 3be344e7d9d..ea33d27a814 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -16,11 +16,11 @@ from homeassistant.components.sun import ( from homeassistant.components.switch.mysensors import ( ATTR_IR_CODE, SERVICE_SEND_IR_CODE) from homeassistant.components.climate import ( - ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HUMIDITY, - ATTR_OPERATION_MODE, ATTR_SWING_MODE, - SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE, - SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE, SERVICE_SET_SWING_MODE, - SERVICE_SET_TEMPERATURE) + ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE, + ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, + SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE, + SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) from homeassistant.components.climate.ecobee import ( ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME, ATTR_RESUME_ALL, SERVICE_RESUME_PROGRAM) @@ -57,6 +57,7 @@ SERVICE_ATTRIBUTES = { SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE], SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY], SERVICE_SET_SWING_MODE: [ATTR_SWING_MODE], + SERVICE_SET_HOLD_MODE: [ATTR_HOLD_MODE], SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE], SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT], SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 518e4ca2c81..898f6ba2df6 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -208,6 +208,27 @@ class TestDemoClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) + def test_set_hold_mode_home(self): + """Test setting the hold mode home.""" + climate.set_hold_mode(self.hass, 'home', ENTITY_ECOBEE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual('home', state.attributes.get('hold_mode')) + + def test_set_hold_mode_away(self): + """Test setting the hold mode away.""" + climate.set_hold_mode(self.hass, 'away', ENTITY_ECOBEE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual('away', state.attributes.get('hold_mode')) + + def test_set_hold_mode_none(self): + """Test setting the hold mode off/false.""" + climate.set_hold_mode(self.hass, None, ENTITY_ECOBEE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual(None, state.attributes.get('hold_mode')) + def test_set_aux_heat_bad_attr(self): """Test setting the auxillary heater without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE)