Improve Alexa error handling (#24745)
This commit is contained in:
parent
d4fc22add4
commit
f5f86993f1
8 changed files with 121 additions and 9 deletions
|
@ -42,11 +42,11 @@ class AbstractConfig:
|
||||||
self._unsub_proactive_report = self.hass.async_create_task(
|
self._unsub_proactive_report = self.hass.async_create_task(
|
||||||
async_enable_proactive_mode(self.hass, self)
|
async_enable_proactive_mode(self.hass, self)
|
||||||
)
|
)
|
||||||
resp = await self._unsub_proactive_report
|
try:
|
||||||
|
await self._unsub_proactive_report
|
||||||
# Failed to start reporting.
|
except Exception: # pylint: disable=broad-except
|
||||||
if resp is None:
|
|
||||||
self._unsub_proactive_report = None
|
self._unsub_proactive_report = None
|
||||||
|
raise
|
||||||
|
|
||||||
async def async_disable_proactive_mode(self):
|
async def async_disable_proactive_mode(self):
|
||||||
"""Disable proactive mode."""
|
"""Disable proactive mode."""
|
||||||
|
|
|
@ -21,6 +21,9 @@ async def async_enable_proactive_mode(hass, smart_home_config):
|
||||||
|
|
||||||
Proactive mode makes this component report state changes to Alexa.
|
Proactive mode makes this component report state changes to Alexa.
|
||||||
"""
|
"""
|
||||||
|
# Validate we can get access token.
|
||||||
|
await smart_home_config.async_get_access_token()
|
||||||
|
|
||||||
async def async_entity_state_listener(changed_entity, old_state,
|
async def async_entity_state_listener(changed_entity, old_state,
|
||||||
new_state):
|
new_state):
|
||||||
if not new_state:
|
if not new_state:
|
||||||
|
|
|
@ -103,6 +103,15 @@ class AlexaConfig(alexa_config.AbstractConfig):
|
||||||
|
|
||||||
if resp.status == 400:
|
if resp.status == 400:
|
||||||
if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'):
|
if body['reason'] in ('RefreshTokenNotFound', 'UnknownRegion'):
|
||||||
|
if self.should_report_state:
|
||||||
|
await self._prefs.async_update(alexa_report_state=False)
|
||||||
|
self.hass.components.persistent_notification.async_create(
|
||||||
|
"There was an error reporting state to Alexa ({}). "
|
||||||
|
"Please re-link your Alexa skill via the Alexa app to "
|
||||||
|
"continue using it.".format(body['reason']),
|
||||||
|
"Alexa state reporting disabled",
|
||||||
|
"cloud_alexa_report",
|
||||||
|
)
|
||||||
raise RequireRelink
|
raise RequireRelink
|
||||||
|
|
||||||
raise alexa_errors.NoTokenAvailable
|
raise alexa_errors.NoTokenAvailable
|
||||||
|
@ -200,6 +209,9 @@ class AlexaConfig(alexa_config.AbstractConfig):
|
||||||
if not to_update and not to_remove:
|
if not to_update and not to_remove:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Make sure it's valid.
|
||||||
|
await self.async_get_access_token()
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
if to_update:
|
if to_update:
|
||||||
|
@ -241,4 +253,7 @@ class AlexaConfig(alexa_config.AbstractConfig):
|
||||||
elif action == 'remove' and self.should_expose(entity_id):
|
elif action == 'remove' and self.should_expose(entity_id):
|
||||||
to_remove.append(entity_id)
|
to_remove.append(entity_id)
|
||||||
|
|
||||||
await self._sync_helper(to_update, to_remove)
|
try:
|
||||||
|
await self._sync_helper(to_update, to_remove)
|
||||||
|
except alexa_errors.NoTokenAvailable:
|
||||||
|
pass
|
||||||
|
|
|
@ -12,7 +12,10 @@ from homeassistant.components.google_assistant import smart_home as ga
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.util.aiohttp import MockRequest
|
from homeassistant.util.aiohttp import MockRequest
|
||||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
from homeassistant.components.alexa import (
|
||||||
|
smart_home as alexa_sh,
|
||||||
|
errors as alexa_errors,
|
||||||
|
)
|
||||||
|
|
||||||
from . import utils, alexa_config, google_config
|
from . import utils, alexa_config, google_config
|
||||||
from .const import DISPATCHER_REMOTE_UPDATE
|
from .const import DISPATCHER_REMOTE_UPDATE
|
||||||
|
@ -98,8 +101,14 @@ class CloudClient(Interface):
|
||||||
"""Initialize the client."""
|
"""Initialize the client."""
|
||||||
self.cloud = cloud
|
self.cloud = cloud
|
||||||
|
|
||||||
if self.alexa_config.should_report_state and self.cloud.is_logged_in:
|
if (not self.alexa_config.should_report_state or
|
||||||
|
not self.cloud.is_logged_in):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
await self.alexa_config.async_enable_proactive_mode()
|
await self.alexa_config.async_enable_proactive_mode()
|
||||||
|
except alexa_errors.NoTokenAvailable:
|
||||||
|
pass
|
||||||
|
|
||||||
async def cleanups(self) -> None:
|
async def cleanups(self) -> None:
|
||||||
"""Cleanup some stuff after logout."""
|
"""Cleanup some stuff after logout."""
|
||||||
|
|
|
@ -14,7 +14,10 @@ from homeassistant.components.http.data_validator import (
|
||||||
RequestDataValidator)
|
RequestDataValidator)
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.components.websocket_api import const as ws_const
|
from homeassistant.components.websocket_api import const as ws_const
|
||||||
from homeassistant.components.alexa import entities as alexa_entities
|
from homeassistant.components.alexa import (
|
||||||
|
entities as alexa_entities,
|
||||||
|
errors as alexa_errors,
|
||||||
|
)
|
||||||
from homeassistant.components.google_assistant import helpers as google_helpers
|
from homeassistant.components.google_assistant import helpers as google_helpers
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
@ -375,6 +378,24 @@ async def websocket_update_prefs(hass, connection, msg):
|
||||||
changes = dict(msg)
|
changes = dict(msg)
|
||||||
changes.pop('id')
|
changes.pop('id')
|
||||||
changes.pop('type')
|
changes.pop('type')
|
||||||
|
|
||||||
|
# If we turn alexa linking on, validate that we can fetch access token
|
||||||
|
if changes.get(PREF_ALEXA_REPORT_STATE):
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(10):
|
||||||
|
await cloud.client.alexa_config.async_get_access_token()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
connection.send_error(msg['id'], 'alexa_timeout',
|
||||||
|
'Timeout validating Alexa access token.')
|
||||||
|
return
|
||||||
|
except alexa_errors.NoTokenAvailable:
|
||||||
|
connection.send_error(
|
||||||
|
msg['id'], 'alexa_relink',
|
||||||
|
'Please go to the Alexa app and re-link the Home Assistant '
|
||||||
|
'skill and then try to enable state reporting.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
await cloud.client.prefs.async_update(**changes)
|
await cloud.client.prefs.async_update(**changes)
|
||||||
|
|
||||||
connection.send_message(websocket_api.result_message(msg['id']))
|
connection.send_message(websocket_api.result_message(msg['id']))
|
||||||
|
@ -575,7 +596,15 @@ async def alexa_sync(hass, connection, msg):
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
|
|
||||||
with async_timeout.timeout(10):
|
with async_timeout.timeout(10):
|
||||||
success = await cloud.client.alexa_config.async_sync_entities()
|
try:
|
||||||
|
success = await cloud.client.alexa_config.async_sync_entities()
|
||||||
|
except alexa_errors.NoTokenAvailable:
|
||||||
|
connection.send_error(
|
||||||
|
msg['id'], 'alexa_relink',
|
||||||
|
'Please go to the Alexa app and re-link the Home Assistant '
|
||||||
|
'skill.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
connection.send_result(msg['id'])
|
connection.send_result(msg['id'])
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Connection session."""
|
"""Connection session."""
|
||||||
|
import asyncio
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback, Context
|
from homeassistant.core import callback, Context
|
||||||
|
@ -101,6 +102,9 @@ class ActiveConnection:
|
||||||
elif isinstance(err, vol.Invalid):
|
elif isinstance(err, vol.Invalid):
|
||||||
code = const.ERR_INVALID_FORMAT
|
code = const.ERR_INVALID_FORMAT
|
||||||
err_message = vol.humanize.humanize_error(msg, err)
|
err_message = vol.humanize.humanize_error(msg, err)
|
||||||
|
elif isinstance(err, asyncio.TimeoutError):
|
||||||
|
code = const.ERR_TIMEOUT
|
||||||
|
err_message = 'Timeout'
|
||||||
else:
|
else:
|
||||||
code = const.ERR_UNKNOWN_ERROR
|
code = const.ERR_UNKNOWN_ERROR
|
||||||
err_message = 'Unknown error'
|
err_message = 'Unknown error'
|
||||||
|
|
|
@ -16,6 +16,7 @@ ERR_HOME_ASSISTANT_ERROR = 'home_assistant_error'
|
||||||
ERR_UNKNOWN_COMMAND = 'unknown_command'
|
ERR_UNKNOWN_COMMAND = 'unknown_command'
|
||||||
ERR_UNKNOWN_ERROR = 'unknown_error'
|
ERR_UNKNOWN_ERROR = 'unknown_error'
|
||||||
ERR_UNAUTHORIZED = 'unauthorized'
|
ERR_UNAUTHORIZED = 'unauthorized'
|
||||||
|
ERR_TIMEOUT = 'timeout'
|
||||||
|
|
||||||
TYPE_RESULT = 'result'
|
TYPE_RESULT = 'result'
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ from homeassistant.components.cloud.const import (
|
||||||
from homeassistant.components.google_assistant.helpers import (
|
from homeassistant.components.google_assistant.helpers import (
|
||||||
GoogleEntity)
|
GoogleEntity)
|
||||||
from homeassistant.components.alexa.entities import LightCapabilities
|
from homeassistant.components.alexa.entities import LightCapabilities
|
||||||
|
from homeassistant.components.alexa import errors as alexa_errors
|
||||||
|
|
||||||
from tests.common import mock_coro
|
from tests.common import mock_coro
|
||||||
from tests.components.google_assistant import MockConfig
|
from tests.components.google_assistant import MockConfig
|
||||||
|
@ -847,3 +848,53 @@ async def test_update_alexa_entity(
|
||||||
assert prefs.alexa_entity_configs['light.kitchen'] == {
|
assert prefs.alexa_entity_configs['light.kitchen'] == {
|
||||||
'should_expose': False,
|
'should_expose': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sync_alexa_entities_timeout(
|
||||||
|
hass, hass_ws_client, setup_api, mock_cloud_login):
|
||||||
|
"""Test that timeout syncing Alexa entities."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
with patch('homeassistant.components.cloud.alexa_config.AlexaConfig'
|
||||||
|
'.async_sync_entities', side_effect=asyncio.TimeoutError):
|
||||||
|
await client.send_json({
|
||||||
|
'id': 5,
|
||||||
|
'type': 'cloud/alexa/sync',
|
||||||
|
})
|
||||||
|
response = await client.receive_json()
|
||||||
|
|
||||||
|
assert not response['success']
|
||||||
|
assert response['error']['code'] == 'timeout'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sync_alexa_entities_no_token(
|
||||||
|
hass, hass_ws_client, setup_api, mock_cloud_login):
|
||||||
|
"""Test sync Alexa entities when we have no token."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
with patch('homeassistant.components.cloud.alexa_config.AlexaConfig'
|
||||||
|
'.async_sync_entities',
|
||||||
|
side_effect=alexa_errors.NoTokenAvailable):
|
||||||
|
await client.send_json({
|
||||||
|
'id': 5,
|
||||||
|
'type': 'cloud/alexa/sync',
|
||||||
|
})
|
||||||
|
response = await client.receive_json()
|
||||||
|
|
||||||
|
assert not response['success']
|
||||||
|
assert response['error']['code'] == 'alexa_relink'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_enable_alexa_state_report_fail(
|
||||||
|
hass, hass_ws_client, setup_api, mock_cloud_login):
|
||||||
|
"""Test enable Alexa entities state reporting when no token available."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
with patch('homeassistant.components.cloud.alexa_config.AlexaConfig'
|
||||||
|
'.async_sync_entities',
|
||||||
|
side_effect=alexa_errors.NoTokenAvailable):
|
||||||
|
await client.send_json({
|
||||||
|
'id': 5,
|
||||||
|
'type': 'cloud/alexa/sync',
|
||||||
|
})
|
||||||
|
response = await client.receive_json()
|
||||||
|
|
||||||
|
assert not response['success']
|
||||||
|
assert response['error']['code'] == 'alexa_relink'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue