Add support for on/off climate (#25026)
* Add support for on/off climate * address comments * Add test for sync overwrite * Add more testspull/25090/head
parent
f5a4af40ee
commit
d47905d119
homeassistant/components/climate
tests/components
climate
demo
|
@ -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:
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue