Proactive Alexa ChangeReport messages (#18114)
* Alexa: implement auth and proactive ChangeReport messages * refactor after rebase from dev to use the new AlexaDirective and Response classes * move to aiohttp; cleanup * better function name * move endpoint to config * allow passing token function * remove uneeded state get * use iterable directly Co-Authored-By: abmantis <abmantis@users.noreply.github.com> * missing delete from previous commit * checks for when user has no auth config * update cloud component * PR suggestions * string lint * Revert "string lint" This reverts commit a05a1f134c9ebc7a6e67c093009744f142256365. * linters are now happier * more happy linters * use internal date parser; improve json response handling * remove unused import * use await instead of async_add_job * protect access token update method * add test_report_state * line too long * add docstring * Update test_smart_home.py * test accept grant api * init prefs if None * add tests for auth and token requests * replace global with hass.data * doc lint
This commit is contained in:
parent
c2525bede2
commit
ead38f6005
6 changed files with 526 additions and 26 deletions
154
homeassistant/components/alexa/auth.py
Normal file
154
homeassistant/components/alexa/auth.py
Normal file
|
@ -0,0 +1,154 @@
|
|||
"""Support for Alexa skill auth."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.util import dt
|
||||
from .const import DEFAULT_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
|
||||
LWA_HEADERS = {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
|
||||
}
|
||||
|
||||
PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
|
||||
STORAGE_KEY = 'alexa_auth'
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_EXPIRE_TIME = "expire_time"
|
||||
STORAGE_ACCESS_TOKEN = "access_token"
|
||||
STORAGE_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
|
||||
class Auth:
|
||||
"""Handle authentication to send events to Alexa."""
|
||||
|
||||
def __init__(self, hass, client_id, client_secret):
|
||||
"""Initialize the Auth class."""
|
||||
self.hass = hass
|
||||
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
self._prefs = None
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
self._get_token_lock = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
async def async_do_auth(self, accept_grant_code):
|
||||
"""Do authentication with an AcceptGrant code."""
|
||||
# access token not retrieved yet for the first time, so this should
|
||||
# be an access token request
|
||||
|
||||
lwa_params = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": accept_grant_code,
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret
|
||||
}
|
||||
_LOGGER.debug("Calling LWA to get the access token (first time), "
|
||||
"with: %s", json.dumps(lwa_params))
|
||||
|
||||
return await self._async_request_new_token(lwa_params)
|
||||
|
||||
async def async_get_access_token(self):
|
||||
"""Perform access token or token refresh request."""
|
||||
async with self._get_token_lock:
|
||||
if self._prefs is None:
|
||||
await self.async_load_preferences()
|
||||
|
||||
if self.is_token_valid():
|
||||
_LOGGER.debug("Token still valid, using it.")
|
||||
return self._prefs[STORAGE_ACCESS_TOKEN]
|
||||
|
||||
if self._prefs[STORAGE_REFRESH_TOKEN] is None:
|
||||
_LOGGER.debug("Token invalid and no refresh token available.")
|
||||
return None
|
||||
|
||||
lwa_params = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret
|
||||
}
|
||||
|
||||
_LOGGER.debug("Calling LWA to refresh the access token.")
|
||||
return await self._async_request_new_token(lwa_params)
|
||||
|
||||
@callback
|
||||
def is_token_valid(self):
|
||||
"""Check if a token is already loaded and if it is still valid."""
|
||||
if not self._prefs[STORAGE_ACCESS_TOKEN]:
|
||||
return False
|
||||
|
||||
expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
|
||||
preemptive_expire_time = expire_time - timedelta(
|
||||
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS)
|
||||
|
||||
return dt.utcnow() < preemptive_expire_time
|
||||
|
||||
async def _async_request_new_token(self, lwa_params):
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self.hass.loop):
|
||||
response = await session.post(LWA_TOKEN_URI,
|
||||
headers=LWA_HEADERS,
|
||||
data=lwa_params,
|
||||
allow_redirects=True)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Timeout calling LWA to get auth token.")
|
||||
return None
|
||||
|
||||
_LOGGER.debug("LWA response header: %s", response.headers)
|
||||
_LOGGER.debug("LWA response status: %s", response.status)
|
||||
|
||||
if response.status != 200:
|
||||
_LOGGER.error("Error calling LWA to get auth token.")
|
||||
return None
|
||||
|
||||
response_json = await response.json()
|
||||
_LOGGER.debug("LWA response body : %s", response_json)
|
||||
|
||||
access_token = response_json["access_token"]
|
||||
refresh_token = response_json["refresh_token"]
|
||||
expires_in = response_json["expires_in"]
|
||||
expire_time = dt.utcnow() + timedelta(seconds=expires_in)
|
||||
|
||||
await self._async_update_preferences(access_token, refresh_token,
|
||||
expire_time.isoformat())
|
||||
|
||||
return access_token
|
||||
|
||||
async def async_load_preferences(self):
|
||||
"""Load preferences with stored tokens."""
|
||||
self._prefs = await self._store.async_load()
|
||||
|
||||
if self._prefs is None:
|
||||
self._prefs = {
|
||||
STORAGE_ACCESS_TOKEN: None,
|
||||
STORAGE_REFRESH_TOKEN: None,
|
||||
STORAGE_EXPIRE_TIME: None
|
||||
}
|
||||
|
||||
async def _async_update_preferences(self, access_token, refresh_token,
|
||||
expire_time):
|
||||
"""Update user preferences."""
|
||||
if self._prefs is None:
|
||||
await self.async_load_preferences()
|
||||
|
||||
if access_token is not None:
|
||||
self._prefs[STORAGE_ACCESS_TOKEN] = access_token
|
||||
if refresh_token is not None:
|
||||
self._prefs[STORAGE_REFRESH_TOKEN] = refresh_token
|
||||
if expire_time is not None:
|
||||
self._prefs[STORAGE_EXPIRE_TIME] = expire_time
|
||||
await self._store.async_save(self._prefs)
|
Loading…
Add table
Add a link
Reference in a new issue