mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Suppress Alexa state reports when not authorized (#64064)
This commit is contained in:
parent
efddace53a
commit
e6899416e1
@ -1,5 +1,6 @@
|
|||||||
"""Config helpers for Alexa."""
|
"""Config helpers for Alexa."""
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
@ -9,6 +10,8 @@ from .state_report import async_enable_proactive_mode
|
|||||||
|
|
||||||
STORE_AUTHORIZED = "authorized"
|
STORE_AUTHORIZED = "authorized"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AbstractConfig(ABC):
|
class AbstractConfig(ABC):
|
||||||
"""Hold the configuration for Alexa."""
|
"""Hold the configuration for Alexa."""
|
||||||
@ -102,13 +105,25 @@ class AbstractConfig(ABC):
|
|||||||
"""Return authorization status."""
|
"""Return authorization status."""
|
||||||
return self._store.authorized
|
return self._store.authorized
|
||||||
|
|
||||||
def set_authorized(self, authorized):
|
async def set_authorized(self, authorized):
|
||||||
"""Set authorization status.
|
"""Set authorization status.
|
||||||
|
|
||||||
- Set when an incoming message is received from Alexa.
|
- Set when an incoming message is received from Alexa.
|
||||||
- Unset if state reporting fails
|
- Unset if state reporting fails
|
||||||
"""
|
"""
|
||||||
self._store.set_authorized(authorized)
|
self._store.set_authorized(authorized)
|
||||||
|
if self.should_report_state != self.is_reporting_states:
|
||||||
|
if self.should_report_state:
|
||||||
|
_LOGGER.debug("Enable proactive mode")
|
||||||
|
try:
|
||||||
|
await self.async_enable_proactive_mode()
|
||||||
|
except Exception:
|
||||||
|
# We failed to enable proactive mode, unset authorized flag
|
||||||
|
self._store.set_authorized(False)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("Disable proactive mode")
|
||||||
|
await self.async_disable_proactive_mode()
|
||||||
|
|
||||||
|
|
||||||
class AlexaConfigStore:
|
class AlexaConfigStore:
|
||||||
|
@ -31,7 +31,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
|
|||||||
"Alexa API not enabled in Home Assistant configuration"
|
"Alexa API not enabled in Home Assistant configuration"
|
||||||
)
|
)
|
||||||
|
|
||||||
config.set_authorized(True)
|
await config.set_authorized(True)
|
||||||
|
|
||||||
if directive.has_endpoint:
|
if directive.has_endpoint:
|
||||||
directive.load_entity(hass, config)
|
directive.load_entity(hass, config)
|
||||||
|
@ -39,7 +39,7 @@ class AlexaConfig(AbstractConfig):
|
|||||||
@property
|
@property
|
||||||
def should_report_state(self):
|
def should_report_state(self):
|
||||||
"""Return if we should proactively report states."""
|
"""Return if we should proactively report states."""
|
||||||
return self._auth is not None
|
return self._auth is not None and self.authorized
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint(self):
|
def endpoint(self):
|
||||||
|
@ -117,7 +117,11 @@ async def async_send_changereport_message(
|
|||||||
try:
|
try:
|
||||||
token = await config.async_get_access_token()
|
token = await config.async_get_access_token()
|
||||||
except (RequireRelink, NoTokenAvailable):
|
except (RequireRelink, NoTokenAvailable):
|
||||||
config.set_authorized(False)
|
await config.set_authorized(False)
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error when sending ChangeReport to Alexa, could not get access token"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {token}"}
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
@ -170,7 +174,7 @@ async def async_send_changereport_message(
|
|||||||
alexa_properties,
|
alexa_properties,
|
||||||
invalidate_access_token=False,
|
invalidate_access_token=False,
|
||||||
)
|
)
|
||||||
config.set_authorized(False)
|
await config.set_authorized(False)
|
||||||
|
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Error when sending ChangeReport to Alexa: %s: %s",
|
"Error when sending ChangeReport to Alexa: %s: %s",
|
||||||
|
@ -75,7 +75,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
@property
|
@property
|
||||||
def should_report_state(self):
|
def should_report_state(self):
|
||||||
"""Return if states should be proactively reported."""
|
"""Return if states should be proactively reported."""
|
||||||
return self._prefs.alexa_report_state
|
return self._prefs.alexa_report_state and self.authorized
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint(self):
|
def endpoint(self):
|
||||||
@ -159,7 +159,6 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
if resp.status == HTTPStatus.BAD_REQUEST:
|
if resp.status == HTTPStatus.BAD_REQUEST:
|
||||||
if body["reason"] in ("RefreshTokenNotFound", "UnknownRegion"):
|
if body["reason"] in ("RefreshTokenNotFound", "UnknownRegion"):
|
||||||
if self.should_report_state:
|
if self.should_report_state:
|
||||||
await self._prefs.async_update(alexa_report_state=False)
|
|
||||||
persistent_notification.async_create(
|
persistent_notification.async_create(
|
||||||
self.hass,
|
self.hass,
|
||||||
f"There was an error reporting state to Alexa ({body['reason']}). "
|
f"There was an error reporting state to Alexa ({body['reason']}). "
|
||||||
|
@ -372,10 +372,10 @@ async def websocket_update_prefs(hass, connection, msg):
|
|||||||
"Please go to the Alexa app and re-link the Home Assistant "
|
"Please go to the Alexa app and re-link the Home Assistant "
|
||||||
"skill and then try to enable state reporting.",
|
"skill and then try to enable state reporting.",
|
||||||
)
|
)
|
||||||
alexa_config.set_authorized(False)
|
await alexa_config.set_authorized(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
alexa_config.set_authorized(True)
|
await alexa_config.set_authorized(True)
|
||||||
|
|
||||||
await cloud.client.prefs.async_update(**changes)
|
await cloud.client.prefs.async_update(**changes)
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
"""Test report state."""
|
"""Test report state."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
from homeassistant.components.alexa import state_report
|
from homeassistant.components.alexa import errors, state_report
|
||||||
|
|
||||||
from . import TEST_URL, get_default_config
|
from . import TEST_URL, get_default_config
|
||||||
|
|
||||||
@ -99,6 +101,37 @@ async def test_report_state_unsets_authorized_on_error(hass, aioclient_mock):
|
|||||||
config._store.set_authorized.assert_called_once_with(False)
|
config._store.set_authorized.assert_called_once_with(False)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exc", [errors.NoTokenAvailable, errors.RequireRelink])
|
||||||
|
async def test_report_state_unsets_authorized_on_access_token_error(
|
||||||
|
hass, aioclient_mock, exc
|
||||||
|
):
|
||||||
|
"""Test proactive state unsets authorized on error."""
|
||||||
|
aioclient_mock.post(TEST_URL, text="", status=202)
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
"binary_sensor.test_contact",
|
||||||
|
"on",
|
||||||
|
{"friendly_name": "Test Contact Sensor", "device_class": "door"},
|
||||||
|
)
|
||||||
|
|
||||||
|
config = get_default_config()
|
||||||
|
|
||||||
|
await state_report.async_enable_proactive_mode(hass, config)
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
"binary_sensor.test_contact",
|
||||||
|
"off",
|
||||||
|
{"friendly_name": "Test Contact Sensor", "device_class": "door"},
|
||||||
|
)
|
||||||
|
|
||||||
|
config._store.set_authorized.assert_not_called()
|
||||||
|
|
||||||
|
with patch.object(config, "async_get_access_token", AsyncMock(side_effect=exc)):
|
||||||
|
# To trigger event listener
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
config._store.set_authorized.assert_called_once_with(False)
|
||||||
|
|
||||||
|
|
||||||
async def test_report_state_instance(hass, aioclient_mock):
|
async def test_report_state_instance(hass, aioclient_mock):
|
||||||
"""Test proactive state reports with instance."""
|
"""Test proactive state reports with instance."""
|
||||||
aioclient_mock.post(TEST_URL, text="", status=202)
|
aioclient_mock.post(TEST_URL, text="", status=202)
|
||||||
|
@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.alexa import errors
|
||||||
from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config
|
from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||||
@ -89,6 +90,7 @@ async def test_alexa_config_report_state(hass, cloud_prefs, cloud_stub):
|
|||||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
||||||
)
|
)
|
||||||
await conf.async_initialize()
|
await conf.async_initialize()
|
||||||
|
await conf.set_authorized(True)
|
||||||
|
|
||||||
assert cloud_prefs.alexa_report_state is False
|
assert cloud_prefs.alexa_report_state is False
|
||||||
assert conf.should_report_state is False
|
assert conf.should_report_state is False
|
||||||
@ -147,6 +149,107 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock):
|
|||||||
assert len(aioclient_mock.mock_calls) == 2
|
assert len(aioclient_mock.mock_calls) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reject_reason,expected_exception",
|
||||||
|
[
|
||||||
|
("RefreshTokenNotFound", errors.RequireRelink),
|
||||||
|
("UnknownRegion", errors.RequireRelink),
|
||||||
|
("OtherReason", errors.NoTokenAvailable),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_alexa_config_fail_refresh_token(
|
||||||
|
hass,
|
||||||
|
cloud_prefs,
|
||||||
|
aioclient_mock,
|
||||||
|
reject_reason,
|
||||||
|
expected_exception,
|
||||||
|
):
|
||||||
|
"""Test Alexa config failing to refresh token."""
|
||||||
|
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://example/alexa_token",
|
||||||
|
json={
|
||||||
|
"access_token": "mock-token",
|
||||||
|
"event_endpoint": "http://example.com/alexa_endpoint",
|
||||||
|
"expires_in": 30,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
aioclient_mock.post("http://example.com/alexa_endpoint", text="", status=202)
|
||||||
|
conf = alexa_config.CloudAlexaConfig(
|
||||||
|
hass,
|
||||||
|
ALEXA_SCHEMA({}),
|
||||||
|
"mock-user-id",
|
||||||
|
cloud_prefs,
|
||||||
|
Mock(
|
||||||
|
alexa_access_token_url="http://example/alexa_token",
|
||||||
|
auth=Mock(async_check_token=AsyncMock()),
|
||||||
|
websession=async_get_clientsession(hass),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await conf.async_initialize()
|
||||||
|
await conf.set_authorized(True)
|
||||||
|
|
||||||
|
assert cloud_prefs.alexa_report_state is False
|
||||||
|
assert conf.should_report_state is False
|
||||||
|
assert conf.is_reporting_states is False
|
||||||
|
|
||||||
|
hass.states.async_set("fan.test_fan", "off")
|
||||||
|
|
||||||
|
# Enable state reporting
|
||||||
|
await cloud_prefs.async_update(alexa_report_state=True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert cloud_prefs.alexa_report_state is True
|
||||||
|
assert conf.should_report_state is True
|
||||||
|
assert conf.is_reporting_states is True
|
||||||
|
|
||||||
|
# Change states to trigger event listener
|
||||||
|
hass.states.async_set("fan.test_fan", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Invalidate the token and try to fetch another
|
||||||
|
conf.async_invalidate_access_token()
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://example/alexa_token",
|
||||||
|
json={"reason": reject_reason},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change states to trigger event listener
|
||||||
|
hass.states.async_set("fan.test_fan", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Check state reporting is still wanted in cloud prefs, but disabled for Alexa
|
||||||
|
assert cloud_prefs.alexa_report_state is True
|
||||||
|
assert conf.should_report_state is False
|
||||||
|
assert conf.is_reporting_states is False
|
||||||
|
|
||||||
|
# Simulate we're again authorized, but token update fails
|
||||||
|
with pytest.raises(expected_exception):
|
||||||
|
await conf.set_authorized(True)
|
||||||
|
|
||||||
|
assert cloud_prefs.alexa_report_state is True
|
||||||
|
assert conf.should_report_state is False
|
||||||
|
assert conf.is_reporting_states is False
|
||||||
|
|
||||||
|
# Simulate we're again authorized and token update succeeds
|
||||||
|
# State reporting should now be re-enabled for Alexa
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
aioclient_mock.post(
|
||||||
|
"http://example/alexa_token",
|
||||||
|
json={
|
||||||
|
"access_token": "mock-token",
|
||||||
|
"event_endpoint": "http://example.com/alexa_endpoint",
|
||||||
|
"expires_in": 30,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await conf.set_authorized(True)
|
||||||
|
assert cloud_prefs.alexa_report_state is True
|
||||||
|
assert conf.should_report_state is True
|
||||||
|
assert conf.is_reporting_states is True
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def patch_sync_helper():
|
def patch_sync_helper():
|
||||||
"""Patch sync helper.
|
"""Patch sync helper.
|
||||||
@ -257,9 +360,11 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
|
|||||||
|
|
||||||
async def test_alexa_update_report_state(hass, cloud_prefs, cloud_stub):
|
async def test_alexa_update_report_state(hass, cloud_prefs, cloud_stub):
|
||||||
"""Test Alexa config responds to reporting state."""
|
"""Test Alexa config responds to reporting state."""
|
||||||
await alexa_config.CloudAlexaConfig(
|
conf = alexa_config.CloudAlexaConfig(
|
||||||
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub
|
||||||
).async_initialize()
|
)
|
||||||
|
await conf.async_initialize()
|
||||||
|
await conf.set_authorized(True)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.cloud.alexa_config.CloudAlexaConfig.async_sync_entities",
|
"homeassistant.components.cloud.alexa_config.CloudAlexaConfig.async_sync_entities",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user