diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 369ef6fc838..ba6f15567d9 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -2,13 +2,13 @@ from datetime import timedelta import functools as ft import logging -from typing import Any, Awaitable, Dict, List, Optional +from typing import Any, Dict, List, Optional import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, PRECISION_TENTHS, PRECISION_WHOLE, - STATE_OFF, STATE_ON, TEMP_CELSIUS) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) @@ -25,7 +25,8 @@ from .const import ( ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_PRESET_MODES, ATTR_SWING_MODE, ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, DOMAIN, HVAC_MODES, + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, DOMAIN, HVAC_MODE_COOL, + HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODES, SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, @@ -49,6 +50,9 @@ CONVERTIBLE_ATTRIBUTE = [ _LOGGER = logging.getLogger(__name__) +TURN_ON_OFF_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, +}) SET_AUX_HEAT_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_AUX_HEAT): cv.boolean, @@ -92,6 +96,14 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) + component.async_register_entity_service( + SERVICE_TURN_ON, TURN_ON_OFF_SCHEMA, + 'async_turn_on' + ) + component.async_register_entity_service( + SERVICE_TURN_OFF, TURN_ON_OFF_SCHEMA, + 'async_turn_off' + ) component.async_register_entity_service( SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA, 'async_set_hvac_mode' @@ -338,90 +350,92 @@ class ClimateDevice(Entity): """Set new target temperature.""" raise NotImplementedError() - def async_set_temperature(self, **kwargs) -> Awaitable[None]: - """Set new target temperature. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job( + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + await self.hass.async_add_executor_job( ft.partial(self.set_temperature, **kwargs)) def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" raise NotImplementedError() - def async_set_humidity(self, humidity: int) -> Awaitable[None]: - """Set new target humidity. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_humidity, humidity) + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + await self.hass.async_add_executor_job(self.set_humidity, humidity) def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" raise NotImplementedError() - def async_set_fan_mode(self, fan_mode: str) -> Awaitable[None]: - """Set new target fan mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_fan_mode, fan_mode) + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + await self.hass.async_add_executor_job(self.set_fan_mode, fan_mode) def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" raise NotImplementedError() - def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: - """Set new target hvac mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_hvac_mode, hvac_mode) + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.hass.async_add_executor_job(self.set_hvac_mode, hvac_mode) def set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" raise NotImplementedError() - def async_set_swing_mode(self, swing_mode: str) -> Awaitable[None]: - """Set new target swing operation. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_swing_mode, swing_mode) + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing operation.""" + await self.hass.async_add_executor_job(self.set_swing_mode, swing_mode) def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" raise NotImplementedError() - def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: - """Set new preset mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_preset_mode, preset_mode) + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.hass.async_add_executor_job( + self.set_preset_mode, preset_mode) def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" raise NotImplementedError() - def async_turn_aux_heat_on(self) -> Awaitable[None]: - """Turn auxiliary heater on. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.turn_aux_heat_on) + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_on) def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" raise NotImplementedError() - def async_turn_aux_heat_off(self) -> Awaitable[None]: - """Turn auxiliary heater off. + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_off) - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.turn_aux_heat_off) + async def async_turn_on(self) -> None: + """Turn the entity on.""" + if hasattr(self, 'turn_on'): + # pylint: disable=no-member + await self.hass.async_add_executor_job(self.turn_on) + return + + # Fake turn on + for mode in (HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_COOL): + if mode not in self.hvac_modes: + continue + await self.async_set_hvac_mode(mode) + break + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + if hasattr(self, 'turn_off'): + # pylint: disable=no-member + await self.hass.async_add_executor_job(self.turn_off) + return + + # Fake turn off + if HVAC_MODE_OFF in self.hvac_modes: + await self.async_set_hvac_mode(HVAC_MODE_OFF) @property def supported_features(self) -> int: diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 8969f60cd89..4e9a4a3a4f4 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -114,3 +114,17 @@ nuheat_resume_program: entity_id: description: Name(s) of entities to change. example: 'climate.kitchen' + +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' diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 33c42ee1eed..0279f356058 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -10,7 +10,8 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON) from homeassistant.loader import bind_hass @@ -188,3 +189,25 @@ def set_swing_mode(hass, swing_mode, entity_id=None): data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) + + +async def async_turn_on(hass, entity_id=None): + """Turn on device.""" + data = {} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + +async def async_turn_off(hass, entity_id=None): + """Turn off device.""" + data = {} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data, blocking=True) diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 744e579a5bc..0c1b7f1ecc0 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,9 +1,11 @@ """The tests for the climate component.""" +from unittest.mock import MagicMock import pytest import voluptuous as vol -from homeassistant.components.climate import SET_TEMPERATURE_SCHEMA +from homeassistant.components.climate import ( + SET_TEMPERATURE_SCHEMA, ClimateDevice) from tests.common import async_mock_service @@ -37,3 +39,25 @@ async def test_set_temp_schema(hass, caplog): assert len(calls) == 1 assert calls[-1].data == data + + +async def test_sync_turn_on(hass): + """Test if adding turn_on work.""" + climate = ClimateDevice() + climate.hass = hass + + climate.turn_on = MagicMock() + await climate.async_turn_on() + + assert climate.turn_on.called + + +async def test_sync_turn_off(hass): + """Test if adding turn_on work.""" + climate = ClimateDevice() + climate.hass = hass + + climate.turn_off = MagicMock() + await climate.async_turn_off() + + assert climate.turn_off.called diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 44637fa9245..628c9e417b3 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -4,12 +4,12 @@ import pytest import voluptuous as vol from homeassistant.components.climate.const import ( - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_HVAC_ACTIONS, - ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODES, + ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_ACTIONS, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, DOMAIN, - HVAC_MODE_COOL, HVAC_MODE_HEAT, PRESET_AWAY, PRESET_ECO) + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, DOMAIN, HVAC_MODE_COOL, + HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO) from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM @@ -279,3 +279,25 @@ async def test_set_aux_heat_off(hass): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF + + +async def test_turn_on(hass): + """Test turn on device.""" + await common.async_set_hvac_mode(hass, HVAC_MODE_OFF, ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == HVAC_MODE_OFF + + await common.async_turn_on(hass, ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == HVAC_MODE_HEAT + + +async def test_turn_off(hass): + """Test turn on device.""" + await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT, ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == HVAC_MODE_HEAT + + await common.async_turn_off(hass, ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == HVAC_MODE_OFF