Fix version conflicts

This commit is contained in:
Pascal Vizeli 2017-12-26 01:43:24 +01:00
commit b5af35bd6c
16 changed files with 256 additions and 75 deletions

5
API.md
View File

@ -331,6 +331,10 @@ Image with `null` and last_version with `null` reset this options.
Proxy to real home-assistant instance. Proxy to real home-assistant instance.
- GET `/homeassistant/websocket`
Proxy to real websocket instance.
### RESTful for API addons ### RESTful for API addons
- GET `/addons` - GET `/addons`
@ -373,6 +377,7 @@ Get all available addons.
{ {
"name": "xy bla", "name": "xy bla",
"description": "description", "description": "description",
"long_description": "null|markdown",
"auto_update": "bool", "auto_update": "bool",
"url": "null|url of addon", "url": "null|url of addon",
"detached": "bool", "detached": "bool",

View File

@ -137,6 +137,7 @@ class Addon(object):
"""Return if auto update is enable.""" """Return if auto update is enable."""
if ATTR_AUTO_UPDATE in self.data.user.get(self._id, {}): if ATTR_AUTO_UPDATE in self.data.user.get(self._id, {}):
return self.data.user[self._id][ATTR_AUTO_UPDATE] return self.data.user[self._id][ATTR_AUTO_UPDATE]
return None
@auto_update.setter @auto_update.setter
def auto_update(self, value): def auto_update(self, value):
@ -159,12 +160,26 @@ class Addon(object):
"""Return a API token for this add-on.""" """Return a API token for this add-on."""
if self.is_installed: if self.is_installed:
return self.data.user[self._id][ATTR_UUID] return self.data.user[self._id][ATTR_UUID]
return None
@property @property
def description(self): def description(self):
"""Return description of addon.""" """Return description of addon."""
return self._mesh[ATTR_DESCRIPTON] return self._mesh[ATTR_DESCRIPTON]
@property
def long_description(self):
"""Return README.md as long_description."""
readme = Path(self.path_location, 'README.md')
# If readme not exists
if not readme.exists():
return None
# Return data
with readme.open('r') as readme_file:
return readme_file.read()
@property @property
def repository(self): def repository(self):
"""Return repository of addon.""" """Return repository of addon."""
@ -333,7 +348,7 @@ class Addon(object):
def audio_input(self): def audio_input(self):
"""Return ALSA config for input or None.""" """Return ALSA config for input or None."""
if not self.with_audio: if not self.with_audio:
return return None
setting = self.config.audio_input setting = self.config.audio_input
if self.is_installed and ATTR_AUDIO_INPUT in self.data.user[self._id]: if self.is_installed and ATTR_AUDIO_INPUT in self.data.user[self._id]:

View File

@ -1,7 +1,7 @@
"""HassIO addons build environment.""" """HassIO addons build environment."""
from pathlib import Path from pathlib import Path
from .validate import SCHEMA_BUILD_CONFIG from .validate import SCHEMA_BUILD_CONFIG, BASE_IMAGE
from ..const import ATTR_SQUASH, ATTR_BUILD_FROM, ATTR_ARGS, META_ADDON from ..const import ATTR_SQUASH, ATTR_BUILD_FROM, ATTR_ARGS, META_ADDON
from ..tools import JsonConfig from ..tools import JsonConfig
@ -24,7 +24,8 @@ class AddonBuild(JsonConfig):
@property @property
def base_image(self): def base_image(self):
"""Base images for this addon.""" """Base images for this addon."""
return self._data[ATTR_BUILD_FROM][self.config.arch] return self._data[ATTR_BUILD_FROM].get(
self.config.arch, BASE_IMAGE[self.config.arch])
@property @property
def squash(self): def squash(self):

View File

@ -2,7 +2,7 @@
"local": { "local": {
"name": "Local Add-Ons", "name": "Local Add-Ons",
"url": "https://home-assistant.io/hassio", "url": "https://home-assistant.io/hassio",
"maintainer": "By our self" "maintainer": "you"
}, },
"core": { "core": {
"name": "Built-in Add-Ons", "name": "Built-in Add-Ons",

View File

@ -219,6 +219,7 @@ def validate_options(raw_schema):
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
# pylint: disable=inconsistent-return-statements
def _single_validate(typ, value, key): def _single_validate(typ, value, key):
"""Validate a single element.""" """Validate a single element."""
# if required argument # if required argument

View File

@ -8,6 +8,7 @@ from .addons import APIAddons
from .homeassistant import APIHomeAssistant from .homeassistant import APIHomeAssistant
from .host import APIHost from .host import APIHost
from .network import APINetwork from .network import APINetwork
from .proxy import APIProxy
from .supervisor import APISupervisor from .supervisor import APISupervisor
from .security import APISecurity from .security import APISecurity
from .snapshots import APISnapshots from .snapshots import APISnapshots
@ -63,9 +64,9 @@ class RestAPI(object):
'/supervisor/options', api_supervisor.options) '/supervisor/options', api_supervisor.options)
self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs) self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs)
def register_homeassistant(self, dock_homeassistant): def register_homeassistant(self, homeassistant):
"""Register homeassistant function.""" """Register homeassistant function."""
api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant) api_hass = APIHomeAssistant(self.config, self.loop, homeassistant)
self.webapp.router.add_get('/homeassistant/info', api_hass.info) self.webapp.router.add_get('/homeassistant/info', api_hass.info)
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs) self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
@ -75,10 +76,21 @@ class RestAPI(object):
self.webapp.router.add_post('/homeassistant/stop', api_hass.stop) self.webapp.router.add_post('/homeassistant/stop', api_hass.stop)
self.webapp.router.add_post('/homeassistant/start', api_hass.start) self.webapp.router.add_post('/homeassistant/start', api_hass.start)
self.webapp.router.add_post('/homeassistant/check', api_hass.check) self.webapp.router.add_post('/homeassistant/check', api_hass.check)
self.webapp.router.add_post(
'/homeassistant/api/{path:.+}', api_hass.api) def register_proxy(self, homeassistant, websession):
"""Register HomeAssistant API Proxy."""
api_proxy = APIProxy(self.loop, homeassistant, websession)
self.webapp.router.add_get( self.webapp.router.add_get(
'/homeassistant/api/{path:.+}', api_hass.api) '/homeassistant/api/websocket', api_proxy.websocket)
self.webapp.router.add_get(
'/homeassistant/websocket', api_proxy.websocket)
self.webapp.router.add_post(
'/homeassistant/api/{path:.+}', api_proxy.api)
self.webapp.router.add_get(
'/homeassistant/api/{path:.+}', api_proxy.api)
self.webapp.router.add_get(
'/homeassistant/api', api_proxy.api)
def register_addons(self, addons): def register_addons(self, addons):
"""Register homeassistant function.""" """Register homeassistant function."""

View File

@ -14,7 +14,7 @@ from ..const import (
ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED, ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED,
ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL,
ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
from ..validate import DOCKER_PORTS from ..validate import DOCKER_PORTS
@ -57,7 +57,7 @@ class APIAddons(object):
"""Return a simplified device list.""" """Return a simplified device list."""
dev_list = addon.devices dev_list = addon.devices
if not dev_list: if not dev_list:
return return None
return [row.split(':')[0] for row in dev_list] return [row.split(':')[0] for row in dev_list]
@api_process @api_process
@ -108,6 +108,7 @@ class APIAddons(object):
return { return {
ATTR_NAME: addon.name, ATTR_NAME: addon.name,
ATTR_DESCRIPTON: addon.description, ATTR_DESCRIPTON: addon.description,
ATTR_LONG_DESCRIPTION: addon.long_description,
ATTR_VERSION: addon.version_installed, ATTR_VERSION: addon.version_installed,
ATTR_AUTO_UPDATE: addon.auto_update, ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_REPOSITORY: addon.repository, ATTR_REPOSITORY: addon.repository,

View File

@ -2,18 +2,13 @@
import asyncio import asyncio
import logging import logging
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPBadGateway
from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
import voluptuous as vol import voluptuous as vol
from .util import api_process, api_process_raw, api_validate from .util import api_process, api_process_raw, api_validate
from ..const import ( from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM,
ATTR_BOOT, ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG,
CONTENT_TYPE_BINARY, HEADER_HA_ACCESS) CONTENT_TYPE_BINARY)
from ..validate import HASS_DEVICES, NETWORK_PORT from ..validate import HASS_DEVICES, NETWORK_PORT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -46,45 +41,6 @@ class APIHomeAssistant(object):
self.loop = loop self.loop = loop
self.homeassistant = homeassistant self.homeassistant = homeassistant
async def homeassistant_proxy(self, path, request):
"""Return a client request with proxy origin for Home-Assistant."""
url = "{}/api/{}".format(self.homeassistant.api_url, path)
try:
data = None
headers = {}
method = getattr(
self.homeassistant.websession, request.method.lower())
# read data
with async_timeout.timeout(10, loop=self.loop):
data = await request.read()
if data:
headers.update({CONTENT_TYPE: request.content_type})
# need api password?
if self.homeassistant.api_password:
headers = {HEADER_HA_ACCESS: self.homeassistant.api_password}
# reset headers
if not headers:
headers = None
client = await method(
url, data=data, headers=headers, timeout=300
)
return client
except aiohttp.ClientError as 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)
raise HTTPBadGateway()
@api_process @api_process
async def info(self, request): async def info(self, request):
"""Return host information.""" """Return host information."""
@ -169,14 +125,3 @@ class APIHomeAssistant(object):
raise RuntimeError(message) raise RuntimeError(message)
return True return True
async def api(self, request):
"""Proxy API request to Home-Assistant."""
path = request.match_info.get('path')
client = await self.homeassistant_proxy(path, request)
return web.Response(
body=await client.read(),
status=client.status,
content_type=client.content_type
)

195
hassio/api/proxy.py Normal file
View File

@ -0,0 +1,195 @@
"""Utils for HomeAssistant Proxy."""
import asyncio
import logging
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPBadGateway
from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
from ..const import HEADER_HA_ACCESS
_LOGGER = logging.getLogger(__name__)
class APIProxy(object):
"""API Proxy for Home-Assistant."""
def __init__(self, loop, homeassistant, websession):
"""Initialize api proxy."""
self.loop = loop
self.homeassistant = homeassistant
self.websession = websession
async def _api_client(self, request, path, timeout=300):
"""Return a client request with proxy origin for Home-Assistant."""
url = f"{self.homeassistant.api_url}/api/{path}"
try:
data = None
headers = {}
method = getattr(self.websession, request.method.lower())
# read data
with async_timeout.timeout(30, loop=self.loop):
data = await request.read()
if data:
headers.update({CONTENT_TYPE: request.content_type})
# need api password?
if self.homeassistant.api_password:
headers = {HEADER_HA_ACCESS: self.homeassistant.api_password}
# reset headers
if not headers:
headers = None
client = await method(
url, data=data, headers=headers, timeout=timeout
)
return client
except aiohttp.ClientError as 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)
raise HTTPBadGateway()
async def api(self, request):
"""Proxy HomeAssistant API Requests."""
path = request.match_info.get('path', '')
# API stream
if path.startswith("stream"):
_LOGGER.info("Home-Assistant Event-Stream start")
client = await self._api_client(request, path, timeout=None)
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
response.write(data)
except aiohttp.ClientError:
await response.write_eof()
except asyncio.CancelledError:
pass
finally:
client.close()
_LOGGER.info("Home-Assistant Event-Stream close")
# Normal request
else:
_LOGGER.info("Home-Assistant '/api/%s' request", 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 def _websocket_client(self):
"""Initialize a websocket api connection."""
url = f"{self.homeassistant.api_url}/api/websocket"
try:
client = await self.websession.ws_connect(
url, heartbeat=60)
# 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.homeassistant.api_password,
})
_LOGGER.error("Authentication to Home-Assistant websocket")
except (aiohttp.ClientError, RuntimeError) as err:
_LOGGER.error("Client error on websocket API %s.", err)
raise HTTPBadGateway()
async def websocket(self, request):
"""Initialize a websocket api connection."""
_LOGGER.info("Home-Assistant Websocket API request initialze")
# init server
server = web.WebSocketResponse(heartbeat=60)
await server.prepare(request)
# handle authentication
await server.send_json({'type': 'auth_required'})
await server.receive_json() # get internal token
await server.send_json({'type': 'auth_ok'})
# init connection to hass
client = await self._websocket_client()
_LOGGER.info("Home-Assistant Websocket API request running")
try:
client_read = None
server_read = None
while not server.closed and not client.closed:
if not client_read:
client_read = asyncio.ensure_future(
client.receive_str(), loop=self.loop)
if not server_read:
server_read = asyncio.ensure_future(
server.receive_str(), loop=self.loop)
# wait until data need to be processed
await asyncio.wait(
[client_read, server_read],
loop=self.loop, return_when=asyncio.FIRST_COMPLETED
)
# server
if server_read.done() and not client.closed:
server_read.exception()
await client.send_str(server_read.result())
server_read = None
# client
if client_read.done() and not server.closed:
client_read.exception()
await server.send_str(client_read.result())
client_read = None
except asyncio.CancelledError:
pass
except RuntimeError as err:
_LOGGER.info("Home-Assistant Websocket API error: %s", err)
finally:
if client_read:
client_read.cancel()
if server_read:
server_read.cancel()
# close connections
await client.close()
await server.close()
_LOGGER.info("Home-Assistant Websocket API connection is closed")
return server

View File

@ -2,7 +2,7 @@
from pathlib import Path from pathlib import Path
from ipaddress import ip_network from ipaddress import ip_network
HASSIO_VERSION = '0.76' HASSIO_VERSION = '0.77'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/{}/version.json') 'hassio/{}/version.json')
@ -57,6 +57,7 @@ ATTR_WATCHDOG = 'watchdog'
ATTR_CHANGELOG = 'changelog' ATTR_CHANGELOG = 'changelog'
ATTR_DATE = 'date' ATTR_DATE = 'date'
ATTR_ARCH = 'arch' ATTR_ARCH = 'arch'
ATTR_LONG_DESCRIPTION = 'long_description'
ATTR_HOSTNAME = 'hostname' ATTR_HOSTNAME = 'hostname'
ATTR_TIMEZONE = 'timezone' ATTR_TIMEZONE = 'timezone'
ATTR_ARGS = 'args' ATTR_ARGS = 'args'

View File

@ -91,6 +91,7 @@ class HassIO(object):
self.supervisor, self.snapshots, self.addons, self.host_control, self.supervisor, self.snapshots, self.addons, self.host_control,
self.updater) self.updater)
self.api.register_homeassistant(self.homeassistant) self.api.register_homeassistant(self.homeassistant)
self.api.register_proxy(self.homeassistant, self.websession)
self.api.register_addons(self.addons) self.api.register_addons(self.addons)
self.api.register_security() self.api.register_security()
self.api.register_snapshots(self.snapshots) self.api.register_snapshots(self.snapshots)

View File

@ -25,6 +25,7 @@ class DockerAddon(DockerInterface):
config, loop, api, image=addon.image, timeout=addon.timeout) config, loop, api, image=addon.image, timeout=addon.timeout)
self.addon = addon self.addon = addon
# pylint: disable=inconsistent-return-statements
def process_metadata(self, metadata, force=False): def process_metadata(self, metadata, force=False):
"""Use addon data instead meta data with legacy.""" """Use addon data instead meta data with legacy."""
if not self.addon.legacy: if not self.addon.legacy:
@ -50,6 +51,7 @@ class DockerAddon(DockerInterface):
"""Return the IPC namespace.""" """Return the IPC namespace."""
if self.addon.host_ipc: if self.addon.host_ipc:
return 'host' return 'host'
return None
@property @property
def hostname(self): def hostname(self):
@ -114,6 +116,7 @@ class DockerAddon(DockerInterface):
return [ return [
"apparmor:unconfined", "apparmor:unconfined",
] ]
return None
@property @property
def tmpfs(self): def tmpfs(self):

View File

@ -27,7 +27,7 @@ class DockerHomeAssistant(DockerInterface):
def devices(self): def devices(self):
"""Create list of special device to map into docker.""" """Create list of special device to map into docker."""
if not self.data.devices: if not self.data.devices:
return return None
devices = [] devices = []
for device in self.data.devices: for device in self.data.devices:
@ -41,7 +41,7 @@ class DockerHomeAssistant(DockerInterface):
Need run inside executor. Need run inside executor.
""" """
if self._is_running(): if self._is_running():
return return False
# cleanup # cleanup
self._stop() self._stop()

View File

@ -70,7 +70,7 @@ class Hardware(object):
devices = devices_file.read() devices = devices_file.read()
except OSError as err: except OSError as err:
_LOGGER.error("Can't read asound data -> %s", err) _LOGGER.error("Can't read asound data -> %s", err)
return return None
audio_list = {} audio_list = {}
@ -110,12 +110,12 @@ class Hardware(object):
stats = stat_file.read() stats = stat_file.read()
except OSError as err: except OSError as err:
_LOGGER.error("Can't read stat data -> %s", err) _LOGGER.error("Can't read stat data -> %s", err)
return return None
# parse stat file # parse stat file
found = RE_BOOT_TIME.search(stats) found = RE_BOOT_TIME.search(stats)
if not found: if not found:
_LOGGER.error("Can't found last boot time!") _LOGGER.error("Can't found last boot time!")
return return None
return datetime.utcfromtimestamp(int(found.group(1))) return datetime.utcfromtimestamp(int(found.group(1)))

View File

@ -29,11 +29,12 @@ def validate_timezone(timezone):
return timezone return timezone
# pylint: disable=inconsistent-return-statements
def convert_to_docker_ports(data): def convert_to_docker_ports(data):
"""Convert data into docker port list.""" """Convert data into docker port list."""
# dynamic ports # dynamic ports
if data is None: if data is None:
return return None
# single port # single port
if isinstance(data, int): if isinstance(data, int):

View File

@ -1,5 +1,5 @@
{ {
"hassio": "0.76", "hassio": "0.77",
"homeassistant": "0.60", "homeassistant": "0.60",
"resinos": "1.1", "resinos": "1.1",
"resinhup": "0.3", "resinhup": "0.3",