mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-27 11:06:32 +00:00
commit
1ec1082068
4
.github/release-drafter.yml
vendored
Normal file
4
.github/release-drafter.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
template: |
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
$CHANGES
|
12
.travis.yml
12
.travis.yml
@ -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
|
||||||
|
9
API.md
9
API.md
@ -273,7 +273,9 @@ return:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "2.3",
|
"version": "2.3",
|
||||||
|
"version_cli": "7",
|
||||||
"version_latest": "2.4",
|
"version_latest": "2.4",
|
||||||
|
"version_cli_latest": "8",
|
||||||
"board": "ova|rpi"
|
"board": "ova|rpi"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -285,6 +287,13 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- POST `/hassos/update/cli`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "optional"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- POST `/hassos/config/sync`
|
- POST `/hassos/config/sync`
|
||||||
|
|
||||||
Load host configs from a USB stick.
|
Load host configs from a USB stick.
|
||||||
|
@ -6,7 +6,6 @@ ENV LANG C.UTF-8
|
|||||||
|
|
||||||
# Setup base
|
# Setup base
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
python3 \
|
|
||||||
git \
|
git \
|
||||||
socat \
|
socat \
|
||||||
glib \
|
glib \
|
||||||
@ -14,12 +13,11 @@ RUN apk add --no-cache \
|
|||||||
eudev-libs \
|
eudev-libs \
|
||||||
&& apk add --no-cache --virtual .build-dependencies \
|
&& apk add --no-cache --virtual .build-dependencies \
|
||||||
make \
|
make \
|
||||||
python3-dev \
|
|
||||||
g++ \
|
g++ \
|
||||||
&& pip3 install --no-cache-dir \
|
&& pip3 install --no-cache-dir \
|
||||||
uvloop==0.10.2 \
|
uvloop==0.10.2 \
|
||||||
cchardet==2.1.1 \
|
cchardet==2.1.1 \
|
||||||
pycryptodome==3.4.11 \
|
pycryptodome==3.6.4 \
|
||||||
&& apk del .build-dependencies
|
&& apk del .build-dependencies
|
||||||
|
|
||||||
# Install HassIO
|
# Install HassIO
|
||||||
|
@ -4,7 +4,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import hassio.bootstrap as bootstrap
|
from hassio import bootstrap
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ from voluptuous.humanize import humanize_error
|
|||||||
|
|
||||||
from .validate import (
|
from .validate import (
|
||||||
validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE)
|
validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE)
|
||||||
from .utils import check_installed
|
from .utils import check_installed, remove_data
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
|
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
|
||||||
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
|
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
|
||||||
@ -636,7 +636,7 @@ class Addon(CoreSysAttributes):
|
|||||||
if self.path_data.is_dir():
|
if self.path_data.is_dir():
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Remove Home-Assistant addon data folder %s", self.path_data)
|
"Remove Home-Assistant addon data folder %s", self.path_data)
|
||||||
shutil.rmtree(str(self.path_data))
|
await remove_data(self.path_data)
|
||||||
|
|
||||||
# Cleanup audio settings
|
# Cleanup audio settings
|
||||||
if self.path_asound.exists():
|
if self.path_asound.exists():
|
||||||
@ -856,12 +856,12 @@ class Addon(CoreSysAttributes):
|
|||||||
# restore data
|
# restore data
|
||||||
def _restore_data():
|
def _restore_data():
|
||||||
"""Restore data."""
|
"""Restore data."""
|
||||||
if self.path_data.is_dir():
|
|
||||||
shutil.rmtree(str(self.path_data), ignore_errors=True)
|
|
||||||
shutil.copytree(str(Path(temp, "data")), str(self.path_data))
|
shutil.copytree(str(Path(temp, "data")), str(self.path_data))
|
||||||
|
|
||||||
|
_LOGGER.info("Restore data for addon %s", self._id)
|
||||||
|
if self.path_data.is_dir():
|
||||||
|
await remove_data(self.path_data)
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Restore data for addon %s", self._id)
|
|
||||||
await self.sys_run_in_executor(_restore_data)
|
await self.sys_run_in_executor(_restore_data)
|
||||||
except shutil.Error as err:
|
except shutil.Error as err:
|
||||||
_LOGGER.error("Can't restore origin data: %s", err)
|
_LOGGER.error("Can't restore origin data: %s", err)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Util addons functions."""
|
"""Util addons functions."""
|
||||||
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@ -33,3 +34,20 @@ def check_installed(method):
|
|||||||
return await method(addon, *args, **kwargs)
|
return await method(addon, *args, **kwargs)
|
||||||
|
|
||||||
return wrap_check
|
return wrap_check
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_data(folder):
|
||||||
|
"""Remove folder and reset privileged."""
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"rm", "-rf", str(folder),
|
||||||
|
stdout=asyncio.subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
|
||||||
|
_, error_msg = await proc.communicate()
|
||||||
|
except OSError as err:
|
||||||
|
error_msg = str(err)
|
||||||
|
|
||||||
|
if proc.returncode == 0:
|
||||||
|
return
|
||||||
|
_LOGGER.error("Can't remove Add-on Data: %s", error_msg)
|
||||||
|
@ -76,6 +76,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp.add_routes([
|
self.webapp.add_routes([
|
||||||
web.get('/hassos/info', api_hassos.info),
|
web.get('/hassos/info', api_hassos.info),
|
||||||
web.post('/hassos/update', api_hassos.update),
|
web.post('/hassos/update', api_hassos.update),
|
||||||
|
web.post('/hassos/update/cli', api_hassos.update_cli),
|
||||||
web.post('/hassos/config/sync', api_hassos.config_sync),
|
web.post('/hassos/config/sync', api_hassos.config_sync),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -5,7 +5,9 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
from ..const import ATTR_VERSION, ATTR_BOARD, ATTR_VERSION_LATEST
|
from ..const import (
|
||||||
|
ATTR_VERSION, ATTR_BOARD, ATTR_VERSION_LATEST, ATTR_VERSION_CLI,
|
||||||
|
ATTR_VERSION_CLI_LATEST)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -23,7 +25,9 @@ class APIHassOS(CoreSysAttributes):
|
|||||||
"""Return hassos information."""
|
"""Return hassos information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_hassos.version,
|
ATTR_VERSION: self.sys_hassos.version,
|
||||||
|
ATTR_VERSION_CLI: self.sys_hassos.version_cli,
|
||||||
ATTR_VERSION_LATEST: self.sys_hassos.version_latest,
|
ATTR_VERSION_LATEST: self.sys_hassos.version_latest,
|
||||||
|
ATTR_VERSION_CLI_LATEST: self.sys_hassos.version_cli_latest,
|
||||||
ATTR_BOARD: self.sys_hassos.board,
|
ATTR_BOARD: self.sys_hassos.board,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +39,14 @@ class APIHassOS(CoreSysAttributes):
|
|||||||
|
|
||||||
await asyncio.shield(self.sys_hassos.update(version))
|
await asyncio.shield(self.sys_hassos.update(version))
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def update_cli(self, request):
|
||||||
|
"""Update HassOS CLI."""
|
||||||
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
|
version = body.get(ATTR_VERSION, self.sys_hassos.version_cli_latest)
|
||||||
|
|
||||||
|
await asyncio.shield(self.sys_hassos.update_cli(version))
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def config_sync(self, request):
|
def config_sync(self, request):
|
||||||
"""Trigger config reload on HassOS."""
|
"""Trigger config reload on HassOS."""
|
||||||
|
@ -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):
|
||||||
|
@ -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,25 @@ 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:
|
||||||
|
break
|
||||||
|
await response.write(data)
|
||||||
|
|
||||||
response = web.StreamResponse()
|
except aiohttp.ClientError:
|
||||||
response.content_type = request.headers.get(CONTENT_TYPE)
|
pass
|
||||||
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:
|
finally:
|
||||||
await response.write_eof()
|
client.close()
|
||||||
|
_LOGGER.info("Home-Assistant EventStream close")
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
return response
|
||||||
pass
|
|
||||||
|
|
||||||
finally:
|
|
||||||
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 +97,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 +114,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 +173,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',
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
|
|
||||||
HASSIO_VERSION = '115'
|
HASSIO_VERSION = '116'
|
||||||
|
|
||||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||||
URL_HASSIO_VERSION = \
|
URL_HASSIO_VERSION = \
|
||||||
@ -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,10 @@ ATTR_DEVICETREE = 'devicetree'
|
|||||||
ATTR_CPE = 'cpe'
|
ATTR_CPE = 'cpe'
|
||||||
ATTR_BOARD = 'board'
|
ATTR_BOARD = 'board'
|
||||||
ATTR_HASSOS = 'hassos'
|
ATTR_HASSOS = 'hassos'
|
||||||
|
ATTR_HASSOS_CLI = 'hassos_cli'
|
||||||
|
ATTR_VERSION_CLI = 'version_cli'
|
||||||
|
ATTR_VERSION_CLI_LATEST = 'version_cli_latest'
|
||||||
|
ATTR_REFRESH_TOKEN = 'refresh_token'
|
||||||
|
|
||||||
SERVICE_MQTT = 'mqtt'
|
SERVICE_MQTT = 'mqtt'
|
||||||
|
|
||||||
|
@ -26,6 +26,9 @@ class HassIO(CoreSysAttributes):
|
|||||||
self.sys_config.timezone = \
|
self.sys_config.timezone = \
|
||||||
await fetch_timezone(self.sys_websession)
|
await fetch_timezone(self.sys_websession)
|
||||||
|
|
||||||
|
# Load Supervisor
|
||||||
|
await self.sys_supervisor.load()
|
||||||
|
|
||||||
# Load DBus
|
# Load DBus
|
||||||
await self.sys_dbus.load()
|
await self.sys_dbus.load()
|
||||||
|
|
||||||
@ -35,9 +38,6 @@ class HassIO(CoreSysAttributes):
|
|||||||
# Load HassOS
|
# Load HassOS
|
||||||
await self.sys_hassos.load()
|
await self.sys_hassos.load()
|
||||||
|
|
||||||
# Load Supervisor
|
|
||||||
await self.sys_supervisor.load()
|
|
||||||
|
|
||||||
# Load Home Assistant
|
# Load Home Assistant
|
||||||
await self.sys_homeassistant.load()
|
await self.sys_homeassistant.load()
|
||||||
|
|
||||||
|
37
hassio/docker/hassos_cli.py
Normal file
37
hassio/docker/hassos_cli.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""HassOS Cli docker object."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import docker
|
||||||
|
|
||||||
|
from .interface import DockerInterface
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DockerHassOSCli(DockerInterface, CoreSysAttributes):
|
||||||
|
"""Docker hassio wrapper for HassOS Cli."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image(self):
|
||||||
|
"""Return name of HassOS cli image."""
|
||||||
|
return f"homeassistant/{self.sys_arch}-hassio-cli"
|
||||||
|
|
||||||
|
def _stop(self):
|
||||||
|
"""Don't need stop."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _attach(self):
|
||||||
|
"""Attach to running docker container.
|
||||||
|
Need run inside executor.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
image = self.sys_docker.images.get(self.image)
|
||||||
|
|
||||||
|
except docker.errors.DockerException:
|
||||||
|
_LOGGER.warning("Can't find a HassOS cli %s", self.image)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self._meta = image.attrs
|
||||||
|
_LOGGER.info("Found HassOS cli %s with version %s",
|
||||||
|
self.image, self.version)
|
@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DockerSupervisor(DockerInterface, CoreSysAttributes):
|
class DockerSupervisor(DockerInterface, CoreSysAttributes):
|
||||||
"""Docker hassio wrapper for HomeAssistant."""
|
"""Docker hassio wrapper for Supervisor."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -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):
|
||||||
|
@ -7,6 +7,7 @@ from cpe import CPE
|
|||||||
|
|
||||||
from .coresys import CoreSysAttributes
|
from .coresys import CoreSysAttributes
|
||||||
from .const import URL_HASSOS_OTA
|
from .const import URL_HASSOS_OTA
|
||||||
|
from .docker.hassos_cli import DockerHassOSCli
|
||||||
from .exceptions import HassOSNotSupportedError, HassOSUpdateError, DBusError
|
from .exceptions import HassOSNotSupportedError, HassOSUpdateError, DBusError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -18,6 +19,7 @@ class HassOS(CoreSysAttributes):
|
|||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize HassOS handler."""
|
"""Initialize HassOS handler."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
|
self.instance = DockerHassOSCli(coresys)
|
||||||
self._available = False
|
self._available = False
|
||||||
self._version = None
|
self._version = None
|
||||||
self._board = None
|
self._board = None
|
||||||
@ -32,11 +34,31 @@ class HassOS(CoreSysAttributes):
|
|||||||
"""Return version of HassOS."""
|
"""Return version of HassOS."""
|
||||||
return self._version
|
return self._version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version_cli(self):
|
||||||
|
"""Return version of HassOS cli."""
|
||||||
|
return self.instance.version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version_latest(self):
|
def version_latest(self):
|
||||||
"""Return version of HassOS."""
|
"""Return version of HassOS."""
|
||||||
return self.sys_updater.version_hassos
|
return self.sys_updater.version_hassos
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version_cli_latest(self):
|
||||||
|
"""Return version of HassOS."""
|
||||||
|
return self.sys_updater.version_hassos_cli
|
||||||
|
|
||||||
|
@property
|
||||||
|
def need_update(self):
|
||||||
|
"""Return true if a HassOS update is available."""
|
||||||
|
return self.version != self.version_latest
|
||||||
|
|
||||||
|
@property
|
||||||
|
def need_cli_update(self):
|
||||||
|
"""Return true if a HassOS cli update is available."""
|
||||||
|
return self.version_cli != self.version_cli_latest
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def board(self):
|
def board(self):
|
||||||
"""Return board name."""
|
"""Return board name."""
|
||||||
@ -56,6 +78,10 @@ class HassOS(CoreSysAttributes):
|
|||||||
try:
|
try:
|
||||||
_LOGGER.info("Fetch OTA update from %s", url)
|
_LOGGER.info("Fetch OTA update from %s", url)
|
||||||
async with self.sys_websession.get(url) as request:
|
async with self.sys_websession.get(url) as request:
|
||||||
|
if request.status != 200:
|
||||||
|
raise HassOSUpdateError()
|
||||||
|
|
||||||
|
# Download RAUCB file
|
||||||
with raucb.open('wb') as ota_file:
|
with raucb.open('wb') as ota_file:
|
||||||
while True:
|
while True:
|
||||||
chunk = await request.content.read(1048576)
|
chunk = await request.content.read(1048576)
|
||||||
@ -86,7 +112,7 @@ class HassOS(CoreSysAttributes):
|
|||||||
cpe = CPE(self.sys_host.info.cpe)
|
cpe = CPE(self.sys_host.info.cpe)
|
||||||
assert cpe.get_product()[0] == 'hassos'
|
assert cpe.get_product()[0] == 'hassos'
|
||||||
except (AssertionError, NotImplementedError):
|
except (AssertionError, NotImplementedError):
|
||||||
_LOGGER.debug("Ignore HassOS")
|
_LOGGER.debug("Found no HassOS")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Store meta data
|
# Store meta data
|
||||||
@ -95,6 +121,7 @@ class HassOS(CoreSysAttributes):
|
|||||||
self._board = cpe.get_target_hardware()[0]
|
self._board = cpe.get_target_hardware()[0]
|
||||||
|
|
||||||
_LOGGER.info("Detect HassOS %s on host system", self.version)
|
_LOGGER.info("Detect HassOS %s on host system", self.version)
|
||||||
|
await self.instance.attach()
|
||||||
|
|
||||||
def config_sync(self):
|
def config_sync(self):
|
||||||
"""Trigger a host config reload from usb.
|
"""Trigger a host config reload from usb.
|
||||||
@ -142,3 +169,17 @@ class HassOS(CoreSysAttributes):
|
|||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"HassOS update fails with: %s", rauc_status.get('LastError'))
|
"HassOS update fails with: %s", rauc_status.get('LastError'))
|
||||||
raise HassOSUpdateError()
|
raise HassOSUpdateError()
|
||||||
|
|
||||||
|
async def update_cli(self, version=None):
|
||||||
|
"""Update local HassOS cli."""
|
||||||
|
version = version or self.version_cli_latest
|
||||||
|
|
||||||
|
if version == self.version_cli:
|
||||||
|
_LOGGER.warning("Version %s is already installed for CLI", version)
|
||||||
|
raise HassOSUpdateError()
|
||||||
|
|
||||||
|
if await self.instance.update(version):
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.error("HassOS CLI update fails.")
|
||||||
|
raise HassOSUpdateError()
|
||||||
|
@ -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()
|
||||||
|
@ -16,7 +16,7 @@ ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL]
|
|||||||
|
|
||||||
def unique_addons(addons_list):
|
def unique_addons(addons_list):
|
||||||
"""Validate that an add-on is unique."""
|
"""Validate that an add-on is unique."""
|
||||||
single = set([addon[ATTR_SLUG] for addon in addons_list])
|
single = set(addon[ATTR_SLUG] for addon in addons_list)
|
||||||
|
|
||||||
if len(single) != len(addons_list):
|
if len(single) != len(addons_list):
|
||||||
raise vol.Invalid("Invalid addon list on snapshot!")
|
raise vol.Invalid("Invalid addon list on snapshot!")
|
||||||
|
@ -10,6 +10,7 @@ HASS_WATCHDOG_API = 'HASS_WATCHDOG_API'
|
|||||||
|
|
||||||
RUN_UPDATE_SUPERVISOR = 29100
|
RUN_UPDATE_SUPERVISOR = 29100
|
||||||
RUN_UPDATE_ADDONS = 57600
|
RUN_UPDATE_ADDONS = 57600
|
||||||
|
RUN_UPDATE_HASSOSCLI = 29100
|
||||||
|
|
||||||
RUN_RELOAD_ADDONS = 21600
|
RUN_RELOAD_ADDONS = 21600
|
||||||
RUN_RELOAD_SNAPSHOTS = 72000
|
RUN_RELOAD_SNAPSHOTS = 72000
|
||||||
@ -35,6 +36,8 @@ class Tasks(CoreSysAttributes):
|
|||||||
self._update_addons, RUN_UPDATE_ADDONS))
|
self._update_addons, RUN_UPDATE_ADDONS))
|
||||||
self.jobs.add(self.sys_scheduler.register_task(
|
self.jobs.add(self.sys_scheduler.register_task(
|
||||||
self._update_supervisor, RUN_UPDATE_SUPERVISOR))
|
self._update_supervisor, RUN_UPDATE_SUPERVISOR))
|
||||||
|
self.jobs.add(self.sys_scheduler.register_task(
|
||||||
|
self._update_hassos_cli, RUN_UPDATE_HASSOSCLI))
|
||||||
|
|
||||||
self.jobs.add(self.sys_scheduler.register_task(
|
self.jobs.add(self.sys_scheduler.register_task(
|
||||||
self.sys_addons.reload, RUN_RELOAD_ADDONS))
|
self.sys_addons.reload, RUN_RELOAD_ADDONS))
|
||||||
@ -79,7 +82,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
if not self.sys_supervisor.need_update:
|
if not self.sys_supervisor.need_update:
|
||||||
return
|
return
|
||||||
|
|
||||||
# don't perform an update on beta/dev channel
|
# don't perform an update on dev channel
|
||||||
if self.sys_dev:
|
if self.sys_dev:
|
||||||
_LOGGER.warning("Ignore Hass.io update on dev channel!")
|
_LOGGER.warning("Ignore Hass.io update on dev channel!")
|
||||||
return
|
return
|
||||||
@ -131,5 +134,20 @@ 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
|
||||||
|
|
||||||
|
async def _update_hassos_cli(self):
|
||||||
|
"""Check and run update of HassOS CLI."""
|
||||||
|
if not self.sys_hassos.need_cli_update:
|
||||||
|
return
|
||||||
|
|
||||||
|
# don't perform an update on dev channel
|
||||||
|
if self.sys_dev:
|
||||||
|
_LOGGER.warning("Ignore HassOS CLI update on dev channel!")
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.info("Found new HassOS CLI version")
|
||||||
|
await self.sys_hassos.update_cli()
|
||||||
|
@ -8,7 +8,7 @@ import aiohttp
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO,
|
URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO,
|
||||||
ATTR_CHANNEL, ATTR_HASSOS)
|
ATTR_CHANNEL, ATTR_HASSOS, ATTR_HASSOS_CLI)
|
||||||
from .coresys import CoreSysAttributes
|
from .coresys import CoreSysAttributes
|
||||||
from .utils import AsyncThrottle
|
from .utils import AsyncThrottle
|
||||||
from .utils.json import JsonConfig
|
from .utils.json import JsonConfig
|
||||||
@ -51,6 +51,11 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
"""Return last version of hassos."""
|
"""Return last version of hassos."""
|
||||||
return self._data.get(ATTR_HASSOS)
|
return self._data.get(ATTR_HASSOS)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version_hassos_cli(self):
|
||||||
|
"""Return last version of hassos cli."""
|
||||||
|
return self._data.get(ATTR_HASSOS_CLI)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channel(self):
|
def channel(self):
|
||||||
"""Return upstream channel of hassio instance."""
|
"""Return upstream channel of hassio instance."""
|
||||||
@ -99,6 +104,7 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
# update hassos version
|
# update hassos version
|
||||||
if self.sys_hassos.available and board:
|
if self.sys_hassos.available and board:
|
||||||
self._data[ATTR_HASSOS] = data['hassos'][board]
|
self._data[ATTR_HASSOS] = data['hassos'][board]
|
||||||
|
self._data[ATTR_HASSOS_CLI] = data['hassos-cli']
|
||||||
|
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
_LOGGER.warning("Can't process version data: %s", err)
|
_LOGGER.warning("Can't process version data: %s", err)
|
||||||
|
@ -247,7 +247,7 @@ class DBusSignalWrapper:
|
|||||||
self._proc.send_signal(SIGINT)
|
self._proc.send_signal(SIGINT)
|
||||||
await self._proc.communicate()
|
await self._proc.communicate()
|
||||||
|
|
||||||
async def __aiter__(self):
|
def __aiter__(self):
|
||||||
"""Start Iteratation."""
|
"""Start Iteratation."""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -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, ATTR_HASSOS_CLI,
|
||||||
|
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):
|
||||||
@ -100,6 +102,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema({
|
|||||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str),
|
vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str),
|
||||||
vol.Optional(ATTR_HASSIO): vol.Coerce(str),
|
vol.Optional(ATTR_HASSIO): vol.Coerce(str),
|
||||||
vol.Optional(ATTR_HASSOS): vol.Coerce(str),
|
vol.Optional(ATTR_HASSOS): vol.Coerce(str),
|
||||||
|
vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str),
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
6
pylintrc
6
pylintrc
@ -21,7 +21,6 @@ disable=
|
|||||||
abstract-class-little-used,
|
abstract-class-little-used,
|
||||||
abstract-class-not-used,
|
abstract-class-not-used,
|
||||||
unused-argument,
|
unused-argument,
|
||||||
global-statement,
|
|
||||||
redefined-variable-type,
|
redefined-variable-type,
|
||||||
too-many-arguments,
|
too-many-arguments,
|
||||||
too-many-branches,
|
too-many-branches,
|
||||||
@ -32,7 +31,10 @@ disable=
|
|||||||
too-many-statements,
|
too-many-statements,
|
||||||
too-many-lines,
|
too-many-lines,
|
||||||
too-few-public-methods,
|
too-few-public-methods,
|
||||||
abstract-method
|
abstract-method,
|
||||||
|
no-else-return,
|
||||||
|
useless-return,
|
||||||
|
not-async-context-manager
|
||||||
|
|
||||||
[EXCEPTIONS]
|
[EXCEPTIONS]
|
||||||
overgeneral-exceptions=Exception,HomeAssistantError
|
overgeneral-exceptions=Exception,HomeAssistantError
|
||||||
|
4
setup.py
4
setup.py
@ -43,13 +43,13 @@ 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',
|
||||||
'pytz==2018.4',
|
'pytz==2018.4',
|
||||||
'pyudev==0.21.0',
|
'pyudev==0.21.0',
|
||||||
'pycryptodome==3.4.11',
|
'pycryptodome==3.6.4',
|
||||||
"cpe==1.2.1"
|
"cpe==1.2.1"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user