diff --git a/API.md b/API.md index 8d15758c8..7dcc6a304 100644 --- a/API.md +++ b/API.md @@ -40,13 +40,11 @@ The addons from `addons` are only installed one. "name": "xy bla", "slug": "xy", "description": "description", - "arch": ["armhf", "aarch64", "i386", "amd64"], "repository": "12345678|null", "version": "LAST_VERSION", "installed": "INSTALL_VERSION", - "detached": "bool", - "build": "bool", - "url": "null|url" + "logo": "bool", + "state": "started|stopped", } ], "addons_repositories": [ @@ -55,10 +53,6 @@ The addons from `addons` are only installed one. } ``` -- GET `/supervisor/addons` - -Get all available addons. Will be delete soon. Look to `/addons` - - POST `/supervisor/update` Optional: ```json @@ -299,7 +293,8 @@ Get all available addons "installed": "none|INSTALL_VERSION", "detached": "bool", "build": "bool", - "url": "null|url" + "url": "null|url", + "logo": "bool" } ], "repositories": [ @@ -332,10 +327,14 @@ Get all available addons "build": "bool", "options": "{}", "network": "{}|null", - "host_network": "bool" + "host_network": "bool", + "logo": "bool", + "webui": "null|http(s)://[HOST]:port/xy/zx" } ``` +- GET `/addons/{addon}/logo` + - POST `/addons/{addon}/options` ```json { diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 139b28e2a..c5b048fe7 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -19,7 +19,7 @@ from ..const import ( ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP, STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM, - ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK) + ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI) from .util import check_installed from ..dock.addon import DockerAddon from ..tools import write_json_file, read_json_file @@ -27,6 +27,7 @@ from ..tools import write_json_file, read_json_file _LOGGER = logging.getLogger(__name__) RE_VOLUME = re.compile(MAP_VOLUME) +RE_WEBUI = re.compile(r"^(.*\[HOST\]:)\[PORT:(\d+)\](.*)$") class Addon(object): @@ -130,7 +131,8 @@ class Addon(object): @property def auto_update(self): """Return if auto update is enable.""" - return self.data.user[self._id][ATTR_AUTO_UPDATE] + if ATTR_AUTO_UPDATE in self.data.user.get(self._id, {}): + return self.data.user[self._id][ATTR_AUTO_UPDATE] @auto_update.setter def auto_update(self, value): @@ -196,6 +198,25 @@ class Addon(object): self.data.save() + @property + def webui(self): + """Return URL to webui or None.""" + if ATTR_WEBUI not in self._mesh: + return + + webui = self._mesh[ATTR_WEBUI] + dock_port = RE_WEBUI.sub(r"\2", webui) + if self.ports is None: + real_port = dock_port + else: + real_port = self.ports.get("{}/tcp".format(dock_port), dock_port) + + # for interface config or port lists + if isinstance(real_port, (tuple, list)): + real_port = real_port[-1] + + return RE_WEBUI.sub(r"\g<1>{}\g<3>".format(real_port), webui) + @property def network_mode(self): """Return network mode of addon.""" @@ -228,6 +249,11 @@ class Addon(object): """Return url of addon.""" return self._mesh.get(ATTR_URL) + @property + def with_logo(self): + """Return True if a logo exists.""" + return self.path_logo.exists() + @property def supported_arch(self): """Return list of supported arch.""" @@ -273,15 +299,20 @@ class Addon(object): return PurePath(self.config.path_extern_addons_data, self._id) @property - def path_addon_options(self): + def path_options(self): """Return path to addons options.""" return Path(self.path_data, "options.json") @property - def path_addon_location(self): + def path_location(self): """Return path to this addon.""" return Path(self._mesh[ATTR_LOCATON]) + @property + def path_logo(self): + """Return path to addon logo.""" + return Path(self.path_location, 'logo.png') + def write_options(self): """Return True if addon options is written to data.""" schema = self.schema @@ -289,7 +320,7 @@ class Addon(object): try: schema(options) - return write_json_file(self.path_addon_options, options) + return write_json_file(self.path_options, options) except vol.Invalid as ex: _LOGGER.error("Addon %s have wrong options -> %s", self._id, humanize_error(options, ex)) diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 7dd4f4c88..258f55d30 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -10,7 +10,7 @@ from ..const import ( ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED, ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK, - ATTR_AUTO_UPDATE) + ATTR_AUTO_UPDATE, ATTR_WEBUI) from ..validate import NETWORK_PORT, DOCKER_PORTS @@ -65,6 +65,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.Optional(ATTR_PORTS): DOCKER_PORTS, + vol.Optional(ATTR_WEBUI): + vol.Match(r"^(?:https?):\/\/\[HOST\]:\[PORT:\d+\].*$"), vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(), vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")], vol.Optional(ATTR_TMPFS): diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 1fbcbc40a..2d5c0110a 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -53,8 +53,6 @@ class RestAPI(object): self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping) self.webapp.router.add_get('/supervisor/info', api_supervisor.info) - self.webapp.router.add_get( - '/supervisor/addons', api_supervisor.available_addons) self.webapp.router.add_post( '/supervisor/update', api_supervisor.update) self.webapp.router.add_post( @@ -94,6 +92,7 @@ class RestAPI(object): self.webapp.router.add_post( '/addons/{addon}/options', api_addons.options) self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs) + self.webapp.router.add_get('/addons/{addon}/logo', api_addons.logo) def register_security(self): """Register security function.""" diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 2e805bf52..08925ad71 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -11,7 +11,8 @@ from ..const import ( ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY, ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK, ATTR_SLUG, ATTR_SOURCE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_ARCH, ATTR_MAINTAINER, - ATTR_INSTALLED, BOOT_AUTO, BOOT_MANUAL) + ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, BOOT_AUTO, BOOT_MANUAL, + CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY) from ..validate import DOCKER_PORTS _LOGGER = logging.getLogger(__name__) @@ -64,6 +65,7 @@ class APIAddons(object): ATTR_REPOSITORY: addon.repository, ATTR_BUILD: addon.need_build, ATTR_URL: addon.url, + ATTR_LOGO: addon.with_logo, }) data_repositories = [] @@ -82,9 +84,10 @@ class APIAddons(object): } @api_process - def reload(self, request): + async def reload(self, request): """Reload all addons data.""" - return self.addons.reload() + await asyncio.shield(self.addons.reload(), loop=self.loop) + return True @api_process async def info(self, request): @@ -106,6 +109,8 @@ class APIAddons(object): ATTR_BUILD: addon.need_build, ATTR_NETWORK: addon.ports, ATTR_HOST_NETWORK: addon.network_mode == 'host', + ATTR_LOGO: addon.with_logo, + ATTR_WEBUI: addon.webui, } @api_process @@ -182,8 +187,18 @@ class APIAddons(object): addon = self._extract_addon(request) return await asyncio.shield(addon.restart(), loop=self.loop) - @api_process_raw + @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request): """Return logs from addon.""" addon = self._extract_addon(request) return addon.logs() + + @api_process_raw(CONTENT_TYPE_PNG) + async def logo(self, request): + """Return logo from addon.""" + addon = self._extract_addon(request, check_installed=False) + if not addon.with_logo: + raise RuntimeError("No image found!") + + with addon.path_logo.open('rb') as png: + return png.read() diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index 9b10497ca..6910a920c 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -6,7 +6,8 @@ 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_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM, + CONTENT_TYPE_BINARY) from ..validate import HASS_DEVICES _LOGGER = logging.getLogger(__name__) @@ -79,7 +80,7 @@ class APIHomeAssistant(object): return await asyncio.shield( self.homeassistant.restart(), loop=self.loop) - @api_process_raw + @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request): """Return homeassistant docker logs. diff --git a/hassio/api/snapshots.py b/hassio/api/snapshots.py index e79d3a2e7..494586fd3 100644 --- a/hassio/api/snapshots.py +++ b/hassio/api/snapshots.py @@ -63,9 +63,10 @@ class APISnapshots(object): } @api_process - def reload(self, request): + async def reload(self, request): """Reload snapshot list.""" - return asyncio.shield(self.snapshots.reload(), loop=self.loop) + await asyncio.shield(self.snapshots.reload(), loop=self.loop) + return True @api_process async def info(self, request): diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 2313ed111..2fc8da802 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -6,11 +6,10 @@ import voluptuous as vol from .util import api_process, api_process_raw, api_validate from ..const import ( - ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, - HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_REPOSITORIES, - ATTR_REPOSITORY, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, - ATTR_DETACHED, ATTR_SOURCE, ATTR_MAINTAINER, ATTR_URL, ATTR_ARCH, - ATTR_BUILD, ATTR_TIMEZONE) + ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, ATTR_ARCH, + HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_LOGO, ATTR_REPOSITORY, + ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_TIMEZONE, + ATTR_STATE, CONTENT_TYPE_BINARY) from ..tools import validate_timezone _LOGGER = logging.getLogger(__name__) @@ -41,42 +40,6 @@ class APISupervisor(object): self.host_control = host_control self.websession = websession - def _addons_list(self, only_installed=False): - """Return a list of addons.""" - data = [] - for addon in self.addons.list_addons: - if only_installed and not addon.is_installed: - continue - - data.append({ - ATTR_NAME: addon.name, - ATTR_SLUG: addon.slug, - ATTR_DESCRIPTON: addon.description, - ATTR_VERSION: addon.last_version, - ATTR_INSTALLED: addon.version_installed, - ATTR_ARCH: addon.supported_arch, - ATTR_DETACHED: addon.is_detached, - ATTR_REPOSITORY: addon.repository, - ATTR_BUILD: addon.need_build, - ATTR_URL: addon.url, - }) - - return data - - def _repositories_list(self): - """Return a list of addons repositories.""" - data = [] - for repository in self.addons.list_repositories: - data.append({ - ATTR_SLUG: repository.slug, - ATTR_NAME: repository.name, - ATTR_SOURCE: repository.source, - ATTR_URL: repository.url, - ATTR_MAINTAINER: repository.maintainer, - }) - - return data - @api_process async def ping(self, request): """Return ok for signal that the api is ready.""" @@ -85,24 +48,30 @@ class APISupervisor(object): @api_process async def info(self, request): """Return host information.""" + list_addons = [] + for addon in self.addons.list_addons: + if addon.is_installed: + list_addons.append({ + ATTR_NAME: addon.name, + ATTR_SLUG: addon.slug, + ATTR_DESCRIPTON: addon.description, + ATTR_STATE: await addon.state(), + ATTR_VERSION: addon.last_version, + ATTR_INSTALLED: addon.version_installed, + ATTR_REPOSITORY: addon.repository, + ATTR_LOGO: addon.with_logo, + }) + return { ATTR_VERSION: HASSIO_VERSION, ATTR_LAST_VERSION: self.config.last_hassio, ATTR_BETA_CHANNEL: self.config.upstream_beta, ATTR_ARCH: self.config.arch, ATTR_TIMEZONE: self.config.timezone, - ATTR_ADDONS: self._addons_list(only_installed=True), + ATTR_ADDONS: list_addons, ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories, } - @api_process - async def available_addons(self, request): - """Return information for all available addons.""" - return { - ATTR_ADDONS: self._addons_list(), - ATTR_REPOSITORIES: self._repositories_list(), - } - @api_process async def options(self, request): """Set supervisor options.""" @@ -150,7 +119,7 @@ class APISupervisor(object): return True - @api_process_raw + @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request): """Return supervisor docker logs. diff --git a/hassio/api/util.py b/hassio/api/util.py index c1e52e249..671073c1d 100644 --- a/hassio/api/util.py +++ b/hassio/api/util.py @@ -9,7 +9,8 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from ..const import ( - JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR) + JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR, + CONTENT_TYPE_BINARY) _LOGGER = logging.getLogger(__name__) @@ -65,18 +66,23 @@ def api_process_hostcontrol(method): return wrap_hostcontrol -def api_process_raw(method): - """Wrap function with raw output to rest api.""" - async def wrap_api(api, *args, **kwargs): - """Return api information.""" - try: - message = await method(api, *args, **kwargs) - except RuntimeError as err: - message = str(err).encode() +def api_process_raw(content): + """Wrap content_type into function.""" + def wrap_method(method): + """Wrap function with raw output to rest api.""" + async def wrap_api(api, *args, **kwargs): + """Return api information.""" + try: + msg_data = await method(api, *args, **kwargs) + msg_type = content + except RuntimeError as err: + msg_data = str(err).encode() + msg_type = CONTENT_TYPE_BINARY - return web.Response(body=message) + return web.Response(body=msg_data, content_type=msg_type) - return wrap_api + return wrap_api + return wrap_method def api_return_error(message=None): diff --git a/hassio/const.py b/hassio/const.py index 25642177c..5cf063dbe 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,7 +1,7 @@ """Const file for HassIO.""" from pathlib import Path -HASSIO_VERSION = '0.45' +HASSIO_VERSION = '0.46' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/master/version.json') @@ -44,6 +44,9 @@ JSON_MESSAGE = 'message' RESULT_ERROR = 'error' RESULT_OK = 'ok' +CONTENT_TYPE_BINARY = 'application/octet-stream' +CONTENT_TYPE_PNG = 'image/png' + ATTR_DATE = 'date' ATTR_ARCH = 'arch' ATTR_HOSTNAME = 'hostname' @@ -63,12 +66,14 @@ ATTR_STARTUP = 'startup' ATTR_BOOT = 'boot' ATTR_PORTS = 'ports' ATTR_MAP = 'map' +ATTR_WEBUI = 'webui' ATTR_OPTIONS = 'options' ATTR_INSTALLED = 'installed' ATTR_DETACHED = 'detached' ATTR_STATE = 'state' ATTR_SCHEMA = 'schema' ATTR_IMAGE = 'image' +ATTR_LOGO = 'logo' ATTR_ADDONS_REPOSITORIES = 'addons_repositories' ATTR_REPOSITORY = 'repository' ATTR_REPOSITORIES = 'repositories' diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 561e60dca..8e4737b0e 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -144,7 +144,7 @@ class DockerAddon(DockerBase): try: # prepare temporary addon build folder try: - source = self.addon.path_addon_location + source = self.addon.path_location shutil.copytree(str(source), str(build_dir)) except shutil.Error as err: _LOGGER.error("Can't copy %s to temporary build folder -> %s", diff --git a/hassio/panel/hassio-main.html b/hassio/panel/hassio-main.html index 22b39cc8d..6fea41309 100644 --- a/hassio/panel/hassio-main.html +++ b/hassio/panel/hassio-main.html @@ -1 +1 @@ -
Hostname | [[data.hostname]] |
Type | [[data.type]] |
OS | [[data.os]] |
Host Control version | [[data.version]] |
Latest available version | [[data.last_version]] |
Current version | [[data.version]] |
Latest version | [[data.last_version]] |
Version | [[data.version]] |
Beta channel | [[data.beta_channel]] |
Latest available version | [[data.last_version]] |
Description | [[addonInfo.description]] |
Version | [[addonState.version]] |
State | [[addonState.state]] |
Boot | [[addonState.boot]] |
Auto update | |
Uses host network | |
Builds locally | |
Detached |
[[addonLogs]]
Configure which add-on repositories to fetch data from. One repository per line.
[[addon.description]]
Made available via repository [[addon.repository]].
This add-on will built locally on the device.
[[logs]]
[[_error]]
Looks like you don't have any snapshots yet.
Hostname | [[data.hostname]] |
Type | [[data.type]] |
OS | [[data.os]] |
Host Control version | [[data.version]] |
Latest available version | [[data.last_version]] |
Current version | [[data.version]] |
Latest version | [[data.last_version]] |
Version | [[data.version]] |
Beta channel | [[data.beta_channel]] |
Latest available version | [[data.last_version]] |
[[addonLogs]]
Configure which add-on repositories to fetch data from. One repository per line.
[[addon.description]]
Made available via repository [[addon.repository]].
This add-on will built locally on the device.
[[logs]]
[[_error]]
Looks like you don't have any snapshots yet.