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