diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index f9ffe4faec9..a5e95ecc36b 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -21,9 +21,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, - TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS) - + ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, + STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, + PRECISION_TENTHS, ) DOMAIN = 'climate' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -63,6 +63,7 @@ SUPPORT_HOLD_MODE = 256 SUPPORT_SWING_MODE = 512 SUPPORT_AWAY_MODE = 1024 SUPPORT_AUX_HEAT = 2048 +SUPPORT_ON_OFF = 4096 ATTR_CURRENT_TEMPERATURE = 'current_temperature' ATTR_MAX_TEMP = 'max_temp' @@ -92,6 +93,10 @@ CONVERTIBLE_ATTRIBUTE = [ _LOGGER = logging.getLogger(__name__) +ON_OFF_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + SET_AWAY_MODE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_AWAY_MODE): cv.boolean, @@ -439,6 +444,32 @@ def async_setup(hass, config): descriptions.get(SERVICE_SET_SWING_MODE), schema=SET_SWING_MODE_SCHEMA) + @asyncio.coroutine + def async_on_off_service(service): + """Handle on/off calls.""" + target_climate = component.async_extract_from_service(service) + + update_tasks = [] + for climate in target_climate: + if service.service == SERVICE_TURN_ON: + yield from climate.async_turn_on() + elif service.service == SERVICE_TURN_OFF: + yield from climate.async_turn_off() + + if not climate.should_poll: + continue + update_tasks.append(climate.async_update_ha_state(True)) + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + hass.services.async_register( + DOMAIN, SERVICE_TURN_OFF, async_on_off_service, + descriptions.get(SERVICE_TURN_OFF), schema=ON_OFF_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TURN_ON, async_on_off_service, + descriptions.get(SERVICE_TURN_ON), schema=ON_OFF_SERVICE_SCHEMA) + return True @@ -449,8 +480,12 @@ class ClimateDevice(Entity): @property def state(self): """Return the current state.""" + if self.is_on is False: + return STATE_OFF if self.current_operation: return self.current_operation + if self.is_on: + return STATE_ON return STATE_UNKNOWN @property @@ -594,6 +629,11 @@ class ClimateDevice(Entity): """Return the current hold mode, e.g., home, away, temp.""" return None + @property + def is_on(self): + """Return true if on.""" + return None + @property def is_aux_heat_on(self): """Return true if aux heater.""" @@ -730,6 +770,28 @@ class ClimateDevice(Entity): """ return self.hass.async_add_job(self.turn_aux_heat_off) + def turn_on(self): + """Turn device on.""" + raise NotImplementedError() + + def async_turn_on(self): + """Turn device on. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.turn_on) + + def turn_off(self): + """Turn device off.""" + raise NotImplementedError() + + def async_turn_off(self): + """Turn device off. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.turn_off) + @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 4c4b57d42a3..981c551d69b 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -9,14 +9,15 @@ from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + SUPPORT_ON_OFF) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE | SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT | SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_ON_OFF) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -56,6 +57,7 @@ class DemoClimate(ClimateDevice): self._swing_list = ['Auto', '1', '2', '3', 'Off'] self._target_temperature_high = target_temp_high self._target_temperature_low = target_temp_low + self._on = True @property def supported_features(self): @@ -132,6 +134,11 @@ class DemoClimate(ClimateDevice): """Return true if aux heat is on.""" return self._aux + @property + def is_on(self): + """Return true if the device is on.""" + return self._on + @property def current_fan_mode(self): """Return the fan setting.""" @@ -206,3 +213,13 @@ class DemoClimate(ClimateDevice): """Turn auxiliary heater off.""" self._aux = False self.schedule_update_ha_state() + + def turn_on(self): + """Turn on.""" + self._on = True + self.schedule_update_ha_state() + + def turn_off(self): + """Turn off.""" + self._on = False + self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index ed23d91587c..32a5a998d87 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -13,13 +13,12 @@ import async_timeout import voluptuous as vol from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, STATE_OFF, TEMP_CELSIUS, - TEMP_FAHRENHEIT) + ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT) from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_AUX_HEAT) + SUPPORT_AUX_HEAT, SUPPORT_ON_OFF) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -47,7 +46,7 @@ FIELD_TO_FLAG = { 'mode': SUPPORT_OPERATION_MODE, 'swing': SUPPORT_SWING_MODE, 'targetTemperature': SUPPORT_TARGET_TEMPERATURE, - 'on': SUPPORT_AUX_HEAT, + 'on': SUPPORT_AUX_HEAT | SUPPORT_ON_OFF, } @@ -92,13 +91,6 @@ class SensiboClimate(ClimateDevice): """Return the list of supported features.""" return self._supported_features - @property - def state(self): - """Return the current state.""" - if not self.is_aux_heat_on: - return STATE_OFF - return super().state - def _do_update(self, data): self._name = data['room']['name'] self._measurements = data['measurements'] @@ -208,6 +200,8 @@ class SensiboClimate(ClimateDevice): """Return true if AC is on.""" return self._ac_states['on'] + is_on = is_aux_heat_on + @property def min_temp(self): """Return the minimum temperature.""" @@ -279,6 +273,9 @@ class SensiboClimate(ClimateDevice): yield from self._client.async_set_ac_state_property( self._id, 'on', False) + async_on = async_turn_aux_heat_on + async_off = async_turn_aux_heat_off + @asyncio.coroutine def async_update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 5edbf438328..cb3db926b85 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -80,7 +80,22 @@ set_swing_mode: example: 'climate.nest' swing_mode: description: New value of swing mode. - example: 1 + example: + +turn_on: + description: Turn climate device on. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + +turn_off: + description: Turn climate device off. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + ecobee_set_fan_min_on_time: description: Set the minimum fan on time. fields: diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index d15249d61f3..9098494bf48 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -250,3 +250,20 @@ class TestDemoClimate(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) + + def test_set_on_off(self): + """Test on/off service.""" + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual('auto', state.state) + + self.hass.services.call(climate.DOMAIN, climate.SERVICE_TURN_OFF, + {climate.ATTR_ENTITY_ID: ENTITY_ECOBEE}) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual('off', state.state) + + self.hass.services.call(climate.DOMAIN, climate.SERVICE_TURN_ON, + {climate.ATTR_ENTITY_ID: ENTITY_ECOBEE}) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual('auto', state.state)