mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-16 21:56:29 +00:00
Fix version conflicts
This commit is contained in:
commit
b5af35bd6c
5
API.md
5
API.md
@ -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",
|
||||
|
@ -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]:
|
||||
|
@ -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):
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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
195
hassio/api/proxy.py
Normal 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
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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)))
|
||||
|
@ -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):
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"hassio": "0.76",
|
||||
"hassio": "0.77",
|
||||
"homeassistant": "0.60",
|
||||
"resinos": "1.1",
|
||||
"resinhup": "0.3",
|
||||
|
Loading…
x
Reference in New Issue
Block a user