Refactor Alexa API, fix thermostats (#17969)
* Refactor Alexa API to use objects for requests This introduces _AlexaDirective to stand in for the previous model of passing basic dict and list data structures to and from handlers. This gives a more expressive platform for functionality common to most or all handlers. I had two use cases in mind: 1) Most responses should include current properties. In the case of locks and thermostats, the response must include the properties or Alexa will give the user a vague error like "Hmm, $device is not responding." Locks currently work, but thermostats do not. I wanted a way to automatically include properties in all responses. This is implemented in a subsequent commit. 2) The previous model had a 1:1 mapping between Alexa endpoints and Home Assistant entities. This works most of the time, but sometimes it's not so great. For example, my Z-wave thermostat shows as three devices in Alexa: one for the temperature sensor, one for the heat, and one for the AC. I'd like to merge these into one device from Alexa's perspective. I believe this will be facilitated with the `endpoint` attribute on `_AlexaDirective`. * Include properties in all Alexa responses The added _AlexaResponse class provides a richer vocabulary for handlers. Among that vocabulary is .merge_context_properties(), which is invoked automatically for any request directed at an endpoint. This adds all supported properties to the response as recommended by the Alexa API docs, and in some cases (locks, thermostats at least) the user will get an error "Hmm, $device is not responding" if properties are not provided in the response. * Fix setting temperature with Alexa thermostats Fixes https://github.com/home-assistant/home-assistant/issues/16577
This commit is contained in:
parent
98163504fb
commit
e16793013a
3 changed files with 572 additions and 306 deletions
File diff suppressed because it is too large
Load diff
|
@ -227,11 +227,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
|
|||
@asyncio.coroutine
|
||||
def async_handle_alexa(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for Alexa."""
|
||||
if not cloud.alexa_enabled:
|
||||
return alexa.turned_off_response(payload)
|
||||
|
||||
result = yield from alexa.async_handle_message(
|
||||
hass, cloud.alexa_config, payload)
|
||||
hass, cloud.alexa_config, payload,
|
||||
enabled=cloud.alexa_enabled)
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
@ -57,20 +57,21 @@ def get_new_request(namespace, name, endpoint=None):
|
|||
return raw_msg
|
||||
|
||||
|
||||
def test_create_api_message_defaults():
|
||||
def test_create_api_message_defaults(hass):
|
||||
"""Create a API message response of a request with defaults."""
|
||||
request = get_new_request('Alexa.PowerController', 'TurnOn', 'switch#xy')
|
||||
request = request['directive']
|
||||
directive_header = request['directive']['header']
|
||||
directive = smart_home._AlexaDirective(request)
|
||||
|
||||
msg = smart_home.api_message(request, payload={'test': 3})
|
||||
msg = directive.response(payload={'test': 3})._response
|
||||
|
||||
assert 'event' in msg
|
||||
msg = msg['event']
|
||||
|
||||
assert msg['header']['messageId'] is not None
|
||||
assert msg['header']['messageId'] != request['header']['messageId']
|
||||
assert msg['header']['messageId'] != directive_header['messageId']
|
||||
assert msg['header']['correlationToken'] == \
|
||||
request['header']['correlationToken']
|
||||
directive_header['correlationToken']
|
||||
assert msg['header']['name'] == 'Response'
|
||||
assert msg['header']['namespace'] == 'Alexa'
|
||||
assert msg['header']['payloadVersion'] == '3'
|
||||
|
@ -78,23 +79,24 @@ def test_create_api_message_defaults():
|
|||
assert 'test' in msg['payload']
|
||||
assert msg['payload']['test'] == 3
|
||||
|
||||
assert msg['endpoint'] == request['endpoint']
|
||||
assert msg['endpoint'] == request['directive']['endpoint']
|
||||
assert msg['endpoint'] is not request['directive']['endpoint']
|
||||
|
||||
|
||||
def test_create_api_message_special():
|
||||
"""Create a API message response of a request with non defaults."""
|
||||
request = get_new_request('Alexa.PowerController', 'TurnOn')
|
||||
request = request['directive']
|
||||
directive_header = request['directive']['header']
|
||||
directive_header.pop('correlationToken')
|
||||
directive = smart_home._AlexaDirective(request)
|
||||
|
||||
request['header'].pop('correlationToken')
|
||||
|
||||
msg = smart_home.api_message(request, 'testName', 'testNameSpace')
|
||||
msg = directive.response('testName', 'testNameSpace')._response
|
||||
|
||||
assert 'event' in msg
|
||||
msg = msg['event']
|
||||
|
||||
assert msg['header']['messageId'] is not None
|
||||
assert msg['header']['messageId'] != request['header']['messageId']
|
||||
assert msg['header']['messageId'] != directive_header['messageId']
|
||||
assert 'correlationToken' not in msg['header']
|
||||
assert msg['header']['name'] == 'testName'
|
||||
assert msg['header']['namespace'] == 'testNameSpace'
|
||||
|
@ -785,13 +787,17 @@ async def test_thermostat(hass):
|
|||
'Alexa.TemperatureSensor', 'temperature',
|
||||
{'value': 75.0, 'scale': 'FAHRENHEIT'})
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
call, msg = 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
|
||||
properties = _ReportedProperties(msg['context']['properties'])
|
||||
properties.assert_equal(
|
||||
'Alexa.ThermostatController', 'targetSetpoint',
|
||||
{'value': 69.0, 'scale': 'FAHRENHEIT'})
|
||||
|
||||
msg = await assert_request_fails(
|
||||
'Alexa.ThermostatController', 'SetTargetTemperature',
|
||||
|
@ -801,7 +807,7 @@ async def test_thermostat(hass):
|
|||
)
|
||||
assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
call, msg = await assert_request_calls_service(
|
||||
'Alexa.ThermostatController', 'SetTargetTemperature',
|
||||
'climate#test_thermostat', 'climate.set_temperature',
|
||||
hass,
|
||||
|
@ -814,6 +820,16 @@ async def test_thermostat(hass):
|
|||
assert call.data['temperature'] == 70.0
|
||||
assert call.data['target_temp_low'] == 68.0
|
||||
assert call.data['target_temp_high'] == 86.0
|
||||
properties = _ReportedProperties(msg['context']['properties'])
|
||||
properties.assert_equal(
|
||||
'Alexa.ThermostatController', 'targetSetpoint',
|
||||
{'value': 70.0, 'scale': 'FAHRENHEIT'})
|
||||
properties.assert_equal(
|
||||
'Alexa.ThermostatController', 'lowerSetpoint',
|
||||
{'value': 68.0, 'scale': 'FAHRENHEIT'})
|
||||
properties.assert_equal(
|
||||
'Alexa.ThermostatController', 'upperSetpoint',
|
||||
{'value': 86.0, 'scale': 'FAHRENHEIT'})
|
||||
|
||||
msg = await assert_request_fails(
|
||||
'Alexa.ThermostatController', 'SetTargetTemperature',
|
||||
|
@ -837,13 +853,17 @@ async def test_thermostat(hass):
|
|||
)
|
||||
assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE'
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
call, msg = 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
|
||||
properties = _ReportedProperties(msg['context']['properties'])
|
||||
properties.assert_equal(
|
||||
'Alexa.ThermostatController', 'targetSetpoint',
|
||||
{'value': 52.0, 'scale': 'FAHRENHEIT'})
|
||||
|
||||
msg = await assert_request_fails(
|
||||
'Alexa.ThermostatController', 'AdjustTargetTemperature',
|
||||
|
@ -1467,3 +1487,24 @@ async def test_logging_request_with_entity(hass, events):
|
|||
'name': 'ErrorResponse'
|
||||
}
|
||||
assert event.context == context
|
||||
|
||||
|
||||
async def test_disabled(hass):
|
||||
"""When enabled=False, everything fails."""
|
||||
hass.states.async_set(
|
||||
'switch.test', 'on', {'friendly_name': "Test switch"})
|
||||
request = get_new_request('Alexa.PowerController', 'TurnOn', 'switch#test')
|
||||
|
||||
call_switch = async_mock_service(hass, 'switch', 'turn_on')
|
||||
|
||||
msg = await smart_home.async_handle_message(
|
||||
hass, DEFAULT_CONFIG, request, enabled=False)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert 'event' in msg
|
||||
msg = msg['event']
|
||||
|
||||
assert not call_switch
|
||||
assert msg['header']['name'] == 'ErrorResponse'
|
||||
assert msg['header']['namespace'] == 'Alexa'
|
||||
assert msg['payload']['type'] == 'BRIDGE_UNREACHABLE'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue