Refactor alexa modules to avoid circular deps (#97618)

* Refactor alexa modules to avoid circula deps

* Add test http api auth and AlexaConfig

* Update test

* Improve test
This commit is contained in:
Jan Bouwhuis 2023-08-05 21:32:53 +02:00 committed by GitHub
parent 6c8971f18a
commit 2e8e5aabae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 388 additions and 354 deletions

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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.

View file

@ -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 = {

View file

@ -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")

View file

@ -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()