mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-18 22:56:31 +00:00
Leverage access and refresh tokens if available (#575)
* Leverage access and refresh tokens if available * Update homeassistant.py * Update homeassistant.py * Update proxy.py * Migrate HomeAssistant to new exception layout * Fix build for 3.7 * Cleanups * Fix style * fix log strings * Fix new style * Fix travis build * python 3.7 * next try * fix * fix lint * Fix lint p2 * Add logging * Fix logging * fix access * Fix spell * fix return * Fix runtime * Add to hass config
This commit is contained in:
parent
1b481e0b37
commit
4df42e054d
12
.travis.yml
12
.travis.yml
@ -1,12 +1,6 @@
|
||||
sudo: false
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.6"
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
sudo: true
|
||||
dist: xenial
|
||||
install: pip install -U tox
|
||||
language: python
|
||||
python: 3.7
|
||||
script: tox
|
||||
|
@ -10,7 +10,7 @@ from ..const import (
|
||||
ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT,
|
||||
ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX,
|
||||
ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_WAIT_BOOT, ATTR_MACHINE,
|
||||
CONTENT_TYPE_BINARY)
|
||||
ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..validate import NETWORK_PORT, DOCKER_IMAGE
|
||||
|
||||
@ -30,6 +30,8 @@ SCHEMA_OPTIONS = vol.Schema({
|
||||
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
||||
vol.Optional(ATTR_WAIT_BOOT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=60)),
|
||||
# Required once we enforce user system
|
||||
vol.Optional(ATTR_REFRESH_TOKEN): str,
|
||||
})
|
||||
|
||||
SCHEMA_VERSION = vol.Schema({
|
||||
@ -83,8 +85,10 @@ class APIHomeAssistant(CoreSysAttributes):
|
||||
if ATTR_WAIT_BOOT in body:
|
||||
self.sys_homeassistant.wait_boot = body[ATTR_WAIT_BOOT]
|
||||
|
||||
if ATTR_REFRESH_TOKEN in body:
|
||||
self.sys_homeassistant.refresh_token = body[ATTR_REFRESH_TOKEN]
|
||||
|
||||
self.sys_homeassistant.save_data()
|
||||
return True
|
||||
|
||||
@api_process
|
||||
async def stats(self, request):
|
||||
@ -109,11 +113,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version)
|
||||
|
||||
if version == self.sys_homeassistant.version:
|
||||
raise RuntimeError("Version {} is already in use".format(version))
|
||||
|
||||
return await asyncio.shield(
|
||||
self.sys_homeassistant.update(version))
|
||||
await asyncio.shield(self.sys_homeassistant.update(version))
|
||||
|
||||
@api_process
|
||||
def stop(self, request):
|
||||
@ -123,7 +123,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
||||
@api_process
|
||||
def start(self, request):
|
||||
"""Start homeassistant."""
|
||||
return asyncio.shield(self.sys_homeassistant.start())
|
||||
asyncio.shield(self.sys_homeassistant.start())
|
||||
|
||||
@api_process
|
||||
def restart(self, request):
|
||||
|
@ -1,15 +1,18 @@
|
||||
"""Utils for HomeAssistant Proxy."""
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPBadGateway, HTTPInternalServerError
|
||||
from aiohttp.web_exceptions import (
|
||||
HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized)
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import async_timeout
|
||||
|
||||
from ..const import HEADER_HA_ACCESS
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import HomeAssistantAuthError, HomeAssistantAPIError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -23,49 +26,43 @@ class APIProxy(CoreSysAttributes):
|
||||
addon = self.sys_addons.from_uuid(hassio_token)
|
||||
|
||||
if not addon:
|
||||
_LOGGER.warning("Unknown Home-Assistant API access!")
|
||||
_LOGGER.warning("Unknown HomeAssistant API access!")
|
||||
elif not addon.access_homeassistant_api:
|
||||
_LOGGER.warning("Not permitted API access: %s", addon.slug)
|
||||
else:
|
||||
_LOGGER.info("%s access from %s", request.path, addon.slug)
|
||||
return
|
||||
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
@asynccontextmanager
|
||||
async def _api_client(self, request, path, timeout=300):
|
||||
"""Return a client request with proxy origin for Home-Assistant."""
|
||||
url = f"{self.sys_homeassistant.api_url}/api/{path}"
|
||||
|
||||
try:
|
||||
data = None
|
||||
headers = {}
|
||||
method = getattr(self.sys_websession_ssl, request.method.lower())
|
||||
params = request.query or None
|
||||
|
||||
# read data
|
||||
with async_timeout.timeout(30):
|
||||
data = await request.read()
|
||||
|
||||
if data:
|
||||
headers.update({CONTENT_TYPE: request.content_type})
|
||||
content_type = request.content_type
|
||||
else:
|
||||
content_type = None
|
||||
|
||||
# need api password?
|
||||
if self.sys_homeassistant.api_password:
|
||||
headers = {
|
||||
HEADER_HA_ACCESS: self.sys_homeassistant.api_password,
|
||||
}
|
||||
|
||||
# reset headers
|
||||
if not headers:
|
||||
headers = None
|
||||
|
||||
client = await method(
|
||||
url, data=data, headers=headers, timeout=timeout,
|
||||
params=params
|
||||
)
|
||||
|
||||
return client
|
||||
async with self.sys_homeassistant.make_request(
|
||||
request.method.lower(), f'api/{path}',
|
||||
content_type=content_type,
|
||||
data=data,
|
||||
timeout=timeout,
|
||||
) as resp:
|
||||
yield resp
|
||||
return
|
||||
|
||||
except HomeAssistantAuthError:
|
||||
_LOGGER.error("Authenticate error on API for request %s", path)
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Client error on API %s request %s.", path, err)
|
||||
|
||||
_LOGGER.error("Client error on API %s request %s", path, err)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Client timeout error on API request %s.", path)
|
||||
_LOGGER.error("Client timeout error on API request %s", path)
|
||||
|
||||
raise HTTPBadGateway()
|
||||
|
||||
@ -74,30 +71,29 @@ class APIProxy(CoreSysAttributes):
|
||||
self._check_access(request)
|
||||
|
||||
_LOGGER.info("Home-Assistant EventStream start")
|
||||
client = await self._api_client(request, 'stream', timeout=None)
|
||||
async with self._api_client(request, 'stream', timeout=None) as client:
|
||||
response = web.StreamResponse()
|
||||
response.content_type = request.headers.get(CONTENT_TYPE)
|
||||
try:
|
||||
await response.prepare(request)
|
||||
while True:
|
||||
data = await client.content.read(10)
|
||||
if not data:
|
||||
await response.write_eof()
|
||||
break
|
||||
await response.write(data)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = request.headers.get(CONTENT_TYPE)
|
||||
try:
|
||||
await response.prepare(request)
|
||||
while True:
|
||||
data = await client.content.read(10)
|
||||
if not data:
|
||||
await response.write_eof()
|
||||
break
|
||||
await response.write(data)
|
||||
except aiohttp.ClientError:
|
||||
await response.write_eof()
|
||||
|
||||
except aiohttp.ClientError:
|
||||
await response.write_eof()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
client.close()
|
||||
_LOGGER.info("Home-Assistant EventStream close")
|
||||
|
||||
finally:
|
||||
client.close()
|
||||
_LOGGER.info("Home-Assistant EventStream close")
|
||||
|
||||
return response
|
||||
return response
|
||||
|
||||
async def api(self, request):
|
||||
"""Proxy HomeAssistant API Requests."""
|
||||
@ -105,14 +101,13 @@ class APIProxy(CoreSysAttributes):
|
||||
|
||||
# Normal request
|
||||
path = request.match_info.get('path', '')
|
||||
client = await self._api_client(request, path)
|
||||
|
||||
data = await client.read()
|
||||
return web.Response(
|
||||
body=data,
|
||||
status=client.status,
|
||||
content_type=client.content_type
|
||||
)
|
||||
async with self._api_client(request, path) as client:
|
||||
data = await client.read()
|
||||
return web.Response(
|
||||
body=data,
|
||||
status=client.status,
|
||||
content_type=client.content_type
|
||||
)
|
||||
|
||||
async def _websocket_client(self):
|
||||
"""Initialize a websocket api connection."""
|
||||
@ -123,19 +118,44 @@ class APIProxy(CoreSysAttributes):
|
||||
url, heartbeat=60, verify_ssl=False)
|
||||
|
||||
# handle authentication
|
||||
for _ in range(2):
|
||||
data = await client.receive_json()
|
||||
if data.get('type') == 'auth_ok':
|
||||
return client
|
||||
elif data.get('type') == 'auth_required':
|
||||
await client.send_json({
|
||||
'type': 'auth',
|
||||
'api_password': self.sys_homeassistant.api_password,
|
||||
})
|
||||
data = await client.receive_json()
|
||||
|
||||
_LOGGER.error("Authentication to Home-Assistant websocket")
|
||||
if data.get('type') == 'auth_ok':
|
||||
return client
|
||||
|
||||
except (aiohttp.ClientError, RuntimeError) as err:
|
||||
if data.get('type') != 'auth_required':
|
||||
# Invalid protocol
|
||||
_LOGGER.error(
|
||||
'Got unexpected response from HA websocket: %s', data)
|
||||
raise HTTPBadGateway()
|
||||
|
||||
if self.sys_homeassistant.refresh_token:
|
||||
await self.sys_homeassistant.ensure_access_token()
|
||||
await client.send_json({
|
||||
'type': 'auth',
|
||||
'access_token': self.sys_homeassistant.access_token,
|
||||
})
|
||||
else:
|
||||
await client.send_json({
|
||||
'type': 'auth',
|
||||
'api_password': self.sys_homeassistant.api_password,
|
||||
})
|
||||
|
||||
data = await client.receive_json()
|
||||
|
||||
if data.get('type') == 'auth_ok':
|
||||
return client
|
||||
|
||||
# Renew the Token is invalid
|
||||
if (data.get('type') == 'invalid_auth' and
|
||||
self.sys_homeassistant.refresh_token):
|
||||
self.sys_homeassistant.access_token = None
|
||||
return await self._websocket_client()
|
||||
|
||||
_LOGGER.error(
|
||||
"Failed authentication to Home-Assistant websocket: %s", data)
|
||||
|
||||
except (RuntimeError, HomeAssistantAPIError) as err:
|
||||
_LOGGER.error("Client error on websocket API %s.", err)
|
||||
|
||||
raise HTTPBadGateway()
|
||||
@ -157,13 +177,19 @@ class APIProxy(CoreSysAttributes):
|
||||
|
||||
# Check API access
|
||||
response = await server.receive_json()
|
||||
hassio_token = response.get('api_password')
|
||||
hassio_token = (response.get('api_password') or
|
||||
response.get('access_token'))
|
||||
addon = self.sys_addons.from_uuid(hassio_token)
|
||||
|
||||
if not addon:
|
||||
if not addon or not addon.access_homeassistant_api:
|
||||
_LOGGER.warning("Unauthorized websocket access!")
|
||||
else:
|
||||
_LOGGER.info("Websocket access from %s", addon.slug)
|
||||
await server.send_json({
|
||||
'type': 'auth_invalid',
|
||||
'message': 'Invalid access',
|
||||
})
|
||||
return server
|
||||
|
||||
_LOGGER.info("Websocket access from %s", addon.slug)
|
||||
|
||||
await server.send_json({
|
||||
'type': 'auth_ok',
|
||||
|
@ -50,7 +50,7 @@ CONTENT_TYPE_JSON = 'application/json'
|
||||
CONTENT_TYPE_TEXT = 'text/plain'
|
||||
CONTENT_TYPE_TAR = 'application/tar'
|
||||
HEADER_HA_ACCESS = 'x-ha-access'
|
||||
HEADER_TOKEN = 'X-HASSIO-KEY'
|
||||
HEADER_TOKEN = 'x-hassio-key'
|
||||
|
||||
ENV_TOKEN = 'HASSIO_TOKEN'
|
||||
ENV_TIME = 'TZ'
|
||||
@ -174,6 +174,7 @@ ATTR_DEVICETREE = 'devicetree'
|
||||
ATTR_CPE = 'cpe'
|
||||
ATTR_BOARD = 'board'
|
||||
ATTR_HASSOS = 'hassos'
|
||||
ATTR_REFRESH_TOKEN = 'refresh_token'
|
||||
|
||||
SERVICE_MQTT = 'mqtt'
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
"""Core Exceptions."""
|
||||
import asyncio
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
||||
class HassioError(Exception):
|
||||
@ -11,6 +14,29 @@ class HassioNotSupportedError(HassioError):
|
||||
pass
|
||||
|
||||
|
||||
# HomeAssistant
|
||||
|
||||
class HomeAssistantError(HassioError):
|
||||
"""Home Assistant exception."""
|
||||
pass
|
||||
|
||||
|
||||
class HomeAssistantUpdateError(HomeAssistantError):
|
||||
"""Error on update of a Home Assistant."""
|
||||
pass
|
||||
|
||||
|
||||
class HomeAssistantAuthError(HomeAssistantError):
|
||||
"""Home Assistant Auth API exception."""
|
||||
pass
|
||||
|
||||
|
||||
class HomeAssistantAPIError(
|
||||
HomeAssistantAuthError, asyncio.TimeoutError, aiohttp.ClientError):
|
||||
"""Home Assistant API exception."""
|
||||
pass
|
||||
|
||||
|
||||
# HassOS
|
||||
|
||||
class HassOSError(HassioError):
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""HomeAssistant control object."""
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@ -7,15 +8,19 @@ import socket
|
||||
import time
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
from aiohttp import hdrs
|
||||
import attr
|
||||
|
||||
from .const import (
|
||||
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
|
||||
ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG,
|
||||
ATTR_WAIT_BOOT, HEADER_HA_ACCESS, CONTENT_TYPE_JSON)
|
||||
ATTR_WAIT_BOOT, ATTR_REFRESH_TOKEN,
|
||||
HEADER_HA_ACCESS)
|
||||
from .coresys import CoreSysAttributes
|
||||
from .docker.homeassistant import DockerHomeAssistant
|
||||
from .exceptions import (
|
||||
HomeAssistantUpdateError, HomeAssistantError, HomeAssistantAPIError,
|
||||
HomeAssistantAuthError)
|
||||
from .utils import convert_to_ascii, process_lock
|
||||
from .utils.json import JsonConfig
|
||||
from .validate import SCHEMA_HASS_CONFIG
|
||||
@ -25,7 +30,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
ConfigResult = attr.make_class('ConfigResult', ['valid', 'log'])
|
||||
ConfigResult = attr.make_class('ConfigResult', ['valid', 'log'], frozen=True)
|
||||
|
||||
|
||||
class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
@ -38,6 +43,8 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
self.instance = DockerHomeAssistant(coresys)
|
||||
self.lock = asyncio.Lock(loop=coresys.loop)
|
||||
self._error_state = False
|
||||
# We don't persist access tokens. Instead we fetch new ones when needed
|
||||
self.access_token = None
|
||||
|
||||
async def load(self):
|
||||
"""Prepare HomeAssistant object."""
|
||||
@ -175,6 +182,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
"""Return a UUID of this HomeAssistant."""
|
||||
return self._data[ATTR_UUID]
|
||||
|
||||
@property
|
||||
def refresh_token(self):
|
||||
"""Return the refresh token to authenticate with HomeAssistant."""
|
||||
return self._data.get(ATTR_REFRESH_TOKEN)
|
||||
|
||||
@refresh_token.setter
|
||||
def refresh_token(self, value):
|
||||
"""Set Home Assistant refresh_token."""
|
||||
self._data[ATTR_REFRESH_TOKEN] = value
|
||||
|
||||
@process_lock
|
||||
async def install_landingpage(self):
|
||||
"""Install a landingpage."""
|
||||
@ -186,7 +203,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
await asyncio.sleep(60)
|
||||
|
||||
# Run landingpage after installation
|
||||
await self._start()
|
||||
_LOGGER.info("Start landingpage")
|
||||
try:
|
||||
await self._start()
|
||||
except HomeAssistantError:
|
||||
_LOGGER.warning("Can't start landingpage")
|
||||
|
||||
@process_lock
|
||||
async def install(self):
|
||||
@ -205,9 +226,15 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
|
||||
# finishing
|
||||
_LOGGER.info("HomeAssistant docker now installed")
|
||||
if self.boot:
|
||||
try:
|
||||
if not self.boot:
|
||||
return
|
||||
_LOGGER.info("Start HomeAssistant")
|
||||
await self._start()
|
||||
await self.instance.cleanup()
|
||||
except HomeAssistantError:
|
||||
_LOGGER.error("Can't start HomeAssistant!")
|
||||
finally:
|
||||
await self.instance.cleanup()
|
||||
|
||||
@process_lock
|
||||
async def update(self, version=None):
|
||||
@ -219,32 +246,37 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
|
||||
if exists and version == self.instance.version:
|
||||
_LOGGER.warning("Version %s is already installed", version)
|
||||
return False
|
||||
return HomeAssistantUpdateError()
|
||||
|
||||
# process a update
|
||||
async def _update(to_version):
|
||||
"""Run Home Assistant update."""
|
||||
try:
|
||||
return await self.instance.update(to_version)
|
||||
_LOGGER.info("Update HomeAssistant to version %s", to_version)
|
||||
if not await self.instance.update(to_version):
|
||||
raise HomeAssistantUpdateError()
|
||||
finally:
|
||||
if running:
|
||||
await self._start()
|
||||
_LOGGER.info("Successfull run HomeAssistant %s", to_version)
|
||||
|
||||
# Update Home Assistant
|
||||
ret = await _update(version)
|
||||
with suppress(HomeAssistantError):
|
||||
await _update(version)
|
||||
return
|
||||
|
||||
# Update going wrong, revert it
|
||||
if self.error_state and rollback:
|
||||
_LOGGER.fatal("Home Assistant update fails -> rollback!")
|
||||
ret = await _update(rollback)
|
||||
|
||||
return ret
|
||||
_LOGGER.fatal("HomeAssistant update fails -> rollback!")
|
||||
await _update(rollback)
|
||||
else:
|
||||
raise HomeAssistantUpdateError()
|
||||
|
||||
async def _start(self):
|
||||
"""Start HomeAssistant docker & wait."""
|
||||
if not await self.instance.run():
|
||||
return False
|
||||
return await self._block_till_run()
|
||||
raise HomeAssistantError()
|
||||
await self._block_till_run()
|
||||
|
||||
@process_lock
|
||||
def start(self):
|
||||
@ -266,7 +298,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
async def restart(self):
|
||||
"""Restart HomeAssistant docker."""
|
||||
await self.instance.stop()
|
||||
return await self._start()
|
||||
await self._start()
|
||||
|
||||
def logs(self):
|
||||
"""Get HomeAssistant docker logs.
|
||||
@ -309,7 +341,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
|
||||
# if not valid
|
||||
if result.exit_code is None:
|
||||
return ConfigResult(False, "")
|
||||
raise HomeAssistantError()
|
||||
|
||||
# parse output
|
||||
log = convert_to_ascii(result.output)
|
||||
@ -317,51 +349,84 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
return ConfigResult(False, log)
|
||||
return ConfigResult(True, log)
|
||||
|
||||
async def check_api_state(self):
|
||||
"""Check if Home-Assistant up and running."""
|
||||
url = f"{self.api_url}/api/"
|
||||
header = {CONTENT_TYPE: CONTENT_TYPE_JSON}
|
||||
async def ensure_access_token(self):
|
||||
"""Ensures there is an access token."""
|
||||
if self.access_token is not None:
|
||||
return
|
||||
|
||||
if self.api_password:
|
||||
header.update({HEADER_HA_ACCESS: self.api_password})
|
||||
|
||||
try:
|
||||
# pylint: disable=bad-continuation
|
||||
with suppress(asyncio.TimeoutError, aiohttp.ClientError):
|
||||
async with self.sys_websession_ssl.get(
|
||||
url, headers=header, timeout=30) as request:
|
||||
status = request.status
|
||||
f"{self.api_url}/auth/token",
|
||||
timeout=30,
|
||||
data={
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": self.refresh_token
|
||||
}
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
_LOGGER.error("Authenticate problem with HomeAssistant!")
|
||||
raise HomeAssistantAuthError()
|
||||
tokens = await resp.json()
|
||||
self.access_token = tokens['access_token']
|
||||
return
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
return False
|
||||
_LOGGER.error("Can't update HomeAssistant access token!")
|
||||
raise HomeAssistantAPIError()
|
||||
|
||||
if status not in (200, 201):
|
||||
_LOGGER.warning("Home-Assistant API config missmatch")
|
||||
return True
|
||||
@asynccontextmanager
|
||||
async def make_request(self, method, path, json=None, content_type=None,
|
||||
data=None, timeout=30):
|
||||
"""Async context manager to make a request with right auth."""
|
||||
url = f"{self.api_url}/{path}"
|
||||
headers = {}
|
||||
|
||||
if content_type is not None:
|
||||
headers[hdrs.CONTENT_TYPE] = content_type
|
||||
|
||||
elif self.api_password:
|
||||
headers[HEADER_HA_ACCESS] = self.api_password
|
||||
|
||||
for _ in (1, 2):
|
||||
# Prepare Access token
|
||||
if self.refresh_token:
|
||||
await self.ensure_access_token()
|
||||
headers[hdrs.AUTHORIZATION] = f'Bearer {self.access_token}'
|
||||
|
||||
async with getattr(self.sys_websession_ssl, method)(
|
||||
url, timeout=timeout, json=json
|
||||
) as resp:
|
||||
# Access token expired
|
||||
if resp.status == 401 and self.refresh_token:
|
||||
self.access_token = None
|
||||
continue
|
||||
yield resp
|
||||
return
|
||||
|
||||
raise HomeAssistantAPIError()
|
||||
|
||||
async def check_api_state(self):
|
||||
"""Return True if Home-Assistant up and running."""
|
||||
with suppress(HomeAssistantAPIError):
|
||||
async with self.make_request('get', 'api/') as resp:
|
||||
if resp.status in (200, 201):
|
||||
return True
|
||||
err = resp.status
|
||||
|
||||
_LOGGER.warning("Home-Assistant API config missmatch: %d", err)
|
||||
return False
|
||||
|
||||
async def send_event(self, event_type, event_data=None):
|
||||
"""Send event to Home-Assistant."""
|
||||
url = f"{self.api_url}/api/events/{event_type}"
|
||||
header = {CONTENT_TYPE: CONTENT_TYPE_JSON}
|
||||
with suppress(HomeAssistantAPIError):
|
||||
async with self.make_request(
|
||||
'get', f'api/events/{event_type}'
|
||||
) as resp:
|
||||
if resp.status in (200, 201):
|
||||
return
|
||||
err = resp.status
|
||||
|
||||
if self.api_password:
|
||||
header.update({HEADER_HA_ACCESS: self.api_password})
|
||||
|
||||
try:
|
||||
# pylint: disable=bad-continuation
|
||||
async with self.sys_websession_ssl.post(
|
||||
url, headers=header, timeout=30,
|
||||
json=event_data) as request:
|
||||
status = request.status
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
||||
_LOGGER.warning(
|
||||
"Home-Assistant event %s fails: %s", event_type, err)
|
||||
return False
|
||||
|
||||
if status not in (200, 201):
|
||||
_LOGGER.warning("Home-Assistant event %s fails", event_type)
|
||||
return False
|
||||
return True
|
||||
_LOGGER.warning("HomeAssistant event %s fails: %s", event_type, err)
|
||||
return HomeAssistantError()
|
||||
|
||||
async def _block_till_run(self):
|
||||
"""Block until Home-Assistant is booting up or startup timeout."""
|
||||
@ -374,27 +439,28 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||
result = sock.connect_ex((str(self.api_ip), self.api_port))
|
||||
sock.close()
|
||||
|
||||
# Check if the port is available
|
||||
if result == 0:
|
||||
return True
|
||||
return False
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
while time.monotonic() - start_time < self.wait_boot:
|
||||
# Check if API response
|
||||
if await self.sys_run_in_executor(check_port):
|
||||
_LOGGER.info("Detect a running Home-Assistant instance")
|
||||
_LOGGER.info("Detect a running HomeAssistant instance")
|
||||
self._error_state = False
|
||||
return True
|
||||
return
|
||||
|
||||
# wait and don't hit the system
|
||||
await asyncio.sleep(10)
|
||||
|
||||
# Check if Container is is_running
|
||||
if not await self.instance.is_running():
|
||||
_LOGGER.error("Home Assistant is crashed!")
|
||||
break
|
||||
|
||||
# wait and don't hit the system
|
||||
await asyncio.sleep(10)
|
||||
|
||||
_LOGGER.warning("Don't wait anymore of Home-Assistant startup!")
|
||||
_LOGGER.warning("Don't wait anymore of HomeAssistant startup!")
|
||||
self._error_state = True
|
||||
return False
|
||||
raise HomeAssistantError()
|
||||
|
@ -131,5 +131,7 @@ class Tasks(CoreSysAttributes):
|
||||
return
|
||||
|
||||
_LOGGER.error("Watchdog found a problem with Home-Assistant API!")
|
||||
await self.sys_homeassistant.restart()
|
||||
self._cache[HASS_WATCHDOG_API] = 0
|
||||
try:
|
||||
await self.sys_homeassistant.restart()
|
||||
finally:
|
||||
self._cache[HASS_WATCHDOG_API] = 0
|
||||
|
@ -9,7 +9,8 @@ from .const import (
|
||||
ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS,
|
||||
ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO,
|
||||
ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG,
|
||||
ATTR_WAIT_BOOT, ATTR_UUID, CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
|
||||
ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN,
|
||||
CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
|
||||
|
||||
|
||||
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
|
||||
@ -88,6 +89,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({
|
||||
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
|
||||
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
|
||||
vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_WAIT_BOOT, default=600):
|
||||
|
3
pylintrc
3
pylintrc
@ -33,7 +33,8 @@ disable=
|
||||
too-few-public-methods,
|
||||
abstract-method,
|
||||
no-else-return,
|
||||
useless-return
|
||||
useless-return,
|
||||
not-async-context-manager
|
||||
|
||||
[EXCEPTIONS]
|
||||
overgeneral-exceptions=Exception,HomeAssistantError
|
||||
|
Loading…
x
Reference in New Issue
Block a user