Implement thermostat support for Alexa (#13340)
* Implement thermostat support for Alexa * util.temperature: Support interval conversions * Use climate.ATTR_OPERATION_MODE for Alexa thermostat mode * Switch coroutines to async/await * Log all Alexa error events
This commit is contained in:
parent
184f2be83e
commit
5801d78017
3 changed files with 374 additions and 17 deletions
|
@ -6,18 +6,20 @@ from datetime import datetime
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
alert, automation, cover, climate, fan, group, input_boolean, light, lock,
|
||||||
media_player, scene, script, switch, http, sensor)
|
media_player, scene, script, switch, http, sensor)
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
from homeassistant.util.temperature import convert as convert_temperature
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
|
||||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
|
||||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||||
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
||||||
|
|
||||||
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -34,6 +36,16 @@ API_TEMP_UNITS = {
|
||||||
TEMP_CELSIUS: 'CELSIUS',
|
TEMP_CELSIUS: 'CELSIUS',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
API_THERMOSTAT_MODES = {
|
||||||
|
climate.STATE_HEAT: 'HEAT',
|
||||||
|
climate.STATE_COOL: 'COOL',
|
||||||
|
climate.STATE_AUTO: 'AUTO',
|
||||||
|
climate.STATE_ECO: 'ECO',
|
||||||
|
climate.STATE_IDLE: 'OFF',
|
||||||
|
climate.STATE_FAN_ONLY: 'OFF',
|
||||||
|
climate.STATE_DRY: 'OFF',
|
||||||
|
}
|
||||||
|
|
||||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||||
|
|
||||||
CONF_DESCRIPTION = 'description'
|
CONF_DESCRIPTION = 'description'
|
||||||
|
@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
|
||||||
raise _UnsupportedProperty(name)
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
temp = self.entity.state
|
||||||
|
if self.entity.domain == climate.DOMAIN:
|
||||||
|
temp = self.entity.attributes.get(
|
||||||
|
climate.ATTR_CURRENT_TEMPERATURE)
|
||||||
return {
|
return {
|
||||||
'value': float(self.entity.state),
|
'value': float(temp),
|
||||||
|
'scale': API_TEMP_UNITS[unit],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaThermostatController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.ThermostatController'
|
||||||
|
|
||||||
|
def properties_supported(self):
|
||||||
|
properties = []
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
|
||||||
|
properties.append({'name': 'targetSetpoint'})
|
||||||
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
|
||||||
|
properties.append({'name': 'lowerSetpoint'})
|
||||||
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
|
||||||
|
properties.append({'name': 'upperSetpoint'})
|
||||||
|
if supported & climate.SUPPORT_OPERATION_MODE:
|
||||||
|
properties.append({'name': 'thermostatMode'})
|
||||||
|
return properties
|
||||||
|
|
||||||
|
def properties_retrievable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_property(self, name):
|
||||||
|
if name == 'thermostatMode':
|
||||||
|
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
||||||
|
mode = API_THERMOSTAT_MODES.get(ha_mode)
|
||||||
|
if mode is None:
|
||||||
|
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
|
||||||
|
self.entity.entity_id, type(self.entity),
|
||||||
|
climate.ATTR_OPERATION_MODE, ha_mode)
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
return mode
|
||||||
|
|
||||||
|
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
temp = None
|
||||||
|
if name == 'targetSetpoint':
|
||||||
|
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
|
||||||
|
elif name == 'lowerSetpoint':
|
||||||
|
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
|
||||||
|
elif name == 'upperSetpoint':
|
||||||
|
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
|
||||||
|
if temp is None:
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'value': float(temp),
|
||||||
'scale': API_TEMP_UNITS[unit],
|
'scale': API_TEMP_UNITS[unit],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
|
||||||
return [_AlexaPowerController(self.entity)]
|
return [_AlexaPowerController(self.entity)]
|
||||||
|
|
||||||
|
|
||||||
|
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||||
|
class _ClimateCapabilities(_AlexaEntity):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.THERMOSTAT]
|
||||||
|
|
||||||
|
def interfaces(self):
|
||||||
|
yield _AlexaThermostatController(self.entity)
|
||||||
|
yield _AlexaTemperatureSensor(self.entity)
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||||
class _CoverCapabilities(_AlexaEntity):
|
class _CoverCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
|
@ -682,17 +756,26 @@ def api_message(request,
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
|
def api_error(request,
|
||||||
|
namespace='Alexa',
|
||||||
|
error_type='INTERNAL_ERROR',
|
||||||
|
error_message="",
|
||||||
|
payload=None):
|
||||||
"""Create a API formatted error response.
|
"""Create a API formatted error response.
|
||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
payload = {
|
payload = payload or {}
|
||||||
'type': error_type,
|
payload['type'] = error_type
|
||||||
'message': error_message,
|
payload['message'] = error_message
|
||||||
}
|
|
||||||
|
|
||||||
return api_message(request, name='ErrorResponse', payload=payload)
|
_LOGGER.info("Request %s/%s error %s: %s",
|
||||||
|
request[API_HEADER]['namespace'],
|
||||||
|
request[API_HEADER]['name'],
|
||||||
|
error_type, error_message)
|
||||||
|
|
||||||
|
return api_message(
|
||||||
|
request, name='ErrorResponse', namespace=namespace, payload=payload)
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||||
|
@ -1104,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
|
||||||
else:
|
else:
|
||||||
msg = 'failed to map input {} to a media source on {}'.format(
|
msg = 'failed to map input {} to a media source on {}'.format(
|
||||||
media_input, entity.entity_id)
|
media_input, entity.entity_id)
|
||||||
_LOGGER.error(msg)
|
|
||||||
return api_error(
|
return api_error(
|
||||||
request, error_type='INVALID_VALUE', error_message=msg)
|
request, error_type='INVALID_VALUE', error_message=msg)
|
||||||
|
|
||||||
|
@ -1276,6 +1358,149 @@ def async_api_previous(hass, config, request, entity):
|
||||||
return api_message(request)
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
|
||||||
|
"""Create temperature value out of range API error response.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
|
temp_range = {
|
||||||
|
'minimumValue': {
|
||||||
|
'value': min_temp,
|
||||||
|
'scale': API_TEMP_UNITS[unit],
|
||||||
|
},
|
||||||
|
'maximumValue': {
|
||||||
|
'value': max_temp,
|
||||||
|
'scale': API_TEMP_UNITS[unit],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = 'The requested temperature {} is out of range'.format(temp)
|
||||||
|
return api_error(
|
||||||
|
request,
|
||||||
|
error_type='TEMPERATURE_VALUE_OUT_OF_RANGE',
|
||||||
|
error_message=msg,
|
||||||
|
payload={'validRange': temp_range},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def temperature_from_object(temp_obj, to_unit, interval=False):
|
||||||
|
"""Get temperature from Temperature object in requested unit."""
|
||||||
|
from_unit = TEMP_CELSIUS
|
||||||
|
temp = float(temp_obj['value'])
|
||||||
|
|
||||||
|
if temp_obj['scale'] == 'FAHRENHEIT':
|
||||||
|
from_unit = TEMP_FAHRENHEIT
|
||||||
|
elif temp_obj['scale'] == 'KELVIN':
|
||||||
|
# convert to Celsius if absolute temperature
|
||||||
|
if not interval:
|
||||||
|
temp -= 273.15
|
||||||
|
|
||||||
|
return convert_temperature(temp, from_unit, to_unit, interval)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
|
||||||
|
@extract_entity
|
||||||
|
async def async_api_set_target_temp(hass, config, request, entity):
|
||||||
|
"""Process a set target temperature request."""
|
||||||
|
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||||
|
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = request[API_PAYLOAD]
|
||||||
|
if 'targetSetpoint' in payload:
|
||||||
|
temp = temperature_from_object(
|
||||||
|
payload['targetSetpoint'], unit)
|
||||||
|
if temp < min_temp or temp > max_temp:
|
||||||
|
return api_error_temp_range(
|
||||||
|
request, temp, min_temp, max_temp, unit)
|
||||||
|
data[ATTR_TEMPERATURE] = temp
|
||||||
|
if 'lowerSetpoint' in payload:
|
||||||
|
temp_low = temperature_from_object(
|
||||||
|
payload['lowerSetpoint'], unit)
|
||||||
|
if temp_low < min_temp or temp_low > max_temp:
|
||||||
|
return api_error_temp_range(
|
||||||
|
request, temp_low, min_temp, max_temp, unit)
|
||||||
|
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
|
||||||
|
if 'upperSetpoint' in payload:
|
||||||
|
temp_high = temperature_from_object(
|
||||||
|
payload['upperSetpoint'], unit)
|
||||||
|
if temp_high < min_temp or temp_high > max_temp:
|
||||||
|
return api_error_temp_range(
|
||||||
|
request, temp_high, min_temp, max_temp, unit)
|
||||||
|
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||||
|
|
||||||
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
|
||||||
|
@extract_entity
|
||||||
|
async def async_api_adjust_target_temp(hass, config, request, entity):
|
||||||
|
"""Process an adjust target temperature request."""
|
||||||
|
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||||
|
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||||
|
|
||||||
|
temp_delta = temperature_from_object(
|
||||||
|
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
|
||||||
|
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
|
||||||
|
|
||||||
|
if target_temp < min_temp or target_temp > max_temp:
|
||||||
|
return api_error_temp_range(
|
||||||
|
request, target_temp, min_temp, max_temp, unit)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
ATTR_TEMPERATURE: target_temp,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||||
|
|
||||||
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
|
||||||
|
@extract_entity
|
||||||
|
async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||||
|
"""Process a set thermostat mode request."""
|
||||||
|
mode = request[API_PAYLOAD]['thermostatMode']
|
||||||
|
|
||||||
|
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||||
|
# Work around a pylint false positive due to
|
||||||
|
# https://github.com/PyCQA/pylint/issues/1830
|
||||||
|
# pylint: disable=stop-iteration-return
|
||||||
|
ha_mode = next(
|
||||||
|
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if ha_mode not in operation_list:
|
||||||
|
msg = 'The requested thermostat mode {} is not supported'.format(mode)
|
||||||
|
return api_error(
|
||||||
|
request,
|
||||||
|
namespace='Alexa.ThermostatController',
|
||||||
|
error_type='UNSUPPORTED_THERMOSTAT_MODE',
|
||||||
|
error_message=msg
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
climate.ATTR_OPERATION_MODE: ha_mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
|
||||||
|
blocking=False)
|
||||||
|
|
||||||
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(('Alexa', 'ReportState'))
|
@HANDLERS.register(('Alexa', 'ReportState'))
|
||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
|
@ -3,17 +3,22 @@ from homeassistant.const import (
|
||||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_NOT_RECOGNIZED_TEMPLATE, TEMPERATURE)
|
TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_NOT_RECOGNIZED_TEMPLATE, TEMPERATURE)
|
||||||
|
|
||||||
|
|
||||||
def fahrenheit_to_celsius(fahrenheit: float) -> float:
|
def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float:
|
||||||
"""Convert a temperature in Fahrenheit to Celsius."""
|
"""Convert a temperature in Fahrenheit to Celsius."""
|
||||||
|
if interval:
|
||||||
|
return fahrenheit / 1.8
|
||||||
return (fahrenheit - 32.0) / 1.8
|
return (fahrenheit - 32.0) / 1.8
|
||||||
|
|
||||||
|
|
||||||
def celsius_to_fahrenheit(celsius: float) -> float:
|
def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float:
|
||||||
"""Convert a temperature in Celsius to Fahrenheit."""
|
"""Convert a temperature in Celsius to Fahrenheit."""
|
||||||
|
if interval:
|
||||||
|
return celsius * 1.8
|
||||||
return celsius * 1.8 + 32.0
|
return celsius * 1.8 + 32.0
|
||||||
|
|
||||||
|
|
||||||
def convert(temperature: float, from_unit: str, to_unit: str) -> float:
|
def convert(temperature: float, from_unit: str, to_unit: str,
|
||||||
|
interval: bool = False) -> float:
|
||||||
"""Convert a temperature from one unit to another."""
|
"""Convert a temperature from one unit to another."""
|
||||||
if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
||||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(
|
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(
|
||||||
|
@ -25,5 +30,5 @@ def convert(temperature: float, from_unit: str, to_unit: str) -> float:
|
||||||
if from_unit == to_unit:
|
if from_unit == to_unit:
|
||||||
return temperature
|
return temperature
|
||||||
elif from_unit == TEMP_CELSIUS:
|
elif from_unit == TEMP_CELSIUS:
|
||||||
return celsius_to_fahrenheit(temperature)
|
return celsius_to_fahrenheit(temperature, interval)
|
||||||
return fahrenheit_to_celsius(temperature)
|
return fahrenheit_to_celsius(temperature, interval)
|
||||||
|
|
|
@ -693,6 +693,133 @@ def test_unknown_sensor(hass):
|
||||||
yield from discovery_test(device, hass, expected_endpoints=0)
|
yield from discovery_test(device, hass, expected_endpoints=0)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thermostat(hass):
|
||||||
|
"""Test thermostat discovery."""
|
||||||
|
device = (
|
||||||
|
'climate.test_thermostat',
|
||||||
|
'cool',
|
||||||
|
{
|
||||||
|
'operation_mode': 'cool',
|
||||||
|
'temperature': 70.0,
|
||||||
|
'target_temp_high': 80.0,
|
||||||
|
'target_temp_low': 60.0,
|
||||||
|
'current_temperature': 75.0,
|
||||||
|
'friendly_name': "Test Thermostat",
|
||||||
|
'supported_features': 1 | 2 | 4 | 128,
|
||||||
|
'operation_list': ['heat', 'cool', 'auto', 'off'],
|
||||||
|
'min_temp': 50,
|
||||||
|
'max_temp': 90,
|
||||||
|
'unit_of_measurement': TEMP_FAHRENHEIT,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
assert appliance['endpointId'] == 'climate#test_thermostat'
|
||||||
|
assert appliance['displayCategories'][0] == 'THERMOSTAT'
|
||||||
|
assert appliance['friendlyName'] == "Test Thermostat"
|
||||||
|
|
||||||
|
assert_endpoint_capabilities(
|
||||||
|
appliance,
|
||||||
|
'Alexa.ThermostatController',
|
||||||
|
'Alexa.TemperatureSensor',
|
||||||
|
)
|
||||||
|
|
||||||
|
properties = await reported_properties(
|
||||||
|
hass, 'climate#test_thermostat')
|
||||||
|
properties.assert_equal(
|
||||||
|
'Alexa.ThermostatController', 'thermostatMode', 'COOL')
|
||||||
|
properties.assert_equal(
|
||||||
|
'Alexa.ThermostatController', 'targetSetpoint',
|
||||||
|
{'value': 70.0, 'scale': 'FAHRENHEIT'})
|
||||||
|
properties.assert_equal(
|
||||||
|
'Alexa.TemperatureSensor', 'temperature',
|
||||||
|
{'value': 75.0, 'scale': 'FAHRENHEIT'})
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
'Alexa.ThermostatController', 'SetTargetTemperature',
|
||||||
|
'climate#test_thermostat', 'climate.set_temperature',
|
||||||
|
hass,
|
||||||
|
payload={'targetSetpoint': {'value': 69.0, 'scale': 'FAHRENHEIT'}}
|
||||||
|
)
|
||||||
|
assert call.data['temperature'] == 69.0
|
||||||
|
|
||||||
|
msg = await assert_request_fails(
|
||||||
|
'Alexa.ThermostatController', 'SetTargetTemperature',
|
||||||
|
'climate#test_thermostat', 'climate.set_temperature',
|
||||||
|
hass,
|
||||||
|
payload={'targetSetpoint': {'value': 0.0, 'scale': 'CELSIUS'}}
|
||||||
|
)
|
||||||
|
assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
'Alexa.ThermostatController', 'SetTargetTemperature',
|
||||||
|
'climate#test_thermostat', 'climate.set_temperature',
|
||||||
|
hass,
|
||||||
|
payload={
|
||||||
|
'targetSetpoint': {'value': 70.0, 'scale': 'FAHRENHEIT'},
|
||||||
|
'lowerSetpoint': {'value': 293.15, 'scale': 'KELVIN'},
|
||||||
|
'upperSetpoint': {'value': 30.0, 'scale': 'CELSIUS'},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert call.data['temperature'] == 70.0
|
||||||
|
assert call.data['target_temp_low'] == 68.0
|
||||||
|
assert call.data['target_temp_high'] == 86.0
|
||||||
|
|
||||||
|
msg = await assert_request_fails(
|
||||||
|
'Alexa.ThermostatController', 'SetTargetTemperature',
|
||||||
|
'climate#test_thermostat', 'climate.set_temperature',
|
||||||
|
hass,
|
||||||
|
payload={
|
||||||
|
'lowerSetpoint': {'value': 273.15, 'scale': 'KELVIN'},
|
||||||
|
'upperSetpoint': {'value': 75.0, 'scale': 'FAHRENHEIT'},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
|
||||||
|
|
||||||
|
msg = await assert_request_fails(
|
||||||
|
'Alexa.ThermostatController', 'SetTargetTemperature',
|
||||||
|
'climate#test_thermostat', 'climate.set_temperature',
|
||||||
|
hass,
|
||||||
|
payload={
|
||||||
|
'lowerSetpoint': {'value': 293.15, 'scale': 'FAHRENHEIT'},
|
||||||
|
'upperSetpoint': {'value': 75.0, 'scale': 'CELSIUS'},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
'Alexa.ThermostatController', 'AdjustTargetTemperature',
|
||||||
|
'climate#test_thermostat', 'climate.set_temperature',
|
||||||
|
hass,
|
||||||
|
payload={'targetSetpointDelta': {'value': -10.0, 'scale': 'KELVIN'}}
|
||||||
|
)
|
||||||
|
assert call.data['temperature'] == 52.0
|
||||||
|
|
||||||
|
msg = await assert_request_fails(
|
||||||
|
'Alexa.ThermostatController', 'AdjustTargetTemperature',
|
||||||
|
'climate#test_thermostat', 'climate.set_temperature',
|
||||||
|
hass,
|
||||||
|
payload={'targetSetpointDelta': {'value': 20.0, 'scale': 'CELSIUS'}}
|
||||||
|
)
|
||||||
|
assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
|
||||||
|
|
||||||
|
call, _ = await assert_request_calls_service(
|
||||||
|
'Alexa.ThermostatController', 'SetThermostatMode',
|
||||||
|
'climate#test_thermostat', 'climate.set_operation_mode',
|
||||||
|
hass,
|
||||||
|
payload={'thermostatMode': 'HEAT'}
|
||||||
|
)
|
||||||
|
assert call.data['operation_mode'] == 'heat'
|
||||||
|
|
||||||
|
msg = await assert_request_fails(
|
||||||
|
'Alexa.ThermostatController', 'SetThermostatMode',
|
||||||
|
'climate#test_thermostat', 'climate.set_operation_mode',
|
||||||
|
hass,
|
||||||
|
payload={'thermostatMode': 'INVALID'}
|
||||||
|
)
|
||||||
|
assert msg['event']['payload']['type'] == 'UNSUPPORTED_THERMOSTAT_MODE'
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def test_exclude_filters(hass):
|
def test_exclude_filters(hass):
|
||||||
"""Test exclusion filters."""
|
"""Test exclusion filters."""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue