Add support for on/off climate ()

* Add support for on/off climate

* address comments

* Add test for sync overwrite

* Add more tests
pull/25090/head
Pascal Vizeli 2019-07-12 00:28:11 +02:00 committed by Paulus Schoutsen
parent f5a4af40ee
commit d47905d119
5 changed files with 153 additions and 56 deletions
homeassistant/components/climate
tests/components

View File

@ -2,13 +2,13 @@
from datetime import timedelta from datetime import timedelta
import functools as ft import functools as ft
import logging import logging
from typing import Any, Awaitable, Dict, List, Optional from typing import Any, Dict, List, Optional
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, PRECISION_TENTHS, PRECISION_WHOLE, 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa from homeassistant.helpers.config_validation import ( # noqa
PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) 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_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP,
ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_PRESET_MODES, ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_PRESET_MODES,
ATTR_SWING_MODE, ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, 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_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY,
SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SERVICE_SET_TEMPERATURE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE,
@ -49,6 +50,9 @@ CONVERTIBLE_ATTRIBUTE = [
_LOGGER = logging.getLogger(__name__) _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({ SET_AUX_HEAT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
vol.Required(ATTR_AUX_HEAT): cv.boolean, 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) EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config) 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( component.async_register_entity_service(
SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA, SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA,
'async_set_hvac_mode' 'async_set_hvac_mode'
@ -338,90 +350,92 @@ class ClimateDevice(Entity):
"""Set new target temperature.""" """Set new target temperature."""
raise NotImplementedError() raise NotImplementedError()
def async_set_temperature(self, **kwargs) -> Awaitable[None]: async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature. """Set new target temperature."""
await self.hass.async_add_executor_job(
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(
ft.partial(self.set_temperature, **kwargs)) ft.partial(self.set_temperature, **kwargs))
def set_humidity(self, humidity: int) -> None: def set_humidity(self, humidity: int) -> None:
"""Set new target humidity.""" """Set new target humidity."""
raise NotImplementedError() raise NotImplementedError()
def async_set_humidity(self, humidity: int) -> Awaitable[None]: async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity. """Set new target humidity."""
await self.hass.async_add_executor_job(self.set_humidity, humidity)
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.set_humidity, humidity)
def set_fan_mode(self, fan_mode: str) -> None: def set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode.""" """Set new target fan mode."""
raise NotImplementedError() raise NotImplementedError()
def async_set_fan_mode(self, fan_mode: str) -> Awaitable[None]: async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode. """Set new target fan mode."""
await self.hass.async_add_executor_job(self.set_fan_mode, 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)
def set_hvac_mode(self, hvac_mode: str) -> None: def set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode.""" """Set new target hvac mode."""
raise NotImplementedError() raise NotImplementedError()
def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode. """Set new target hvac mode."""
await self.hass.async_add_executor_job(self.set_hvac_mode, 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)
def set_swing_mode(self, swing_mode: str) -> None: def set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation.""" """Set new target swing operation."""
raise NotImplementedError() raise NotImplementedError()
def async_set_swing_mode(self, swing_mode: str) -> Awaitable[None]: async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation. """Set new target swing operation."""
await self.hass.async_add_executor_job(self.set_swing_mode, swing_mode)
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)
def set_preset_mode(self, preset_mode: str) -> None: def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
raise NotImplementedError() raise NotImplementedError()
def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode. """Set new preset mode."""
await self.hass.async_add_executor_job(
This method must be run in the event loop and returns a coroutine. self.set_preset_mode, preset_mode)
"""
return self.hass.async_add_job(self.set_preset_mode, preset_mode)
def turn_aux_heat_on(self) -> None: def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on.""" """Turn auxiliary heater on."""
raise NotImplementedError() raise NotImplementedError()
def async_turn_aux_heat_on(self) -> Awaitable[None]: async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on. """Turn auxiliary heater on."""
await self.hass.async_add_executor_job(self.turn_aux_heat_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)
def turn_aux_heat_off(self) -> None: def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off.""" """Turn auxiliary heater off."""
raise NotImplementedError() raise NotImplementedError()
def async_turn_aux_heat_off(self) -> Awaitable[None]: async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off. """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. async def async_turn_on(self) -> None:
""" """Turn the entity on."""
return self.hass.async_add_job(self.turn_aux_heat_off) 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 @property
def supported_features(self) -> int: def supported_features(self) -> int:

View File

@ -114,3 +114,17 @@ nuheat_resume_program:
entity_id: entity_id:
description: Name(s) of entities to change. description: Name(s) of entities to change.
example: 'climate.kitchen' 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'

View File

@ -10,7 +10,8 @@ from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, 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_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE,
SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) 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 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 data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) 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)

View File

@ -1,9 +1,11 @@
"""The tests for the climate component.""" """The tests for the climate component."""
from unittest.mock import MagicMock
import pytest import pytest
import voluptuous as vol 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 from tests.common import async_mock_service
@ -37,3 +39,25 @@ async def test_set_temp_schema(hass, caplog):
assert len(calls) == 1 assert len(calls) == 1
assert calls[-1].data == data 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

View File

@ -4,12 +4,12 @@ import pytest
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_HVAC_ACTIONS, ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE,
ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODES, ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_ACTIONS, ATTR_HVAC_MODES,
ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP,
ATTR_PRESET_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_PRESET_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, DOMAIN, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, DOMAIN, HVAC_MODE_COOL,
HVAC_MODE_COOL, HVAC_MODE_HEAT, PRESET_AWAY, PRESET_ECO) HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO)
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import METRIC_SYSTEM 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) state = hass.states.get(ENTITY_CLIMATE)
assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF 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