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:
parent
6c8971f18a
commit
2e8e5aabae
9 changed files with 388 additions and 354 deletions
|
@ -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.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue