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.
- GET `/homeassistant/websocket`
Proxy to real websocket instance.
### RESTful for API addons
- GET `/addons`
@ -373,6 +377,7 @@ Get all available addons.
{
"name": "xy bla",
"description": "description",
"long_description": "null|markdown",
"auto_update": "bool",
"url": "null|url of addon",
"detached": "bool",

View File

@ -137,6 +137,7 @@ class Addon(object):
"""Return if auto update is enable."""
if ATTR_AUTO_UPDATE in self.data.user.get(self._id, {}):
return self.data.user[self._id][ATTR_AUTO_UPDATE]
return None
@auto_update.setter
def auto_update(self, value):
@ -159,12 +160,26 @@ class Addon(object):
"""Return a API token for this add-on."""
if self.is_installed:
return self.data.user[self._id][ATTR_UUID]
return None
@property
def description(self):
"""Return description of addon."""
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
def repository(self):
"""Return repository of addon."""
@ -333,7 +348,7 @@ class Addon(object):
def audio_input(self):
"""Return ALSA config for input or None."""
if not self.with_audio:
return
return None
setting = self.config.audio_input
if self.is_installed and ATTR_AUDIO_INPUT in self.data.user[self._id]:

View File

@ -1,7 +1,7 @@
"""HassIO addons build environment."""
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 ..tools import JsonConfig
@ -24,7 +24,8 @@ class AddonBuild(JsonConfig):
@property
def base_image(self):
"""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
def squash(self):

View File

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

View File

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

View File

@ -8,6 +8,7 @@ from .addons import APIAddons
from .homeassistant import APIHomeAssistant
from .host import APIHost
from .network import APINetwork
from .proxy import APIProxy
from .supervisor import APISupervisor
from .security import APISecurity
from .snapshots import APISnapshots
@ -63,9 +64,9 @@ class RestAPI(object):
'/supervisor/options', api_supervisor.options)
self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs)
def register_homeassistant(self, dock_homeassistant):
def register_homeassistant(self, homeassistant):
"""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/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/start', api_hass.start)
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(
'/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):
"""Register homeassistant function."""

View File

@ -14,7 +14,7 @@ from ..const import (
ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED,
ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API,
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)
from ..validate import DOCKER_PORTS
@ -57,7 +57,7 @@ class APIAddons(object):
"""Return a simplified device list."""
dev_list = addon.devices
if not dev_list:
return
return None
return [row.split(':')[0] for row in dev_list]
@api_process
@ -108,6 +108,7 @@ class APIAddons(object):
return {
ATTR_NAME: addon.name,
ATTR_DESCRIPTON: addon.description,
ATTR_LONG_DESCRIPTION: addon.long_description,
ATTR_VERSION: addon.version_installed,
ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_REPOSITORY: addon.repository,

View File

@ -2,18 +2,13 @@
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
import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM,
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
_LOGGER = logging.getLogger(__name__)
@ -46,45 +41,6 @@ class APIHomeAssistant(object):
self.loop = loop
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
async def info(self, request):
"""Return host information."""
@ -169,14 +125,3 @@ class APIHomeAssistant(object):
raise RuntimeError(message)
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 ipaddress import ip_network
HASSIO_VERSION = '0.76'
HASSIO_VERSION = '0.77'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/{}/version.json')
@ -57,6 +57,7 @@ ATTR_WATCHDOG = 'watchdog'
ATTR_CHANGELOG = 'changelog'
ATTR_DATE = 'date'
ATTR_ARCH = 'arch'
ATTR_LONG_DESCRIPTION = 'long_description'
ATTR_HOSTNAME = 'hostname'
ATTR_TIMEZONE = 'timezone'
ATTR_ARGS = 'args'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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