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:
Paulus Schoutsen 2018-07-20 16:55:48 +02:00 committed by Pascal Vizeli
parent 1b481e0b37
commit 4df42e054d
10 changed files with 275 additions and 157 deletions

View File

@ -1,12 +1,6 @@
sudo: false sudo: true
matrix: dist: xenial
fast_finish: true
include:
- python: "3.6"
cache:
directories:
- $HOME/.cache/pip
install: pip install -U tox install: pip install -U tox
language: python language: python
python: 3.7
script: tox script: tox

View File

@ -10,7 +10,7 @@ from ..const import (
ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT, ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT,
ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX,
ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_WAIT_BOOT, ATTR_MACHINE, 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 ..coresys import CoreSysAttributes
from ..validate import NETWORK_PORT, DOCKER_IMAGE from ..validate import NETWORK_PORT, DOCKER_IMAGE
@ -30,6 +30,8 @@ SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_WATCHDOG): vol.Boolean(), vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT): vol.Optional(ATTR_WAIT_BOOT):
vol.All(vol.Coerce(int), vol.Range(min=60)), 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({ SCHEMA_VERSION = vol.Schema({
@ -83,8 +85,10 @@ class APIHomeAssistant(CoreSysAttributes):
if ATTR_WAIT_BOOT in body: if ATTR_WAIT_BOOT in body:
self.sys_homeassistant.wait_boot = body[ATTR_WAIT_BOOT] 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() self.sys_homeassistant.save_data()
return True
@api_process @api_process
async def stats(self, request): async def stats(self, request):
@ -109,11 +113,7 @@ class APIHomeAssistant(CoreSysAttributes):
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version) version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version)
if version == self.sys_homeassistant.version: await asyncio.shield(self.sys_homeassistant.update(version))
raise RuntimeError("Version {} is already in use".format(version))
return await asyncio.shield(
self.sys_homeassistant.update(version))
@api_process @api_process
def stop(self, request): def stop(self, request):
@ -123,7 +123,7 @@ class APIHomeAssistant(CoreSysAttributes):
@api_process @api_process
def start(self, request): def start(self, request):
"""Start homeassistant.""" """Start homeassistant."""
return asyncio.shield(self.sys_homeassistant.start()) asyncio.shield(self.sys_homeassistant.start())
@api_process @api_process
def restart(self, request): def restart(self, request):

View File

@ -1,15 +1,18 @@
"""Utils for HomeAssistant Proxy.""" """Utils for HomeAssistant Proxy."""
import asyncio import asyncio
from contextlib import asynccontextmanager
import logging import logging
import aiohttp import aiohttp
from aiohttp import web 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 from aiohttp.hdrs import CONTENT_TYPE
import async_timeout import async_timeout
from ..const import HEADER_HA_ACCESS from ..const import HEADER_HA_ACCESS
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import HomeAssistantAuthError, HomeAssistantAPIError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -23,49 +26,43 @@ class APIProxy(CoreSysAttributes):
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_uuid(hassio_token)
if not addon: 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: else:
_LOGGER.info("%s access from %s", request.path, addon.slug) _LOGGER.info("%s access from %s", request.path, addon.slug)
return
raise HTTPUnauthorized()
@asynccontextmanager
async def _api_client(self, request, path, timeout=300): async def _api_client(self, request, path, timeout=300):
"""Return a client request with proxy origin for Home-Assistant.""" """Return a client request with proxy origin for Home-Assistant."""
url = f"{self.sys_homeassistant.api_url}/api/{path}"
try: try:
data = None
headers = {}
method = getattr(self.sys_websession_ssl, request.method.lower())
params = request.query or None
# read data # read data
with async_timeout.timeout(30): with async_timeout.timeout(30):
data = await request.read() data = await request.read()
if data: if data:
headers.update({CONTENT_TYPE: request.content_type}) content_type = request.content_type
else:
content_type = None
# need api password? async with self.sys_homeassistant.make_request(
if self.sys_homeassistant.api_password: request.method.lower(), f'api/{path}',
headers = { content_type=content_type,
HEADER_HA_ACCESS: self.sys_homeassistant.api_password, data=data,
} timeout=timeout,
) as resp:
# reset headers yield resp
if not headers: return
headers = None
client = await method(
url, data=data, headers=headers, timeout=timeout,
params=params
)
return client
except HomeAssistantAuthError:
_LOGGER.error("Authenticate error on API for request %s", path)
except aiohttp.ClientError as err: 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: 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() raise HTTPBadGateway()
@ -74,30 +71,29 @@ class APIProxy(CoreSysAttributes):
self._check_access(request) self._check_access(request)
_LOGGER.info("Home-Assistant EventStream start") _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() except aiohttp.ClientError:
response.content_type = request.headers.get(CONTENT_TYPE) await response.write_eof()
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: except asyncio.CancelledError:
await response.write_eof() pass
except asyncio.CancelledError: finally:
pass client.close()
_LOGGER.info("Home-Assistant EventStream close")
finally: return response
client.close()
_LOGGER.info("Home-Assistant EventStream close")
return response
async def api(self, request): async def api(self, request):
"""Proxy HomeAssistant API Requests.""" """Proxy HomeAssistant API Requests."""
@ -105,14 +101,13 @@ class APIProxy(CoreSysAttributes):
# Normal request # Normal request
path = request.match_info.get('path', '') path = request.match_info.get('path', '')
client = await self._api_client(request, path) async with self._api_client(request, path) as client:
data = await client.read()
data = await client.read() return web.Response(
return web.Response( body=data,
body=data, status=client.status,
status=client.status, content_type=client.content_type
content_type=client.content_type )
)
async def _websocket_client(self): async def _websocket_client(self):
"""Initialize a websocket api connection.""" """Initialize a websocket api connection."""
@ -123,19 +118,44 @@ class APIProxy(CoreSysAttributes):
url, heartbeat=60, verify_ssl=False) url, heartbeat=60, verify_ssl=False)
# handle authentication # handle authentication
for _ in range(2): data = await client.receive_json()
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,
})
_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) _LOGGER.error("Client error on websocket API %s.", err)
raise HTTPBadGateway() raise HTTPBadGateway()
@ -157,13 +177,19 @@ class APIProxy(CoreSysAttributes):
# Check API access # Check API access
response = await server.receive_json() 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) 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!") _LOGGER.warning("Unauthorized websocket access!")
else: await server.send_json({
_LOGGER.info("Websocket access from %s", addon.slug) 'type': 'auth_invalid',
'message': 'Invalid access',
})
return server
_LOGGER.info("Websocket access from %s", addon.slug)
await server.send_json({ await server.send_json({
'type': 'auth_ok', 'type': 'auth_ok',

View File

@ -50,7 +50,7 @@ CONTENT_TYPE_JSON = 'application/json'
CONTENT_TYPE_TEXT = 'text/plain' CONTENT_TYPE_TEXT = 'text/plain'
CONTENT_TYPE_TAR = 'application/tar' CONTENT_TYPE_TAR = 'application/tar'
HEADER_HA_ACCESS = 'x-ha-access' HEADER_HA_ACCESS = 'x-ha-access'
HEADER_TOKEN = 'X-HASSIO-KEY' HEADER_TOKEN = 'x-hassio-key'
ENV_TOKEN = 'HASSIO_TOKEN' ENV_TOKEN = 'HASSIO_TOKEN'
ENV_TIME = 'TZ' ENV_TIME = 'TZ'
@ -174,6 +174,7 @@ ATTR_DEVICETREE = 'devicetree'
ATTR_CPE = 'cpe' ATTR_CPE = 'cpe'
ATTR_BOARD = 'board' ATTR_BOARD = 'board'
ATTR_HASSOS = 'hassos' ATTR_HASSOS = 'hassos'
ATTR_REFRESH_TOKEN = 'refresh_token'
SERVICE_MQTT = 'mqtt' SERVICE_MQTT = 'mqtt'

View File

@ -1,4 +1,7 @@
"""Core Exceptions.""" """Core Exceptions."""
import asyncio
import aiohttp
class HassioError(Exception): class HassioError(Exception):
@ -11,6 +14,29 @@ class HassioNotSupportedError(HassioError):
pass 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 # HassOS
class HassOSError(HassioError): class HassOSError(HassioError):

View File

@ -1,5 +1,6 @@
"""HomeAssistant control object.""" """HomeAssistant control object."""
import asyncio import asyncio
from contextlib import asynccontextmanager, suppress
import logging import logging
import os import os
import re import re
@ -7,15 +8,19 @@ import socket
import time import time
import aiohttp import aiohttp
from aiohttp.hdrs import CONTENT_TYPE from aiohttp import hdrs
import attr import attr
from .const import ( from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID, FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, 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 .coresys import CoreSysAttributes
from .docker.homeassistant import DockerHomeAssistant from .docker.homeassistant import DockerHomeAssistant
from .exceptions import (
HomeAssistantUpdateError, HomeAssistantError, HomeAssistantAPIError,
HomeAssistantAuthError)
from .utils import convert_to_ascii, process_lock from .utils import convert_to_ascii, process_lock
from .utils.json import JsonConfig from .utils.json import JsonConfig
from .validate import SCHEMA_HASS_CONFIG from .validate import SCHEMA_HASS_CONFIG
@ -25,7 +30,7 @@ _LOGGER = logging.getLogger(__name__)
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
# pylint: disable=invalid-name # pylint: disable=invalid-name
ConfigResult = attr.make_class('ConfigResult', ['valid', 'log']) ConfigResult = attr.make_class('ConfigResult', ['valid', 'log'], frozen=True)
class HomeAssistant(JsonConfig, CoreSysAttributes): class HomeAssistant(JsonConfig, CoreSysAttributes):
@ -38,6 +43,8 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self.instance = DockerHomeAssistant(coresys) self.instance = DockerHomeAssistant(coresys)
self.lock = asyncio.Lock(loop=coresys.loop) self.lock = asyncio.Lock(loop=coresys.loop)
self._error_state = False 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): async def load(self):
"""Prepare HomeAssistant object.""" """Prepare HomeAssistant object."""
@ -175,6 +182,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Return a UUID of this HomeAssistant.""" """Return a UUID of this HomeAssistant."""
return self._data[ATTR_UUID] 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 @process_lock
async def install_landingpage(self): async def install_landingpage(self):
"""Install a landingpage.""" """Install a landingpage."""
@ -186,7 +203,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await asyncio.sleep(60) await asyncio.sleep(60)
# Run landingpage after installation # 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 @process_lock
async def install(self): async def install(self):
@ -205,9 +226,15 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
# finishing # finishing
_LOGGER.info("HomeAssistant docker now installed") _LOGGER.info("HomeAssistant docker now installed")
if self.boot: try:
if not self.boot:
return
_LOGGER.info("Start HomeAssistant")
await self._start() await self._start()
await self.instance.cleanup() except HomeAssistantError:
_LOGGER.error("Can't start HomeAssistant!")
finally:
await self.instance.cleanup()
@process_lock @process_lock
async def update(self, version=None): async def update(self, version=None):
@ -219,32 +246,37 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
if exists and version == self.instance.version: if exists and version == self.instance.version:
_LOGGER.warning("Version %s is already installed", version) _LOGGER.warning("Version %s is already installed", version)
return False return HomeAssistantUpdateError()
# process a update # process a update
async def _update(to_version): async def _update(to_version):
"""Run Home Assistant update.""" """Run Home Assistant update."""
try: 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: finally:
if running: if running:
await self._start() await self._start()
_LOGGER.info("Successfull run HomeAssistant %s", to_version)
# Update Home Assistant # Update Home Assistant
ret = await _update(version) with suppress(HomeAssistantError):
await _update(version)
return
# Update going wrong, revert it # Update going wrong, revert it
if self.error_state and rollback: if self.error_state and rollback:
_LOGGER.fatal("Home Assistant update fails -> rollback!") _LOGGER.fatal("HomeAssistant update fails -> rollback!")
ret = await _update(rollback) await _update(rollback)
else:
return ret raise HomeAssistantUpdateError()
async def _start(self): async def _start(self):
"""Start HomeAssistant docker & wait.""" """Start HomeAssistant docker & wait."""
if not await self.instance.run(): if not await self.instance.run():
return False raise HomeAssistantError()
return await self._block_till_run() await self._block_till_run()
@process_lock @process_lock
def start(self): def start(self):
@ -266,7 +298,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def restart(self): async def restart(self):
"""Restart HomeAssistant docker.""" """Restart HomeAssistant docker."""
await self.instance.stop() await self.instance.stop()
return await self._start() await self._start()
def logs(self): def logs(self):
"""Get HomeAssistant docker logs. """Get HomeAssistant docker logs.
@ -309,7 +341,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
# if not valid # if not valid
if result.exit_code is None: if result.exit_code is None:
return ConfigResult(False, "") raise HomeAssistantError()
# parse output # parse output
log = convert_to_ascii(result.output) log = convert_to_ascii(result.output)
@ -317,51 +349,84 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
return ConfigResult(False, log) return ConfigResult(False, log)
return ConfigResult(True, log) return ConfigResult(True, log)
async def check_api_state(self): async def ensure_access_token(self):
"""Check if Home-Assistant up and running.""" """Ensures there is an access token."""
url = f"{self.api_url}/api/" if self.access_token is not None:
header = {CONTENT_TYPE: CONTENT_TYPE_JSON} return
if self.api_password: with suppress(asyncio.TimeoutError, aiohttp.ClientError):
header.update({HEADER_HA_ACCESS: self.api_password})
try:
# pylint: disable=bad-continuation
async with self.sys_websession_ssl.get( async with self.sys_websession_ssl.get(
url, headers=header, timeout=30) as request: f"{self.api_url}/auth/token",
status = request.status 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): _LOGGER.error("Can't update HomeAssistant access token!")
return False raise HomeAssistantAPIError()
if status not in (200, 201): @asynccontextmanager
_LOGGER.warning("Home-Assistant API config missmatch") async def make_request(self, method, path, json=None, content_type=None,
return True 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): async def send_event(self, event_type, event_data=None):
"""Send event to Home-Assistant.""" """Send event to Home-Assistant."""
url = f"{self.api_url}/api/events/{event_type}" with suppress(HomeAssistantAPIError):
header = {CONTENT_TYPE: CONTENT_TYPE_JSON} 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: _LOGGER.warning("HomeAssistant event %s fails: %s", event_type, err)
header.update({HEADER_HA_ACCESS: self.api_password}) return HomeAssistantError()
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
async def _block_till_run(self): async def _block_till_run(self):
"""Block until Home-Assistant is booting up or startup timeout.""" """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)) result = sock.connect_ex((str(self.api_ip), self.api_port))
sock.close() sock.close()
# Check if the port is available
if result == 0: if result == 0:
return True return True
return False
except OSError: except OSError:
pass pass
return False
while time.monotonic() - start_time < self.wait_boot: while time.monotonic() - start_time < self.wait_boot:
# Check if API response # Check if API response
if await self.sys_run_in_executor(check_port): 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 self._error_state = False
return True return
# wait and don't hit the system
await asyncio.sleep(10)
# Check if Container is is_running # Check if Container is is_running
if not await self.instance.is_running(): if not await self.instance.is_running():
_LOGGER.error("Home Assistant is crashed!") _LOGGER.error("Home Assistant is crashed!")
break break
# wait and don't hit the system _LOGGER.warning("Don't wait anymore of HomeAssistant startup!")
await asyncio.sleep(10)
_LOGGER.warning("Don't wait anymore of Home-Assistant startup!")
self._error_state = True self._error_state = True
return False raise HomeAssistantError()

View File

@ -131,5 +131,7 @@ class Tasks(CoreSysAttributes):
return return
_LOGGER.error("Watchdog found a problem with Home-Assistant API!") _LOGGER.error("Watchdog found a problem with Home-Assistant API!")
await self.sys_homeassistant.restart() try:
self._cache[HASS_WATCHDOG_API] = 0 await self.sys_homeassistant.restart()
finally:
self._cache[HASS_WATCHDOG_API] = 0

View File

@ -9,7 +9,8 @@ from .const import (
ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS,
ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO,
ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, 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\-]+))?$") 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.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), 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_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT, default=600): vol.Optional(ATTR_WAIT_BOOT, default=600):

View File

@ -33,7 +33,8 @@ disable=
too-few-public-methods, too-few-public-methods,
abstract-method, abstract-method,
no-else-return, no-else-return,
useless-return useless-return,
not-async-context-manager
[EXCEPTIONS] [EXCEPTIONS]
overgeneral-exceptions=Exception,HomeAssistantError overgeneral-exceptions=Exception,HomeAssistantError

View File

@ -43,7 +43,7 @@ setup(
'attr==0.3.1', 'attr==0.3.1',
'async_timeout==3.0.0', 'async_timeout==3.0.0',
'aiohttp==3.3.2', 'aiohttp==3.3.2',
'docker==3.3.0', 'docker==3.4.0',
'colorlog==3.1.2', 'colorlog==3.1.2',
'voluptuous==0.11.1', 'voluptuous==0.11.1',
'gitpython==2.1.10', 'gitpython==2.1.10',