diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 0008ba26f8a..219553b3563 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers.typing import ConfigType -from . import flash_briefings, intent, smart_home_http +from . import flash_briefings, intent, smart_home from .const import ( CONF_AUDIO, CONF_DISPLAY_CATEGORIES, @@ -100,6 +100,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if CONF_SMART_HOME in config: smart_home_config: dict[str, Any] | None = config[CONF_SMART_HOME] smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) - await smart_home_http.async_setup(hass, smart_home_config) + await smart_home.async_setup(hass, smart_home_config) return True diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c1b99b017e5..4235d739d22 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -75,8 +75,7 @@ from .errors import ( AlexaUnsupportedThermostatModeError, AlexaVideoActionNotPermittedForContentError, ) -from .messages import AlexaDirective, AlexaResponse -from .state_report import async_enable_proactive_mode +from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive" diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py deleted file mode 100644 index 4dd154ea11f..00000000000 --- a/homeassistant/components/alexa/messages.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Alexa models.""" -import logging -from uuid import uuid4 - -from .const import ( - API_CONTEXT, - API_DIRECTIVE, - API_ENDPOINT, - API_EVENT, - API_HEADER, - API_PAYLOAD, - API_SCOPE, -) -from .entities import ENTITY_ADAPTERS -from .errors import AlexaInvalidEndpointError - -_LOGGER = logging.getLogger(__name__) - - -class AlexaDirective: - """An incoming Alexa directive.""" - - def __init__(self, request): - """Initialize a directive.""" - self._directive = request[API_DIRECTIVE] - self.namespace = self._directive[API_HEADER]["namespace"] - self.name = self._directive[API_HEADER]["name"] - self.payload = self._directive[API_PAYLOAD] - self.has_endpoint = API_ENDPOINT in self._directive - - self.entity = self.entity_id = self.endpoint = self.instance = None - - def load_entity(self, hass, config): - """Set attributes related to the entity for this request. - - Sets these attributes when self.has_endpoint is True: - - - entity - - entity_id - - endpoint - - instance (when header includes instance property) - - Behavior when self.has_endpoint is False is undefined. - - Will raise AlexaInvalidEndpointError if the endpoint in the request is - malformed or nonexistent. - """ - _endpoint_id = self._directive[API_ENDPOINT]["endpointId"] - self.entity_id = _endpoint_id.replace("#", ".") - - self.entity = hass.states.get(self.entity_id) - if not self.entity or not config.should_expose(self.entity_id): - raise AlexaInvalidEndpointError(_endpoint_id) - - self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) - if "instance" in self._directive[API_HEADER]: - self.instance = self._directive[API_HEADER]["instance"] - - def response(self, name="Response", namespace="Alexa", payload=None): - """Create an API formatted response. - - Async friendly. - """ - response = AlexaResponse(name, namespace, payload) - - token = self._directive[API_HEADER].get("correlationToken") - if token: - response.set_correlation_token(token) - - if self.has_endpoint: - response.set_endpoint(self._directive[API_ENDPOINT].copy()) - - return response - - def error( - self, - namespace="Alexa", - error_type="INTERNAL_ERROR", - error_message="", - payload=None, - ): - """Create a API formatted error response. - - Async friendly. - """ - payload = payload or {} - payload["type"] = error_type - payload["message"] = error_message - - _LOGGER.info( - "Request %s/%s error %s: %s", - self._directive[API_HEADER]["namespace"], - self._directive[API_HEADER]["name"], - error_type, - error_message, - ) - - return self.response(name="ErrorResponse", namespace=namespace, payload=payload) - - -class AlexaResponse: - """Class to hold a response.""" - - def __init__(self, name, namespace, payload=None): - """Initialize the response.""" - payload = payload or {} - self._response = { - API_EVENT: { - API_HEADER: { - "namespace": namespace, - "name": name, - "messageId": str(uuid4()), - "payloadVersion": "3", - }, - API_PAYLOAD: payload, - } - } - - @property - def name(self): - """Return the name of this response.""" - return self._response[API_EVENT][API_HEADER]["name"] - - @property - def namespace(self): - """Return the namespace of this response.""" - return self._response[API_EVENT][API_HEADER]["namespace"] - - def set_correlation_token(self, token): - """Set the correlationToken. - - This should normally mirror the value from a request, and is set by - AlexaDirective.response() usually. - """ - self._response[API_EVENT][API_HEADER]["correlationToken"] = token - - def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None): - """Set the endpoint dictionary. - - This is used to send proactive messages to Alexa. - """ - self._response[API_EVENT][API_ENDPOINT] = { - API_SCOPE: {"type": "BearerToken", "token": bearer_token} - } - - if endpoint_id is not None: - self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id - - if cookie is not None: - self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie - - def set_endpoint(self, endpoint): - """Set the endpoint. - - This should normally mirror the value from a request, and is set by - AlexaDirective.response() usually. - """ - self._response[API_EVENT][API_ENDPOINT] = endpoint - - def _properties(self): - context = self._response.setdefault(API_CONTEXT, {}) - return context.setdefault("properties", []) - - def add_context_property(self, prop): - """Add a property to the response context. - - The Alexa response includes a list of properties which provides - feedback on how states have changed. For example if a user asks, - "Alexa, set thermostat to 20 degrees", the API expects a response with - the new value of the property, and Alexa will respond to the user - "Thermostat set to 20 degrees". - - async_handle_message() will call .merge_context_properties() for every - request automatically, however often handlers will call services to - change state but the effects of those changes are applied - asynchronously. Thus, handlers should call this method to confirm - changes before returning. - """ - self._properties().append(prop) - - def merge_context_properties(self, endpoint): - """Add all properties from given endpoint if not already set. - - Handlers should be using .add_context_property(). - """ - properties = self._properties() - already_set = {(p["namespace"], p["name"]) for p in properties} - - for prop in endpoint.serialize_properties(): - if (prop["namespace"], prop["name"]) not in already_set: - self.add_context_property(prop) - - def serialize(self): - """Return response as a JSON-able data structure.""" - return self._response diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 24229507877..3f8932a48bc 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,16 +1,153 @@ """Support for alexa Smart Home Skill API.""" import logging -import homeassistant.core as ha +from homeassistant import core +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.typing import ConfigType -from .const import API_DIRECTIVE, API_HEADER, EVENT_ALEXA_SMART_HOME +from .auth import Auth +from .config import AbstractConfig +from .const import ( + API_DIRECTIVE, + API_HEADER, + CONF_ENDPOINT, + CONF_ENTITY_CONFIG, + CONF_FILTER, + CONF_LOCALE, + EVENT_ALEXA_SMART_HOME, +) from .errors import AlexaBridgeUnreachableError, AlexaError from .handlers import HANDLERS -from .messages import AlexaDirective +from .state_report import AlexaDirective + +SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" _LOGGER = logging.getLogger(__name__) +class AlexaConfig(AbstractConfig): + """Alexa config.""" + + def __init__(self, hass, config): + """Initialize Alexa config.""" + super().__init__(hass) + self._config = config + + if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): + self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET]) + else: + self._auth = None + + @property + def supports_auth(self): + """Return if config supports auth.""" + return self._auth is not None + + @property + def should_report_state(self): + """Return if we should proactively report states.""" + return self._auth is not None and self.authorized + + @property + def endpoint(self): + """Endpoint for report state.""" + return self._config.get(CONF_ENDPOINT) + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG) or {} + + @property + def locale(self): + """Return config locale.""" + return self._config.get(CONF_LOCALE) + + @core.callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "" + + @core.callback + def should_expose(self, entity_id): + """If an entity should be exposed.""" + if not self._config[CONF_FILTER].empty_filter: + return self._config[CONF_FILTER](entity_id) + + entity_registry = er.async_get(self.hass) + if registry_entry := entity_registry.async_get(entity_id): + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) + else: + auxiliary_entity = False + return not auxiliary_entity + + @core.callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._auth.async_invalidate_access_token() + + async def async_get_access_token(self): + """Get an access token.""" + return await self._auth.async_get_access_token() + + async def async_accept_grant(self, code): + """Accept a grant.""" + return await self._auth.async_do_auth(code) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: + """Activate Smart Home functionality of Alexa component. + + This is optional, triggered by having a `smart_home:` sub-section in the + alexa configuration. + + Even if that's disabled, the functionality in this module may still be used + by the cloud component which will call async_handle_message directly. + """ + smart_home_config = AlexaConfig(hass, config) + await smart_home_config.async_initialize() + hass.http.register_view(SmartHomeView(smart_home_config)) + + if smart_home_config.should_report_state: + await smart_home_config.async_enable_proactive_mode() + + +class SmartHomeView(HomeAssistantView): + """Expose Smart Home v3 payload interface via HTTP POST.""" + + url = SMART_HOME_HTTP_ENDPOINT + name = "api:alexa:smart_home" + + def __init__(self, smart_home_config): + """Initialize.""" + self.smart_home_config = smart_home_config + + async def post(self, request): + """Handle Alexa Smart Home requests. + + The Smart Home API requires the endpoint to be implemented in AWS + Lambda, which will need to forward the requests to here and pass back + the response. + """ + hass = request.app["hass"] + user = request["hass_user"] + message = await request.json() + + _LOGGER.debug("Received Alexa Smart Home request: %s", message) + + response = await async_handle_message( + hass, self.smart_home_config, message, context=core.Context(user_id=user.id) + ) + _LOGGER.debug("Sending Alexa Smart Home response: %s", response) + return b"" if response is None else self.json(response) + + async def async_handle_message(hass, config, request, context=None, enabled=True): """Handle incoming API messages. @@ -21,7 +158,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True assert request[API_DIRECTIVE][API_HEADER]["payloadVersion"] == "3" if context is None: - context = ha.Context() + context = Context() directive = AlexaDirective(request) diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py deleted file mode 100644 index 3a702421d94..00000000000 --- a/homeassistant/components/alexa/smart_home_http.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Alexa HTTP interface.""" -import logging - -from homeassistant import core -from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import ConfigType - -from .auth import Auth -from .config import AbstractConfig -from .const import CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE -from .smart_home import async_handle_message - -_LOGGER = logging.getLogger(__name__) -SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" - - -class AlexaConfig(AbstractConfig): - """Alexa config.""" - - def __init__(self, hass, config): - """Initialize Alexa config.""" - super().__init__(hass) - self._config = config - - if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): - self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET]) - else: - self._auth = None - - @property - def supports_auth(self): - """Return if config supports auth.""" - return self._auth is not None - - @property - def should_report_state(self): - """Return if we should proactively report states.""" - return self._auth is not None and self.authorized - - @property - def endpoint(self): - """Endpoint for report state.""" - return self._config.get(CONF_ENDPOINT) - - @property - def entity_config(self): - """Return entity config.""" - return self._config.get(CONF_ENTITY_CONFIG) or {} - - @property - def locale(self): - """Return config locale.""" - return self._config.get(CONF_LOCALE) - - @core.callback - def user_identifier(self): - """Return an identifier for the user that represents this config.""" - return "" - - @core.callback - def should_expose(self, entity_id): - """If an entity should be exposed.""" - if not self._config[CONF_FILTER].empty_filter: - return self._config[CONF_FILTER](entity_id) - - entity_registry = er.async_get(self.hass) - if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = ( - registry_entry.entity_category is not None - or registry_entry.hidden_by is not None - ) - else: - auxiliary_entity = False - return not auxiliary_entity - - @core.callback - def async_invalidate_access_token(self): - """Invalidate access token.""" - self._auth.async_invalidate_access_token() - - async def async_get_access_token(self): - """Get an access token.""" - return await self._auth.async_get_access_token() - - async def async_accept_grant(self, code): - """Accept a grant.""" - return await self._auth.async_do_auth(code) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: - """Activate Smart Home functionality of Alexa component. - - This is optional, triggered by having a `smart_home:` sub-section in the - alexa configuration. - - Even if that's disabled, the functionality in this module may still be used - by the cloud component which will call async_handle_message directly. - """ - smart_home_config = AlexaConfig(hass, config) - await smart_home_config.async_initialize() - hass.http.register_view(SmartHomeView(smart_home_config)) - - if smart_home_config.should_report_state: - await smart_home_config.async_enable_proactive_mode() - - -class SmartHomeView(HomeAssistantView): - """Expose Smart Home v3 payload interface via HTTP POST.""" - - url = SMART_HOME_HTTP_ENDPOINT - name = "api:alexa:smart_home" - - def __init__(self, smart_home_config): - """Initialize.""" - self.smart_home_config = smart_home_config - - async def post(self, request): - """Handle Alexa Smart Home requests. - - The Smart Home API requires the endpoint to be implemented in AWS - Lambda, which will need to forward the requests to here and pass back - the response. - """ - hass = request.app["hass"] - user = request["hass_user"] - message = await request.json() - - _LOGGER.debug("Received Alexa Smart Home request: %s", message) - - response = await async_handle_message( - hass, self.smart_home_config, message, context=core.Context(user_id=user.id) - ) - _LOGGER.debug("Sending Alexa Smart Home response: %s", response) - return b"" if response is None else self.json(response) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 04bb561560f..808e0eac482 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,6 +6,7 @@ from http import HTTPStatus import json import logging from typing import TYPE_CHECKING, cast +from uuid import uuid4 import aiohttp import async_timeout @@ -19,10 +20,21 @@ from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util from homeassistant.util.json import JsonObjectType, json_loads_object -from .const import API_CHANGE, DATE_FORMAT, DOMAIN, Cause +from .const import ( + API_CHANGE, + API_CONTEXT, + API_DIRECTIVE, + API_ENDPOINT, + API_EVENT, + API_HEADER, + API_PAYLOAD, + API_SCOPE, + DATE_FORMAT, + DOMAIN, + Cause, +) from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id -from .errors import NoTokenAvailable, RequireRelink -from .messages import AlexaResponse +from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink if TYPE_CHECKING: from .config import AbstractConfig @@ -31,6 +43,184 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_TIMEOUT = 10 +class AlexaDirective: + """An incoming Alexa directive.""" + + def __init__(self, request): + """Initialize a directive.""" + self._directive = request[API_DIRECTIVE] + self.namespace = self._directive[API_HEADER]["namespace"] + self.name = self._directive[API_HEADER]["name"] + self.payload = self._directive[API_PAYLOAD] + self.has_endpoint = API_ENDPOINT in self._directive + + self.entity = self.entity_id = self.endpoint = self.instance = None + + def load_entity(self, hass, config): + """Set attributes related to the entity for this request. + + Sets these attributes when self.has_endpoint is True: + + - entity + - entity_id + - endpoint + - instance (when header includes instance property) + + Behavior when self.has_endpoint is False is undefined. + + Will raise AlexaInvalidEndpointError if the endpoint in the request is + malformed or nonexistent. + """ + _endpoint_id = self._directive[API_ENDPOINT]["endpointId"] + self.entity_id = _endpoint_id.replace("#", ".") + + self.entity = hass.states.get(self.entity_id) + if not self.entity or not config.should_expose(self.entity_id): + raise AlexaInvalidEndpointError(_endpoint_id) + + self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) + if "instance" in self._directive[API_HEADER]: + self.instance = self._directive[API_HEADER]["instance"] + + def response(self, name="Response", namespace="Alexa", payload=None): + """Create an API formatted response. + + Async friendly. + """ + response = AlexaResponse(name, namespace, payload) + + token = self._directive[API_HEADER].get("correlationToken") + if token: + response.set_correlation_token(token) + + if self.has_endpoint: + response.set_endpoint(self._directive[API_ENDPOINT].copy()) + + return response + + def error( + self, + namespace="Alexa", + error_type="INTERNAL_ERROR", + error_message="", + payload=None, + ): + """Create a API formatted error response. + + Async friendly. + """ + payload = payload or {} + payload["type"] = error_type + payload["message"] = error_message + + _LOGGER.info( + "Request %s/%s error %s: %s", + self._directive[API_HEADER]["namespace"], + self._directive[API_HEADER]["name"], + error_type, + error_message, + ) + + return self.response(name="ErrorResponse", namespace=namespace, payload=payload) + + +class AlexaResponse: + """Class to hold a response.""" + + def __init__(self, name, namespace, payload=None): + """Initialize the response.""" + payload = payload or {} + self._response = { + API_EVENT: { + API_HEADER: { + "namespace": namespace, + "name": name, + "messageId": str(uuid4()), + "payloadVersion": "3", + }, + API_PAYLOAD: payload, + } + } + + @property + def name(self): + """Return the name of this response.""" + return self._response[API_EVENT][API_HEADER]["name"] + + @property + def namespace(self): + """Return the namespace of this response.""" + return self._response[API_EVENT][API_HEADER]["namespace"] + + def set_correlation_token(self, token): + """Set the correlationToken. + + This should normally mirror the value from a request, and is set by + AlexaDirective.response() usually. + """ + self._response[API_EVENT][API_HEADER]["correlationToken"] = token + + def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None): + """Set the endpoint dictionary. + + This is used to send proactive messages to Alexa. + """ + self._response[API_EVENT][API_ENDPOINT] = { + API_SCOPE: {"type": "BearerToken", "token": bearer_token} + } + + if endpoint_id is not None: + self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id + + if cookie is not None: + self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie + + def set_endpoint(self, endpoint): + """Set the endpoint. + + This should normally mirror the value from a request, and is set by + AlexaDirective.response() usually. + """ + self._response[API_EVENT][API_ENDPOINT] = endpoint + + def _properties(self): + context = self._response.setdefault(API_CONTEXT, {}) + return context.setdefault("properties", []) + + def add_context_property(self, prop): + """Add a property to the response context. + + The Alexa response includes a list of properties which provides + feedback on how states have changed. For example if a user asks, + "Alexa, set thermostat to 20 degrees", the API expects a response with + the new value of the property, and Alexa will respond to the user + "Thermostat set to 20 degrees". + + async_handle_message() will call .merge_context_properties() for every + request automatically, however often handlers will call services to + change state but the effects of those changes are applied + asynchronously. Thus, handlers should call this method to confirm + changes before returning. + """ + self._properties().append(prop) + + def merge_context_properties(self, endpoint): + """Add all properties from given endpoint if not already set. + + Handlers should be using .add_context_property(). + """ + properties = self._properties() + already_set = {(p["namespace"], p["name"]) for p in properties} + + for prop in endpoint.serialize_properties(): + if (prop["namespace"], prop["name"]) not in already_set: + self.add_context_property(prop) + + def serialize(self): + """Return response as a JSON-able data structure.""" + return self._response + + async def async_enable_proactive_mode(hass, smart_home_config): """Enable the proactive mode. diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index 53a836f97f3..4cbe112af49 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -4,7 +4,7 @@ from uuid import uuid4 import pytest -from homeassistant.components.alexa import config, smart_home, smart_home_http +from homeassistant.components.alexa import config, smart_home from homeassistant.components.alexa.const import CONF_ENDPOINT, CONF_FILTER, CONF_LOCALE from homeassistant.core import Context, callback from homeassistant.helpers import entityfilter @@ -16,7 +16,7 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" TEST_LOCALE = "en-US" -class MockConfig(smart_home_http.AlexaConfig): +class MockConfig(smart_home.AlexaConfig): """Mock Alexa config.""" entity_config = { diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 477e7884e4f..317febcfdd1 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,10 +1,10 @@ """Test for smart home alexa support.""" from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest -from homeassistant.components.alexa import messages, smart_home +from homeassistant.components.alexa import smart_home, state_report import homeassistant.components.camera as camera from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.media_player import MediaPlayerEntityFeature @@ -29,6 +29,7 @@ from .test_common import ( ) from tests.common import async_capture_events, async_mock_service +from tests.typing import ClientSessionGenerator @pytest.fixture @@ -58,7 +59,7 @@ def test_create_api_message_defaults(hass: HomeAssistant) -> None: """Create an API message response of a request with defaults.""" request = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy") directive_header = request["directive"]["header"] - directive = messages.AlexaDirective(request) + directive = state_report.AlexaDirective(request) msg = directive.response(payload={"test": 3})._response @@ -84,7 +85,7 @@ def test_create_api_message_special() -> None: request = get_new_request("Alexa.PowerController", "TurnOn") directive_header = request["directive"]["header"] directive_header.pop("correlationToken") - directive = messages.AlexaDirective(request) + directive = state_report.AlexaDirective(request) msg = directive.response("testName", "testNameSpace")._response @@ -4372,3 +4373,27 @@ async def test_api_message_sets_authorized(hass: HomeAssistant) -> None: config._store.set_authorized.assert_not_called() await smart_home.async_handle_message(hass, config, msg) config._store.set_authorized.assert_called_once_with(True) + + +async def test_alexa_config( + hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> None: + """Test all methods of the AlexaConfig class.""" + config = { + "filter": entityfilter.FILTER_SCHEMA({"include_domains": ["sensor"]}), + } + test_config = smart_home.AlexaConfig(hass, config) + await test_config.async_initialize() + assert not test_config.supports_auth + assert not test_config.should_report_state + assert test_config.endpoint is None + assert test_config.entity_config == {} + assert test_config.user_identifier() == "" + assert test_config.locale is None + assert test_config.should_expose("sensor.test") + assert not test_config.should_expose("switch.test") + with patch.object(test_config, "_auth", AsyncMock()): + test_config.async_invalidate_access_token() + assert len(test_config._auth.async_invalidate_access_token.mock_calls) + await test_config.async_accept_grant("grant_code") + test_config._auth.async_do_auth.assert_called_once_with("grant_code") diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index b279e75b634..b0f78e958d7 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -1,8 +1,11 @@ """Test Smart Home HTTP endpoints.""" from http import HTTPStatus import json +from typing import Any -from homeassistant.components.alexa import DOMAIN, smart_home_http +import pytest + +from homeassistant.components.alexa import DOMAIN, smart_home from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -19,19 +22,31 @@ async def do_http_discovery(config, hass, hass_client): request = get_new_request("Alexa.Discovery", "Discover") response = await http_client.post( - smart_home_http.SMART_HOME_HTTP_ENDPOINT, + smart_home.SMART_HOME_HTTP_ENDPOINT, data=json.dumps(request), headers={"content-type": CONTENT_TYPE_JSON}, ) return response +@pytest.mark.parametrize( + "config", + [ + {"alexa": {"smart_home": None}}, + { + "alexa": { + "smart_home": { + "client_id": "someclientid", + "client_secret": "verysecret", + } + } + }, + ], +) async def test_http_api( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator, config: dict[str, Any] ) -> None: """With `smart_home:` HTTP API is exposed.""" - config = {"alexa": {"smart_home": None}} - response = await do_http_discovery(config, hass, hass_client) response_data = await response.json()