Proactive Alexa ChangeReport messages (#18114)
* Alexa: implement auth and proactive ChangeReport messages * refactor after rebase from dev to use the new AlexaDirective and Response classes * move to aiohttp; cleanup * better function name * move endpoint to config * allow passing token function * remove uneeded state get * use iterable directly Co-Authored-By: abmantis <abmantis@users.noreply.github.com> * missing delete from previous commit * checks for when user has no auth config * update cloud component * PR suggestions * string lint * Revert "string lint" This reverts commit a05a1f134c9ebc7a6e67c093009744f142256365. * linters are now happier * more happy linters * use internal date parser; improve json response handling * remove unused import * use await instead of async_add_job * protect access token update method * add test_report_state * line too long * add docstring * Update test_smart_home.py * test accept grant api * init prefs if None * add tests for auth and token requests * replace global with hass.data * doc lint
This commit is contained in:
parent
c2525bede2
commit
ead38f6005
6 changed files with 526 additions and 26 deletions
|
@ -11,11 +11,24 @@ from homeassistant.const import (
|
|||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components import alexa
|
||||
from homeassistant.components.alexa import smart_home
|
||||
from homeassistant.components.alexa.auth import Auth
|
||||
from homeassistant.helpers import entityfilter
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
DEFAULT_CONFIG = smart_home.Config(should_expose=lambda entity_id: True)
|
||||
|
||||
async def get_access_token():
|
||||
"""Return a test access token."""
|
||||
return "thisisnotanacesstoken"
|
||||
|
||||
|
||||
TEST_URL = "https://api.amazonalexa.com/v3/events"
|
||||
TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token"
|
||||
|
||||
DEFAULT_CONFIG = smart_home.Config(
|
||||
endpoint=TEST_URL,
|
||||
async_get_access_token=get_access_token,
|
||||
should_expose=lambda entity_id: True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -940,12 +953,15 @@ async def test_exclude_filters(hass):
|
|||
hass.states.async_set(
|
||||
'cover.deny', 'off', {'friendly_name': "Blocked cover"})
|
||||
|
||||
config = smart_home.Config(should_expose=entityfilter.generate_filter(
|
||||
include_domains=[],
|
||||
include_entities=[],
|
||||
exclude_domains=['script'],
|
||||
exclude_entities=['cover.deny'],
|
||||
))
|
||||
config = smart_home.Config(
|
||||
endpoint=None,
|
||||
async_get_access_token=None,
|
||||
should_expose=entityfilter.generate_filter(
|
||||
include_domains=[],
|
||||
include_entities=[],
|
||||
exclude_domains=['script'],
|
||||
exclude_entities=['cover.deny'],
|
||||
))
|
||||
|
||||
msg = await smart_home.async_handle_message(hass, config, request)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -972,12 +988,15 @@ async def test_include_filters(hass):
|
|||
hass.states.async_set(
|
||||
'group.allow', 'off', {'friendly_name': "Allowed group"})
|
||||
|
||||
config = smart_home.Config(should_expose=entityfilter.generate_filter(
|
||||
include_domains=['automation', 'group'],
|
||||
include_entities=['script.deny'],
|
||||
exclude_domains=[],
|
||||
exclude_entities=[],
|
||||
))
|
||||
config = smart_home.Config(
|
||||
endpoint=None,
|
||||
async_get_access_token=None,
|
||||
should_expose=entityfilter.generate_filter(
|
||||
include_domains=['automation', 'group'],
|
||||
include_entities=['script.deny'],
|
||||
exclude_domains=[],
|
||||
exclude_entities=[],
|
||||
))
|
||||
|
||||
msg = await smart_home.async_handle_message(hass, config, request)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -998,12 +1017,15 @@ async def test_never_exposed_entities(hass):
|
|||
hass.states.async_set(
|
||||
'group.allow', 'off', {'friendly_name': "Allowed group"})
|
||||
|
||||
config = smart_home.Config(should_expose=entityfilter.generate_filter(
|
||||
include_domains=['group'],
|
||||
include_entities=[],
|
||||
exclude_domains=[],
|
||||
exclude_entities=[],
|
||||
))
|
||||
config = smart_home.Config(
|
||||
endpoint=None,
|
||||
async_get_access_token=None,
|
||||
should_expose=entityfilter.generate_filter(
|
||||
include_domains=['group'],
|
||||
include_entities=[],
|
||||
exclude_domains=[],
|
||||
exclude_entities=[],
|
||||
))
|
||||
|
||||
msg = await smart_home.async_handle_message(hass, config, request)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -1293,6 +1315,33 @@ async def test_api_increase_color_temp(hass, result, initial):
|
|||
assert msg['header']['name'] == 'Response'
|
||||
|
||||
|
||||
async def test_api_accept_grant(hass):
|
||||
"""Test api AcceptGrant process."""
|
||||
request = get_new_request("Alexa.Authorization", "AcceptGrant")
|
||||
|
||||
# add payload
|
||||
request['directive']['payload'] = {
|
||||
'grant': {
|
||||
'type': 'OAuth2.AuthorizationCode',
|
||||
'code': 'VGhpcyBpcyBhbiBhdXRob3JpemF0aW9uIGNvZGUuIDotKQ=='
|
||||
},
|
||||
'grantee': {
|
||||
'type': 'BearerToken',
|
||||
'token': 'access-token-from-skill'
|
||||
}
|
||||
}
|
||||
|
||||
# setup test devices
|
||||
msg = await smart_home.async_handle_message(
|
||||
hass, DEFAULT_CONFIG, request)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert 'event' in msg
|
||||
msg = msg['event']
|
||||
|
||||
assert msg['header']['name'] == 'AcceptGrant.Response'
|
||||
|
||||
|
||||
async def test_report_lock_state(hass):
|
||||
"""Test LockController implements lockState property."""
|
||||
hass.states.async_set(
|
||||
|
@ -1412,6 +1461,8 @@ async def test_entity_config(hass):
|
|||
'light.test_1', 'on', {'friendly_name': "Test light 1"})
|
||||
|
||||
config = smart_home.Config(
|
||||
endpoint=None,
|
||||
async_get_access_token=None,
|
||||
should_expose=lambda entity_id: True,
|
||||
entity_config={
|
||||
'light.test_1': {
|
||||
|
@ -1598,3 +1649,104 @@ async def test_disabled(hass):
|
|||
assert msg['header']['name'] == 'ErrorResponse'
|
||||
assert msg['header']['namespace'] == 'Alexa'
|
||||
assert msg['payload']['type'] == 'BRIDGE_UNREACHABLE'
|
||||
|
||||
|
||||
async def test_report_state(hass, aioclient_mock):
|
||||
"""Test proactive state reports."""
|
||||
aioclient_mock.post(TEST_URL, json={'data': 'is irrelevant'})
|
||||
|
||||
hass.states.async_set(
|
||||
'binary_sensor.test_contact',
|
||||
'on',
|
||||
{
|
||||
'friendly_name': "Test Contact Sensor",
|
||||
'device_class': 'door',
|
||||
}
|
||||
)
|
||||
|
||||
await smart_home.async_enable_proactive_mode(hass, DEFAULT_CONFIG)
|
||||
|
||||
hass.states.async_set(
|
||||
'binary_sensor.test_contact',
|
||||
'off',
|
||||
{
|
||||
'friendly_name': "Test Contact Sensor",
|
||||
'device_class': 'door',
|
||||
}
|
||||
)
|
||||
|
||||
# To trigger event listener
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
call = aioclient_mock.mock_calls
|
||||
|
||||
call_json = json.loads(call[0][2])
|
||||
assert call_json["event"]["payload"]["change"]["properties"][0][
|
||||
"value"] == "NOT_DETECTED"
|
||||
assert call_json["event"]["endpoint"][
|
||||
"endpointId"] == "binary_sensor#test_contact"
|
||||
|
||||
|
||||
async def run_auth_get_access_token(hass, aioclient_mock, expires_in,
|
||||
client_id, client_secret,
|
||||
accept_grant_code, refresh_token):
|
||||
"""Do auth and request a new token for tests."""
|
||||
aioclient_mock.post(TEST_TOKEN_URL,
|
||||
json={'access_token': 'the_access_token',
|
||||
'refresh_token': refresh_token,
|
||||
'expires_in': expires_in})
|
||||
|
||||
auth = Auth(hass, client_id, client_secret)
|
||||
await auth.async_do_auth(accept_grant_code)
|
||||
await auth.async_get_access_token()
|
||||
|
||||
|
||||
async def test_auth_get_access_token_expired(hass, aioclient_mock):
|
||||
"""Test the auth get access token function."""
|
||||
client_id = "client123"
|
||||
client_secret = "shhhhh"
|
||||
accept_grant_code = "abcdefg"
|
||||
refresh_token = "refresher"
|
||||
|
||||
await run_auth_get_access_token(hass, aioclient_mock, -5,
|
||||
client_id, client_secret,
|
||||
accept_grant_code, refresh_token)
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 2
|
||||
calls = aioclient_mock.mock_calls
|
||||
|
||||
auth_call_json = calls[0][2]
|
||||
token_call_json = calls[1][2]
|
||||
|
||||
assert auth_call_json["grant_type"] == "authorization_code"
|
||||
assert auth_call_json["code"] == accept_grant_code
|
||||
assert auth_call_json["client_id"] == client_id
|
||||
assert auth_call_json["client_secret"] == client_secret
|
||||
|
||||
assert token_call_json["grant_type"] == "refresh_token"
|
||||
assert token_call_json["refresh_token"] == refresh_token
|
||||
assert token_call_json["client_id"] == client_id
|
||||
assert token_call_json["client_secret"] == client_secret
|
||||
|
||||
|
||||
async def test_auth_get_access_token_not_expired(hass, aioclient_mock):
|
||||
"""Test the auth get access token function."""
|
||||
client_id = "client123"
|
||||
client_secret = "shhhhh"
|
||||
accept_grant_code = "abcdefg"
|
||||
refresh_token = "refresher"
|
||||
|
||||
await run_auth_get_access_token(hass, aioclient_mock, 555,
|
||||
client_id, client_secret,
|
||||
accept_grant_code, refresh_token)
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
call = aioclient_mock.mock_calls
|
||||
|
||||
auth_call_json = call[0][2]
|
||||
|
||||
assert auth_call_json["grant_type"] == "authorization_code"
|
||||
assert auth_call_json["code"] == accept_grant_code
|
||||
assert auth_call_json["client_id"] == client_id
|
||||
assert auth_call_json["client_secret"] == client_secret
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue