From 68390469df06029bfe6be72e9a61eb9ffe9c1b96 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 31 Jul 2017 11:51:34 +0200 Subject: [PATCH 01/14] Pump hass.io version to 0.51 --- hassio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/const.py b/hassio/const.py index 05e9bb069..ecbca9d19 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,7 +1,7 @@ """Const file for HassIO.""" from pathlib import Path -HASSIO_VERSION = '0.50' +HASSIO_VERSION = '0.51' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/master/version.json') From 3809f20c6aefbabda19d44cc7b6a81514ba19e59 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 1 Aug 2017 11:34:29 +0200 Subject: [PATCH 02/14] Update homeassistant to 0.50.2 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 2446cb76b..3dd6acab1 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "hassio": "0.50", - "homeassistant": "0.50", + "homeassistant": "0.50.2", "resinos": "1.0", "resinhup": "0.3", "generic": "0.3", From 8afde1e881020dd555a06dc01d5b4cfb1000dcd3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 2 Aug 2017 16:59:38 +0200 Subject: [PATCH 03/14] Return a error on update with own version (#124) Return a error on update with own version --- hassio/addons/addon.py | 38 +++++++++++++++++++---------- hassio/api/addons.py | 48 +++++++++++++++++++++++++------------ hassio/api/homeassistant.py | 15 ++++++------ hassio/api/host.py | 12 +++++++--- hassio/api/snapshots.py | 12 ++++++---- hassio/api/supervisor.py | 4 ++-- hassio/homeassistant.py | 14 ++++++----- 7 files changed, 92 insertions(+), 51 deletions(-) diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index cea570caa..f3bcb9f20 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -413,14 +413,20 @@ class Addon(object): return STATE_STOPPED @check_installed - async def start(self): - """Set options and start addon.""" - return await self.addon_docker.run() + def start(self): + """Set options and start addon. + + Return a coroutine. + """ + return self.addon_docker.run() @check_installed - async def stop(self): - """Stop addon.""" - return await self.addon_docker.stop() + def stop(self): + """Stop addon. + + Return a coroutine. + """ + return self.addon_docker.stop() @check_installed async def update(self, version=None): @@ -430,7 +436,7 @@ class Addon(object): if version == self.version_installed: _LOGGER.warning( "Addon %s is already installed in %s", self._id, version) - return True + return False if not await self.addon_docker.update(version): return False @@ -439,14 +445,20 @@ class Addon(object): return True @check_installed - async def restart(self): - """Restart addon.""" - return await self.addon_docker.restart() + def restart(self): + """Restart addon. + + Return a coroutine. + """ + return self.addon_docker.restart() @check_installed - async def logs(self): - """Return addons log output.""" - return await self.addon_docker.logs() + def logs(self): + """Return addons log output. + + Return a coroutine. + """ + return self.addon_docker.logs() @check_installed async def snapshot(self, tar_file): diff --git a/hassio/api/addons.py b/hassio/api/addons.py index f98a19098..33bc547a2 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -152,20 +152,26 @@ class APIAddons(object): """Install addon.""" body = await api_validate(SCHEMA_VERSION, request) addon = self._extract_addon(request, check_installed=False) - version = body.get(ATTR_VERSION) + version = body.get(ATTR_VERSION, addon.last_version) return await asyncio.shield( addon.install(version=version), loop=self.loop) @api_process - async def uninstall(self, request): - """Uninstall addon.""" + def uninstall(self, request): + """Uninstall addon. + + Return a coroutine. + """ addon = self._extract_addon(request) - return await asyncio.shield(addon.uninstall(), loop=self.loop) + return asyncio.shield(addon.uninstall(), loop=self.loop) @api_process - async def start(self, request): - """Start addon.""" + def start(self, request): + """Start addon. + + Return a coroutine. + """ addon = self._extract_addon(request) # check options @@ -175,33 +181,45 @@ class APIAddons(object): except vol.Invalid as ex: raise RuntimeError(humanize_error(options, ex)) from None - return await asyncio.shield(addon.start(), loop=self.loop) + return asyncio.shield(addon.start(), loop=self.loop) @api_process - async def stop(self, request): - """Stop addon.""" + def stop(self, request): + """Stop addon. + + Return a coroutine. + """ addon = self._extract_addon(request) - return await asyncio.shield(addon.stop(), loop=self.loop) + return asyncio.shield(addon.stop(), loop=self.loop) @api_process async def update(self, request): """Update addon.""" body = await api_validate(SCHEMA_VERSION, request) addon = self._extract_addon(request) - version = body.get(ATTR_VERSION) + version = body.get(ATTR_VERSION, addon.last_version) + + if version == addon.version_installed: + raise RuntimeError("Version %s is already in use", version) return await asyncio.shield( addon.update(version=version), loop=self.loop) @api_process - async def restart(self, request): - """Restart addon.""" + def restart(self, request): + """Restart addon. + + Return a coroutine. + """ addon = self._extract_addon(request) - return await asyncio.shield(addon.restart(), loop=self.loop) + return asyncio.shield(addon.restart(), loop=self.loop) @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request): - """Return logs from addon.""" + """Return logs from addon. + + Return a coroutine. + """ addon = self._extract_addon(request) return addon.logs() diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index 6910a920c..df2c55e3e 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -65,20 +65,19 @@ class APIHomeAssistant(object): body = await api_validate(SCHEMA_VERSION, request) version = body.get(ATTR_VERSION, self.config.last_homeassistant) - if self.homeassistant.in_progress: - raise RuntimeError("Other task is in progress") + if version == self.homeassistant.version: + raise RuntimeError("Version {} is already in use".format(version)) return await asyncio.shield( self.homeassistant.update(version), loop=self.loop) @api_process - async def restart(self, request): - """Restart homeassistant.""" - if self.homeassistant.in_progress: - raise RuntimeError("Other task is in progress") + def restart(self, request): + """Restart homeassistant. - return await asyncio.shield( - self.homeassistant.restart(), loop=self.loop) + Return a coroutine. + """ + return asyncio.shield(self.homeassistant.restart(), loop=self.loop) @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request): diff --git a/hassio/api/host.py b/hassio/api/host.py index 1ea2ad9ef..ceb960402 100644 --- a/hassio/api/host.py +++ b/hassio/api/host.py @@ -40,12 +40,18 @@ class APIHost(object): @api_process_hostcontrol def reboot(self, request): - """Reboot host.""" + """Reboot host. + + Return a coroutine. + """ return self.host_control.reboot() @api_process_hostcontrol def shutdown(self, request): - """Poweroff host.""" + """Poweroff host. + + Return a coroutine. + """ return self.host_control.shutdown() @api_process_hostcontrol @@ -55,7 +61,7 @@ class APIHost(object): version = body.get(ATTR_VERSION, self.host_control.last_version) if version == self.host_control.version: - raise RuntimeError("Version is already in use") + raise RuntimeError("Version {} is already in use".format(version)) return await asyncio.shield( self.host_control.update(version=version), loop=self.loop) diff --git a/hassio/api/snapshots.py b/hassio/api/snapshots.py index 494586fd3..42467acd2 100644 --- a/hassio/api/snapshots.py +++ b/hassio/api/snapshots.py @@ -111,10 +111,13 @@ class APISnapshots(object): self.snapshots.do_snapshot_partial(**body), loop=self.loop) @api_process - async def restore_full(self, request): - """Full-Restore a snapshot.""" + def restore_full(self, request): + """Full-Restore a snapshot. + + Return a coroutine. + """ snapshot = self._extract_snapshot(request) - return await asyncio.shield( + return asyncio.shield( self.snapshots.do_restore_full(snapshot), loop=self.loop) @api_process @@ -125,7 +128,8 @@ class APISnapshots(object): return await asyncio.shield( self.snapshots.do_restore_partial(snapshot, **body), - loop=self.loop) + loop=self.loop + ) @api_process async def remove(self, request): diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 2fc8da802..2fd5c82e3 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -96,10 +96,10 @@ class APISupervisor(object): version = body.get(ATTR_VERSION, self.config.last_hassio) if version == self.supervisor.version: - raise RuntimeError("Version is already in use") + raise RuntimeError("Version {} is already in use".format(version)) return await asyncio.shield( - self.supervisor.update(version), loop=self.loop) + self.supervisor.update(version=version), loop=self.loop) @api_process async def reload(self, request): diff --git a/hassio/homeassistant.py b/hassio/homeassistant.py index 70bcf05c6..8f519b370 100644 --- a/hassio/homeassistant.py +++ b/hassio/homeassistant.py @@ -113,13 +113,15 @@ class HomeAssistant(JsonConfig): _LOGGER.info("HomeAssistant docker now installed") await self.docker.cleanup() - def update(self, version=None): - """Update HomeAssistant version. - - Return a coroutine. - """ + async def update(self, version=None): + """Update HomeAssistant version.""" version = version or self.last_version - return self.docker.update(version) + + if version == self.docker.version: + _LOGGER.warning("Version %s is already installed", version) + return False + + return await self.docker.update(version) def run(self): """Run HomeAssistant docker. From f5b166a7f0e847cebfdc644d19d32d1a28e8b940 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 4 Aug 2017 16:32:17 +0200 Subject: [PATCH 04/14] Use addon slug as hostname instead docker name (#132) --- hassio/dock/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 679ea961c..8d573b58e 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -107,7 +107,7 @@ class DockerAddon(DockerBase): self.dock.containers.run( self.image, name=self.name, - hostname=self.name, + hostname=self.addon.slug, detach=True, network_mode=self.addon.network_mode, ports=self.addon.ports, From ec72d382209bf375fcadb1dfadeb3398b54f7ccb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 Aug 2017 17:00:14 +0200 Subject: [PATCH 05/14] Sync names --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fdef404aa..71ff79643 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Hass.io + ### First private cloud solution for home automation. Hass.io is a Docker based system for managing your Home Assistant installation and related applications. The system is controlled via Home Assistant which communicates with the supervisor. The supervisor provides an API to manage the installation. This includes changing network settings or installing and updating software. ![](misc/hassio.png?raw=true) -[HassIO-Addons](https://github.com/home-assistant/hassio-addons) | [HassIO-Build](https://github.com/home-assistant/hassio-build) +- [Hass.io Addons](https://github.com/home-assistant/hassio-addons) +- [Hass.io Build](https://github.com/home-assistant/hassio-build) ## Installation From 46f323791d9500314ebed3312d69eb9a3d032306 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 6 Aug 2017 22:48:46 +0200 Subject: [PATCH 06/14] Update to new beta version of image --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 3dd6acab1..bade9cb1f 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "hassio": "0.50", - "homeassistant": "0.50.2", + "homeassistant": "0.51-dev", "resinos": "1.0", "resinhup": "0.3", "generic": "0.3", From eaa489abec90f4008b68a0c55e5cdc7e415d3312 Mon Sep 17 00:00:00 2001 From: William Johansson Date: Mon, 7 Aug 2017 21:58:57 +0200 Subject: [PATCH 07/14] Allow privileged capability SYS_RAWIO (#136) In order to allow writes to /dev/mem, which is needed e.g. to use the GPIO ports on Raspberry Pi, the SYS_RAWIO capability needs to be granted. Fixes #134. --- hassio/addons/validate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 320b8a8b3..dc7f447e2 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -38,6 +38,7 @@ STARTUP_ALL = [ PRIVILEGED_ALL = [ "NET_ADMIN", "SYS_ADMIN", + "SYS_RAWIO" ] From 99cf44aacdbcfd896ad57128d01af91885878806 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Aug 2017 00:53:54 +0200 Subject: [PATCH 08/14] Cleanup config / new updater object / New audio (#135) * Cleanup config / new updater object / New audio * Cleanup beta_channel * fix lint * fix lint p3 * Fix lint p4 * Allow set audio options * Fix errors * add host options --- API.md | 26 +++--- hassio/addons/__init__.py | 2 +- hassio/addons/validate.py | 9 +-- hassio/api/__init__.py | 5 +- hassio/api/host.py | 23 +++++- hassio/api/security.py | 2 +- hassio/api/supervisor.py | 16 ++-- hassio/config.py | 162 +++++++++++++++----------------------- hassio/const.py | 11 ++- hassio/core.py | 10 ++- hassio/homeassistant.py | 8 +- hassio/tasks.py | 14 ++-- hassio/tools.py | 59 ++++++-------- hassio/updater.py | 85 ++++++++++++++++++++ hassio/validate.py | 49 +++++++++++- 15 files changed, 303 insertions(+), 178 deletions(-) create mode 100644 hassio/updater.py diff --git a/API.md b/API.md index ae3595074..8a5aa4b3d 100644 --- a/API.md +++ b/API.md @@ -203,16 +203,29 @@ Return QR-Code - POST `/host/reboot` - GET `/host/info` -See HostControl info command. - ```json { "type": "", "version": "", "last_version": "", - "features": ["shutdown", "reboot", "update", "network_info", "network_control"], + "features": ["shutdown", "reboot", "update", "hostname", "network_info", "network_control"], "hostname": "", - "os": "" + "os": "", + "audio": { + "input": "0,0", + "output": "0,0" + } +} +``` + +- POST `/host/options` + +```json +{ + "audio": { + "input": "0,0", + "output": "0,0" + } } ``` @@ -259,11 +272,6 @@ Optional: ```json { "hostname": "", - "mode": "dhcp|fixed", - "ssid": "", - "ip": "", - "netmask": "", - "gateway": "" } ``` diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index f59b47320..9d7875e4b 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -78,7 +78,7 @@ class AddonManager(object): # don't add built-in repository to config if url not in BUILTIN_REPOSITORIES: - self.config.addons_repositories = url + self.config.add_addon_repository(url) tasks = [_add_repository(url) for url in new_rep - old_rep] if tasks: diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index dc7f447e2..34564cbb2 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -42,11 +42,8 @@ PRIVILEGED_ALL = [ ] -def _migrate_startup(value): - """Migrate startup schema. - - REMOVE after 0.50- - """ +def _simple_startup(value): + """Simple startup schema.""" if value == "before": return STARTUP_SERVICES if value == "after": @@ -63,7 +60,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_URL): vol.Url(), vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)], vol.Required(ATTR_STARTUP): - vol.All(_migrate_startup, vol.In(STARTUP_ALL)), + vol.All(_simple_startup, vol.In(STARTUP_ALL)), vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.Optional(ATTR_PORTS): DOCKER_PORTS, diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 832649ddf..9358c2ec8 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -37,6 +37,7 @@ class RestAPI(object): self.webapp.router.add_post('/host/reboot', api_host.reboot) self.webapp.router.add_post('/host/shutdown', api_host.shutdown) self.webapp.router.add_post('/host/update', api_host.update) + self.webapp.router.add_post('/host/options', api_host.options) def register_network(self, host_control): """Register network function.""" @@ -46,11 +47,11 @@ class RestAPI(object): self.webapp.router.add_post('/network/options', api_net.options) def register_supervisor(self, supervisor, snapshots, addons, host_control, - websession): + updater): """Register supervisor function.""" api_supervisor = APISupervisor( self.config, self.loop, supervisor, snapshots, addons, - host_control, websession) + host_control, updater) self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping) self.webapp.router.add_get('/supervisor/info', api_supervisor.info) diff --git a/hassio/api/host.py b/hassio/api/host.py index ceb960402..2a53e5221 100644 --- a/hassio/api/host.py +++ b/hassio/api/host.py @@ -7,7 +7,8 @@ import voluptuous as vol from .util import api_process_hostcontrol, api_process, api_validate from ..const import ( ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES, - ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO) + ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO, ATTR_OUTPUT) +from ..validate import ALSA_CHANNEL _LOGGER = logging.getLogger(__name__) @@ -15,6 +16,13 @@ SCHEMA_VERSION = vol.Schema({ vol.Optional(ATTR_VERSION): vol.Coerce(str), }) +SCHEMA_OPTIONS = vol.Schema({ + vol.Optional(ATTR_AUDIO): vol.Schema({ + vol.Optional(ATTR_OUTPUT): ALSA_CHANNEL, + vol.Optional(ATTR_INPUT): ALSA_CHANNEL, + }) +}) + class APIHost(object): """Handle rest api for host functions.""" @@ -38,6 +46,19 @@ class APIHost(object): ATTR_OS: self.host_control.os_info, } + @api_process + async def options(self, request): + """Process host options.""" + body = await api_validate(SCHEMA_OPTIONS, request) + + if ATTR_AUDIO in body: + if ATTR_OUTPUT in body[ATTR_AUDIO]: + self.config.audio_output = body[ATTR_AUDIO][ATTR_OUTPUT] + if ATTR_INPUT in body[ATTR_AUDIO]: + self.config.audio_input = body[ATTR_AUDIO][ATTR_INPUT] + + return True + @api_process_hostcontrol def reboot(self, request): """Reboot host. diff --git a/hassio/api/security.py b/hassio/api/security.py index 12a09f638..01887e200 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -98,5 +98,5 @@ class APISecurity(object): session = hashlib.sha256(os.urandom(54)).hexdigest() # store session - self.config.security_sessions = (session, valid_until) + self.config.add_security_session(session, valid_until) return {ATTR_SESSION: session} diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 2fd5c82e3..e8f4846eb 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -10,7 +10,7 @@ from ..const import ( 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 +from ..validate import validate_timezone _LOGGER = logging.getLogger(__name__) @@ -30,7 +30,7 @@ class APISupervisor(object): """Handle rest api for supervisor functions.""" def __init__(self, config, loop, supervisor, snapshots, addons, - host_control, websession): + host_control, updater): """Initialize supervisor rest api part.""" self.config = config self.loop = loop @@ -38,7 +38,7 @@ class APISupervisor(object): self.addons = addons self.snapshots = snapshots self.host_control = host_control - self.websession = websession + self.updater = updater @api_process async def ping(self, request): @@ -64,8 +64,8 @@ class APISupervisor(object): return { ATTR_VERSION: HASSIO_VERSION, - ATTR_LAST_VERSION: self.config.last_hassio, - ATTR_BETA_CHANNEL: self.config.upstream_beta, + ATTR_LAST_VERSION: self.updater.version_hassio, + ATTR_BETA_CHANNEL: self.updater.beta_channel, ATTR_ARCH: self.config.arch, ATTR_TIMEZONE: self.config.timezone, ATTR_ADDONS: list_addons, @@ -78,7 +78,7 @@ class APISupervisor(object): body = await api_validate(SCHEMA_OPTIONS, request) if ATTR_BETA_CHANNEL in body: - self.config.upstream_beta = body[ATTR_BETA_CHANNEL] + self.updater.beta_channel = body[ATTR_BETA_CHANNEL] if ATTR_TIMEZONE in body: self.config.timezone = body[ATTR_TIMEZONE] @@ -93,7 +93,7 @@ class APISupervisor(object): async def update(self, request): """Update supervisor OS.""" body = await api_validate(SCHEMA_VERSION, request) - version = body.get(ATTR_VERSION, self.config.last_hassio) + version = body.get(ATTR_VERSION, self.updater.version_hassio) if version == self.supervisor.version: raise RuntimeError("Version {} is already in use".format(version)) @@ -107,7 +107,7 @@ class APISupervisor(object): tasks = [ self.addons.reload(), self.snapshots.reload(), - self.config.fetch_update_infos(self.websession), + self.updater.fetch_data(), self.host_control.load() ] results, _ = await asyncio.shield( diff --git a/hassio/config.py b/hassio/config.py index b8a2630fb..ae076520c 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -4,121 +4,61 @@ import logging import os from pathlib import Path, PurePath -import voluptuous as vol - -from .const import FILE_HASSIO_CONFIG, HASSIO_DATA -from .tools import fetch_last_versions, JsonConfig, validate_timezone +from .const import ( + FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_SECURITY, ATTR_SESSIONS, + ATTR_PASSWORD, ATTR_TOTP, ATTR_TIMEZONE, ATTR_API_ENDPOINT, + ATTR_INITIALIZE, ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO, ATTR_INPUT, + ATTR_OUTPUT) +from .tools import JsonConfig +from .validate import SCHEMA_HASSIO_CONFIG _LOGGER = logging.getLogger(__name__) DATETIME_FORMAT = "%Y%m%d %H:%M:%S" HOMEASSISTANT_CONFIG = PurePath("homeassistant") -HOMEASSISTANT_LAST = 'homeassistant_last' HASSIO_SSL = PurePath("ssl") -HASSIO_LAST = 'hassio_last' ADDONS_CORE = PurePath("addons/core") ADDONS_LOCAL = PurePath("addons/local") ADDONS_GIT = PurePath("addons/git") ADDONS_DATA = PurePath("addons/data") -ADDONS_CUSTOM_LIST = 'addons_custom_list' BACKUP_DATA = PurePath("backup") SHARE_DATA = PurePath("share") TMP_DATA = PurePath("tmp") -UPSTREAM_BETA = 'upstream_beta' -API_ENDPOINT = 'api_endpoint' -TIMEZONE = 'timezone' - -SECURITY_INITIALIZE = 'security_initialize' -SECURITY_TOTP = 'security_totp' -SECURITY_PASSWORD = 'security_password' -SECURITY_SESSIONS = 'security_sessions' - - -# pylint: disable=no-value-for-parameter -SCHEMA_CONFIG = vol.Schema({ - vol.Optional(UPSTREAM_BETA, default=False): vol.Boolean(), - vol.Optional(API_ENDPOINT): vol.Coerce(str), - vol.Optional(TIMEZONE, default='UTC'): validate_timezone, - vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str), - vol.Optional(HASSIO_LAST): vol.Coerce(str), - vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()], - vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(), - vol.Optional(SECURITY_TOTP): vol.Coerce(str), - vol.Optional(SECURITY_PASSWORD): vol.Coerce(str), - vol.Optional(SECURITY_SESSIONS, default={}): - {vol.Coerce(str): vol.Coerce(str)}, -}, extra=vol.REMOVE_EXTRA) - class CoreConfig(JsonConfig): """Hold all core config data.""" def __init__(self): """Initialize config object.""" - super().__init__(FILE_HASSIO_CONFIG, SCHEMA_CONFIG) + super().__init__(FILE_HASSIO_CONFIG, SCHEMA_HASSIO_CONFIG) self.arch = None - async def fetch_update_infos(self, websession): - """Read current versions from web.""" - last = await fetch_last_versions(websession, beta=self.upstream_beta) - - if last: - self._data.update({ - HOMEASSISTANT_LAST: last.get('homeassistant'), - HASSIO_LAST: last.get('hassio'), - }) - self.save() - return True - - return False - @property def api_endpoint(self): """Return IP address of api endpoint.""" - return self._data[API_ENDPOINT] + return self._data[ATTR_API_ENDPOINT] @api_endpoint.setter def api_endpoint(self, value): """Store IP address of api endpoint.""" - self._data[API_ENDPOINT] = value - - @property - def upstream_beta(self): - """Return True if we run in beta upstream.""" - return self._data[UPSTREAM_BETA] - - @upstream_beta.setter - def upstream_beta(self, value): - """Set beta upstream mode.""" - self._data[UPSTREAM_BETA] = bool(value) - self.save() + self._data[ATTR_API_ENDPOINT] = value @property def timezone(self): """Return system timezone.""" - return self._data[TIMEZONE] + return self._data[ATTR_TIMEZONE] @timezone.setter def timezone(self, value): """Set system timezone.""" - self._data[TIMEZONE] = value + self._data[ATTR_TIMEZONE] = value self.save() - @property - def last_homeassistant(self): - """Actual version of homeassistant.""" - return self._data.get(HOMEASSISTANT_LAST) - - @property - def last_hassio(self): - """Actual version of hassio.""" - return self._data.get(HASSIO_LAST) - @property def path_hassio(self): """Return hassio data path.""" @@ -207,73 +147,101 @@ class CoreConfig(JsonConfig): @property def addons_repositories(self): """Return list of addons custom repositories.""" - return self._data[ADDONS_CUSTOM_LIST] + return self._data[ATTR_ADDONS_CUSTOM_LIST] - @addons_repositories.setter - def addons_repositories(self, repo): + def add_addon_repository(self, repo): """Add a custom repository to list.""" - if repo in self._data[ADDONS_CUSTOM_LIST]: + if repo in self._data[ATTR_ADDONS_CUSTOM_LIST]: return - self._data[ADDONS_CUSTOM_LIST].append(repo) + self._data[ATTR_ADDONS_CUSTOM_LIST].append(repo) self.save() def drop_addon_repository(self, repo): """Remove a custom repository from list.""" - if repo not in self._data[ADDONS_CUSTOM_LIST]: + if repo not in self._data[ATTR_ADDONS_CUSTOM_LIST]: return - self._data[ADDONS_CUSTOM_LIST].remove(repo) + self._data[ATTR_ADDONS_CUSTOM_LIST].remove(repo) self.save() @property def security_initialize(self): """Return is security was initialize.""" - return self._data[SECURITY_INITIALIZE] + return self._data[ATTR_SECURITY][ATTR_INITIALIZE] @security_initialize.setter def security_initialize(self, value): """Set is security initialize.""" - self._data[SECURITY_INITIALIZE] = value + self._data[ATTR_SECURITY][ATTR_INITIALIZE] = value self.save() @property def security_totp(self): """Return the TOTP key.""" - return self._data.get(SECURITY_TOTP) + return self._data[ATTR_SECURITY].get(ATTR_TOTP) @security_totp.setter def security_totp(self, value): """Set the TOTP key.""" - self._data[SECURITY_TOTP] = value + self._data[ATTR_SECURITY][ATTR_TOTP] = value self.save() @property def security_password(self): """Return the password key.""" - return self._data.get(SECURITY_PASSWORD) + return self._data[ATTR_SECURITY].get(ATTR_PASSWORD) @security_password.setter def security_password(self, value): """Set the password key.""" - self._data[SECURITY_PASSWORD] = value + self._data[ATTR_SECURITY][ATTR_PASSWORD] = value self.save() @property def security_sessions(self): """Return api sessions.""" - return {session: datetime.strptime(until, DATETIME_FORMAT) for - session, until in self._data[SECURITY_SESSIONS].items()} + if ATTR_SESSIONS not in self._data[ATTR_SECURITY]: + return {} - @security_sessions.setter - def security_sessions(self, value): + return { + session: datetime.strptime(until, DATETIME_FORMAT) for + session, until in self._data[ATTR_SECURITY][ATTR_SESSIONS].items() + } + + def add_security_session(self, session, valid): """Set the a new session.""" - session, valid = value - if valid is None: - self._data[SECURITY_SESSIONS].pop(session, None) - else: - self._data[SECURITY_SESSIONS].update( - {session: valid.strftime(DATETIME_FORMAT)} - ) - + self._data[ATTR_SECURITY][ATTR_SESSIONS].update( + {session: valid.strftime(DATETIME_FORMAT)} + ) + self.save() + + def drop_security_session(self, session): + """Delete the a session.""" + if ATTR_SESSIONS not in self._data[ATTR_SECURITY]: + return + + self._data[ATTR_SECURITY][ATTR_SESSIONS].pop(session, None) + self.save() + + @property + def audio_output(self): + """Return ALSA audio output card,dev.""" + return self._data[ATTR_AUDIO].get(ATTR_OUTPUT) + + @audio_output.setter + def audio_output(self, value): + """Set ALSA audio output card,dev.""" + self._data[ATTR_AUDIO][ATTR_OUTPUT] = value + self.save() + + @property + def audio_input(self): + """Return ALSA audio input card,dev.""" + return self._data[ATTR_AUDIO].get(ATTR_INPUT) + + @audio_input.setter + def audio_input(self, value): + """Set ALSA audio input card,dev.""" + self._data[ATTR_AUDIO][ATTR_INPUT] = value self.save() diff --git a/hassio/const.py b/hassio/const.py index ecbca9d19..65b8c95b7 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -4,9 +4,7 @@ from pathlib import Path HASSIO_VERSION = '0.51' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' - 'hassio/master/version.json') -URL_HASSIO_VERSION_BETA = ('https://raw.githubusercontent.com/home-assistant/' - 'hassio/dev/version.json') + 'hassio/{}/version.json') URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons' @@ -25,6 +23,7 @@ RESTART_EXIT_CODE = 100 FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json") FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json") FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json") +FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json") SOCKET_DOCKER = Path("/var/run/docker.sock") SOCKET_HC = Path("/var/run/hassio-hc.sock") @@ -83,6 +82,7 @@ ATTR_PASSWORD = 'password' ATTR_TOTP = 'totp' ATTR_INITIALIZE = 'initialize' ATTR_SESSION = 'session' +ATTR_SESSIONS = 'sessions' ATTR_LOCATON = 'location' ATTR_BUILD = 'build' ATTR_DEVICES = 'devices' @@ -95,6 +95,7 @@ ATTR_USER = 'user' ATTR_SYSTEM = 'system' ATTR_SNAPSHOTS = 'snapshots' ATTR_HOMEASSISTANT = 'homeassistant' +ATTR_HASSIO = 'hassio' ATTR_FOLDERS = 'folders' ATTR_SIZE = 'size' ATTR_TYPE = 'type' @@ -103,8 +104,12 @@ ATTR_AUTO_UPDATE = 'auto_update' ATTR_CUSTOM = 'custom' ATTR_AUDIO = 'audio' ATTR_INPUT = 'input' +ATTR_OUTPUT = 'output' ATTR_DISK = 'disk' ATTR_SERIAL = 'serial' +ATTR_SECURITY = 'security' +ATTR_API_ENDPOINT = 'api_endpoint' +ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list' STARTUP_INITIALIZE = 'initialize' STARTUP_SYSTEM = 'system' diff --git a/hassio/core.py b/hassio/core.py index 1a4fedc80..b2cebed7c 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -19,6 +19,7 @@ from .homeassistant import HomeAssistant from .scheduler import Scheduler from .dock.supervisor import DockerSupervisor from .snapshots import SnapshotsManager +from .updater import Updater from .tasks import ( hassio_update, homeassistant_watchdog, api_sessions_cleanup, addons_update) from .tools import get_local_ip, fetch_timezone @@ -35,6 +36,7 @@ class HassIO(object): self.loop = loop self.config = config self.websession = aiohttp.ClientSession(loop=loop) + self.updater = Updater(config, loop, self.websession) self.scheduler = Scheduler(loop) self.api = RestAPI(config, loop) self.hardware = Hardware() @@ -46,7 +48,7 @@ class HassIO(object): # init homeassistant self.homeassistant = HomeAssistant( - config, loop, self.dock, self.websession) + config, loop, self.dock, self.updater) # init HostControl self.host_control = HostControl(loop) @@ -87,7 +89,7 @@ class HassIO(object): self.api.register_network(self.host_control) self.api.register_supervisor( self.supervisor, self.snapshots, self.addons, self.host_control, - self.websession) + self.updater) self.api.register_homeassistant(self.homeassistant) self.api.register_addons(self.addons) self.api.register_security() @@ -113,7 +115,7 @@ class HassIO(object): # schedule self update task self.scheduler.register_task( - hassio_update(self.config, self.supervisor, self.websession), + hassio_update(self.supervisor, self.updater), RUN_UPDATE_SUPERVISOR_TASKS) # schedule snapshot update tasks @@ -128,7 +130,7 @@ class HassIO(object): # on release channel, try update itself # on beta channel, only read new versions await asyncio.wait( - [hassio_update(self.config, self.supervisor, self.websession)()], + [hassio_update(self.supervisor, self.updater)()], loop=self.loop ) diff --git a/hassio/homeassistant.py b/hassio/homeassistant.py index 8f519b370..22511da66 100644 --- a/hassio/homeassistant.py +++ b/hassio/homeassistant.py @@ -16,12 +16,12 @@ _LOGGER = logging.getLogger(__name__) class HomeAssistant(JsonConfig): """Hass core object for handle it.""" - def __init__(self, config, loop, dock, websession): + def __init__(self, config, loop, dock, updater): """Initialize hass object.""" super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG) self.config = config self.loop = loop - self.websession = websession + self.updater = updater self.docker = DockerHomeAssistant(config, loop, dock, self) async def prepare(self): @@ -45,7 +45,7 @@ class HomeAssistant(JsonConfig): """Return last available version of homeassistant.""" if self.is_custom_image: return self._data.get(ATTR_LAST_VERSION) - return self.config.last_homeassistant + return self.updater.version_homeassistant @property def image(self): @@ -101,7 +101,7 @@ class HomeAssistant(JsonConfig): while True: # read homeassistant tag and install it if not self.last_version: - await self.config.fetch_update_infos(self.websession) + await self.updater.fetch_data() tag = self.last_version if tag and await self.docker.install(tag): diff --git a/hassio/tasks.py b/hassio/tasks.py index 7fd888869..5c39f66a6 100644 --- a/hassio/tasks.py +++ b/hassio/tasks.py @@ -13,7 +13,7 @@ def api_sessions_cleanup(config): now = datetime.now() for session, until_valid in config.security_sessions.items(): if now >= until_valid: - config.security_sessions = (session, None) + config.drop_security_session(session) return _api_sessions_cleanup @@ -43,21 +43,21 @@ def addons_update(loop, addons): return _addons_update -def hassio_update(config, supervisor, websession): +def hassio_update(supervisor, updater): """Create scheduler task for update of supervisor hassio.""" async def _hassio_update(): """Check and run update of supervisor hassio.""" - await config.fetch_update_infos(websession) - if config.last_hassio == supervisor.version: + await updater.fetch_data() + if updater.version_hassio == supervisor.version: return # don't perform a update on beta/dev channel - if config.upstream_beta: + if updater.beta_channel: _LOGGER.warning("Ignore Hass.IO update on beta upstream!") return - _LOGGER.info("Found new HassIO version %s.", config.last_hassio) - await supervisor.update(config.last_hassio) + _LOGGER.info("Found new HassIO version %s.", updater.version_hassio) + await supervisor.update(updater.version_hassio) return _hassio_update diff --git a/hassio/tools.py b/hassio/tools.py index 975cfc303..6832b22df 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -1,41 +1,21 @@ """Tools file for HassIO.""" import asyncio from contextlib import suppress +from datetime import datetime import json import logging import socket import aiohttp import async_timeout -import pytz import voluptuous as vol from voluptuous.humanize import humanize_error -from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA - _LOGGER = logging.getLogger(__name__) FREEGEOIP_URL = "https://freegeoip.io/json/" -async def fetch_last_versions(websession, beta=False): - """Fetch current versions from github. - - Is a coroutine. - """ - url = URL_HASSIO_VERSION_BETA if beta else URL_HASSIO_VERSION - try: - with async_timeout.timeout(10, loop=websession.loop): - async with websession.get(url) as request: - return await request.json(content_type=None) - - except (aiohttp.ClientError, asyncio.TimeoutError, KeyError) as err: - _LOGGER.warning("Can't fetch versions from %s! %s", url, err) - - except json.JSONDecodeError as err: - _LOGGER.warning("Can't parse versions from %s! %s", url, err) - - def get_local_ip(loop): """Retrieve local IP address. @@ -76,19 +56,6 @@ def read_json_file(jsonfile): return json.loads(cfile.read()) -def validate_timezone(timezone): - """Validate voluptuous timezone.""" - try: - pytz.timezone(timezone) - except pytz.exceptions.UnknownTimeZoneError: - raise vol.Invalid( - "Invalid time zone passed in. Valid options can be found here: " - "http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \ - from None - - return timezone - - async def fetch_timezone(websession): """Read timezone from freegeoip.""" data = {} @@ -140,3 +107,27 @@ class JsonConfig(object): _LOGGER.error("Can't store config in %s", self._file) return False return True + + +class AsyncThrottle(object): + """ + Decorator that prevents a function from being called more than once every + time period. + """ + def __init__(self, delta): + """Initialize async throttle.""" + self.throttle_period = delta + self.time_of_last_call = datetime.min + + def __call__(self, method): + """Throttle function""" + async def wrapper(*args, **kwargs): + """Throttle function wrapper""" + now = datetime.now() + time_since_last_call = now - self.time_of_last_call + + if time_since_last_call > self.throttle_period: + self.time_of_last_call = now + return await method(*args, **kwargs) + + return wrapper diff --git a/hassio/updater.py b/hassio/updater.py new file mode 100644 index 000000000..eebb8e0bb --- /dev/null +++ b/hassio/updater.py @@ -0,0 +1,85 @@ +"""Fetch last versions from webserver.""" +import asyncio +from datetime import timedelta +import json +import logging + +import aiohttp +import async_timeout + +from .const import ( + URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO, + ATTR_BETA_CHANNEL) +from .tools import AsyncThrottle, JsonConfig +from .validate import SCHEMA_UPDATER_CONFIG + +_LOGGER = logging.getLogger(__name__) + + +class Updater(JsonConfig): + """Fetch last versions from version.json.""" + + def __init__(self, config, loop, websession): + """Initialize updater.""" + super().__init__(FILE_HASSIO_UPDATER, SCHEMA_UPDATER_CONFIG) + self.config = config + self.loop = loop + self.websession = websession + + @property + def version_homeassistant(self): + """Return last version of homeassistant.""" + return self._data.get(ATTR_HOMEASSISTANT) + + @property + def version_hassio(self): + """Return last version of hassio.""" + return self._data.get(ATTR_HASSIO) + + @property + def upstream(self): + """Return Upstream branch for version.""" + if self.beta_channel: + return 'dev' + return 'master' + + @property + def beta_channel(self): + """Return True if we run in beta upstream.""" + return self._data[ATTR_BETA_CHANNEL] + + @beta_channel.setter + def beta_channel(self, value): + """Set beta upstream mode.""" + self._data[ATTR_BETA_CHANNEL] = bool(value) + self.save() + + @AsyncThrottle(timedelta(seconds=60)) + async def fetch_data(self): + """Fetch current versions from github. + + Is a coroutine. + """ + url = URL_HASSIO_VERSION.format(self.upstream) + try: + with async_timeout.timeout(10, loop=self.loop): + async with self.websession.get(url) as request: + data = await request.json(content_type=None) + + except (aiohttp.ClientError, asyncio.TimeoutError, KeyError) as err: + _LOGGER.warning("Can't fetch versions from %s -> %s", url, err) + return + + except json.JSONDecodeError as err: + _LOGGER.warning("Can't parse versions from %s -> %s", url, err) + return + + # data valid? + if not data: + _LOGGER.warning("Invalid data from %s", url) + return + + # update versions + self._data[ATTR_HOMEASSISTANT] = data.get('homeassistant') + self._data[ATTR_HASSIO] = data.get('hassio') + self.save() diff --git a/hassio/validate.py b/hassio/validate.py index ae0cb5a60..c2b6ae04e 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -1,11 +1,31 @@ """Validate functions.""" import voluptuous as vol -from .const import ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION +import pytz + +from .const import ( + ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_SESSIONS, ATTR_PASSWORD, + ATTR_TOTP, ATTR_INITIALIZE, ATTR_BETA_CHANNEL, ATTR_TIMEZONE, ATTR_INPUT, + ATTR_SECURITY, ATTR_API_ENDPOINT, ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO, + ATTR_OUTPUT, ATTR_HOMEASSISTANT, ATTR_HASSIO) NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) HASS_DEVICES = [vol.Match(r"^[^/]*$")] +ALSA_CHANNEL = vol.Match(r"\d+,\d+") + + +def validate_timezone(timezone): + """Validate voluptuous timezone.""" + try: + pytz.timezone(timezone) + except pytz.exceptions.UnknownTimeZoneError: + raise vol.Invalid( + "Invalid time zone passed in. Valid options can be found here: " + "http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \ + from None + + return timezone def convert_to_docker_ports(data): @@ -40,3 +60,30 @@ SCHEMA_HASS_CONFIG = vol.Schema({ vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str), vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), }) + + +# pylint: disable=no-value-for-parameter +SCHEMA_UPDATER_CONFIG = vol.Schema({ + vol.Optional(ATTR_BETA_CHANNEL, default=False): vol.Boolean(), + vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str), + vol.Optional(ATTR_HASSIO): vol.Coerce(str), +}) + + +# pylint: disable=no-value-for-parameter +SCHEMA_HASSIO_CONFIG = vol.Schema({ + vol.Optional(ATTR_API_ENDPOINT): vol.Coerce(str), + vol.Optional(ATTR_TIMEZONE, default='UTC'): validate_timezone, + vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[]): [vol.Url()], + vol.Optional(ATTR_SECURITY, default={}): vol.Schema({ + vol.Optional(ATTR_INITIALIZE, default=False): vol.Boolean(), + vol.Optional(ATTR_TOTP): vol.Coerce(str), + vol.Optional(ATTR_PASSWORD): vol.Coerce(str), + vol.Optional(ATTR_SESSIONS, default={}): + vol.Schema({vol.Coerce(str): vol.Coerce(str)}), + }), + vol.Optional(ATTR_AUDIO, default={}): vol.Schema({ + vol.Optional(ATTR_OUTPUT): ALSA_CHANNEL, + vol.Optional(ATTR_INPUT): ALSA_CHANNEL, + }), +}, extra=vol.REMOVE_EXTRA) From cd3b38290234a6c38f84bbbfbc2b506af45ffd44 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Aug 2017 10:47:39 +0200 Subject: [PATCH 09/14] Cleanup json / api code with new options (#137) * Cleanup json / api code with new options * fix lint --- API.md | 6 ++---- hassio/api/host.py | 19 +++++++++---------- hassio/config.py | 35 ++++++++++++++--------------------- hassio/const.py | 2 ++ hassio/validate.py | 24 ++++++++++-------------- 5 files changed, 37 insertions(+), 49 deletions(-) diff --git a/API.md b/API.md index 8a5aa4b3d..ae5400f95 100644 --- a/API.md +++ b/API.md @@ -222,10 +222,8 @@ Return QR-Code ```json { - "audio": { - "input": "0,0", - "output": "0,0" - } + "audio_input": "0,0", + "audio_output": "0,0" } ``` diff --git a/hassio/api/host.py b/hassio/api/host.py index 2a53e5221..074452897 100644 --- a/hassio/api/host.py +++ b/hassio/api/host.py @@ -7,7 +7,8 @@ import voluptuous as vol from .util import api_process_hostcontrol, api_process, api_validate from ..const import ( ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES, - ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO, ATTR_OUTPUT) + ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO, ATTR_AUDIO_INPUT, + ATTR_AUDIO_OUTPUT) from ..validate import ALSA_CHANNEL _LOGGER = logging.getLogger(__name__) @@ -17,10 +18,8 @@ SCHEMA_VERSION = vol.Schema({ }) SCHEMA_OPTIONS = vol.Schema({ - vol.Optional(ATTR_AUDIO): vol.Schema({ - vol.Optional(ATTR_OUTPUT): ALSA_CHANNEL, - vol.Optional(ATTR_INPUT): ALSA_CHANNEL, - }) + vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL, + vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL, }) @@ -51,11 +50,11 @@ class APIHost(object): """Process host options.""" body = await api_validate(SCHEMA_OPTIONS, request) - if ATTR_AUDIO in body: - if ATTR_OUTPUT in body[ATTR_AUDIO]: - self.config.audio_output = body[ATTR_AUDIO][ATTR_OUTPUT] - if ATTR_INPUT in body[ATTR_AUDIO]: - self.config.audio_input = body[ATTR_AUDIO][ATTR_INPUT] + if ATTR_AUDIO_OUTPUT in body: + self.config.audio_output = body[ATTR_AUDIO_OUTPUT] + + if ATTR_AUDIO_INPUT in body: + self.config.audio_input = body[ATTR_AUDIO_INPUT] return True diff --git a/hassio/config.py b/hassio/config.py index ae076520c..7f501ba68 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -7,8 +7,7 @@ from pathlib import Path, PurePath from .const import ( FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_SECURITY, ATTR_SESSIONS, ATTR_PASSWORD, ATTR_TOTP, ATTR_TIMEZONE, ATTR_API_ENDPOINT, - ATTR_INITIALIZE, ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO, ATTR_INPUT, - ATTR_OUTPUT) + ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT) from .tools import JsonConfig from .validate import SCHEMA_HASSIO_CONFIG @@ -168,80 +167,74 @@ class CoreConfig(JsonConfig): @property def security_initialize(self): """Return is security was initialize.""" - return self._data[ATTR_SECURITY][ATTR_INITIALIZE] + return self._data[ATTR_SECURITY] @security_initialize.setter def security_initialize(self, value): """Set is security initialize.""" - self._data[ATTR_SECURITY][ATTR_INITIALIZE] = value + self._data[ATTR_SECURITY] = value self.save() @property def security_totp(self): """Return the TOTP key.""" - return self._data[ATTR_SECURITY].get(ATTR_TOTP) + return self._data.get(ATTR_TOTP) @security_totp.setter def security_totp(self, value): """Set the TOTP key.""" - self._data[ATTR_SECURITY][ATTR_TOTP] = value + self._data[ATTR_TOTP] = value self.save() @property def security_password(self): """Return the password key.""" - return self._data[ATTR_SECURITY].get(ATTR_PASSWORD) + return self._data.get(ATTR_PASSWORD) @security_password.setter def security_password(self, value): """Set the password key.""" - self._data[ATTR_SECURITY][ATTR_PASSWORD] = value + self._data[ATTR_PASSWORD] = value self.save() @property def security_sessions(self): """Return api sessions.""" - if ATTR_SESSIONS not in self._data[ATTR_SECURITY]: - return {} - return { session: datetime.strptime(until, DATETIME_FORMAT) for - session, until in self._data[ATTR_SECURITY][ATTR_SESSIONS].items() + session, until in self._data[ATTR_SESSIONS].items() } def add_security_session(self, session, valid): """Set the a new session.""" - self._data[ATTR_SECURITY][ATTR_SESSIONS].update( + self._data[ATTR_SESSIONS].update( {session: valid.strftime(DATETIME_FORMAT)} ) self.save() def drop_security_session(self, session): """Delete the a session.""" - if ATTR_SESSIONS not in self._data[ATTR_SECURITY]: - return - - self._data[ATTR_SECURITY][ATTR_SESSIONS].pop(session, None) + self._data[ATTR_SESSIONS].pop(session, None) self.save() @property def audio_output(self): """Return ALSA audio output card,dev.""" - return self._data[ATTR_AUDIO].get(ATTR_OUTPUT) + return self._data.get(ATTR_AUDIO_OUTPUT) @audio_output.setter def audio_output(self, value): """Set ALSA audio output card,dev.""" - self._data[ATTR_AUDIO][ATTR_OUTPUT] = value + self._data[ATTR_AUDIO_OUTPUT] = value self.save() @property def audio_input(self): """Return ALSA audio input card,dev.""" - return self._data[ATTR_AUDIO].get(ATTR_INPUT) + return self._data.get(ATTR_AUDIO_INPUT) @audio_input.setter def audio_input(self, value): """Set ALSA audio input card,dev.""" - self._data[ATTR_AUDIO][ATTR_INPUT] = value + self._data[ATTR_AUDIO_INPUT] = value self.save() diff --git a/hassio/const.py b/hassio/const.py index 65b8c95b7..65f7ea853 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -103,6 +103,8 @@ ATTR_TIMEOUT = 'timeout' ATTR_AUTO_UPDATE = 'auto_update' ATTR_CUSTOM = 'custom' ATTR_AUDIO = 'audio' +ATTR_AUDIO_INPUT = 'audio_input' +ATTR_AUDIO_OUTPUT = 'audio_output' ATTR_INPUT = 'input' ATTR_OUTPUT = 'output' ATTR_DISK = 'disk' diff --git a/hassio/validate.py b/hassio/validate.py index c2b6ae04e..6f50ecc98 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -5,9 +5,9 @@ import pytz from .const import ( ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_SESSIONS, ATTR_PASSWORD, - ATTR_TOTP, ATTR_INITIALIZE, ATTR_BETA_CHANNEL, ATTR_TIMEZONE, ATTR_INPUT, - ATTR_SECURITY, ATTR_API_ENDPOINT, ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO, - ATTR_OUTPUT, ATTR_HOMEASSISTANT, ATTR_HASSIO) + ATTR_TOTP, ATTR_SECURITY, ATTR_BETA_CHANNEL, ATTR_TIMEZONE, + ATTR_API_ENDPOINT, ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT, + ATTR_AUDIO_INPUT, ATTR_HOMEASSISTANT, ATTR_HASSIO) NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) @@ -75,15 +75,11 @@ SCHEMA_HASSIO_CONFIG = vol.Schema({ vol.Optional(ATTR_API_ENDPOINT): vol.Coerce(str), vol.Optional(ATTR_TIMEZONE, default='UTC'): validate_timezone, vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[]): [vol.Url()], - vol.Optional(ATTR_SECURITY, default={}): vol.Schema({ - vol.Optional(ATTR_INITIALIZE, default=False): vol.Boolean(), - vol.Optional(ATTR_TOTP): vol.Coerce(str), - vol.Optional(ATTR_PASSWORD): vol.Coerce(str), - vol.Optional(ATTR_SESSIONS, default={}): - vol.Schema({vol.Coerce(str): vol.Coerce(str)}), - }), - vol.Optional(ATTR_AUDIO, default={}): vol.Schema({ - vol.Optional(ATTR_OUTPUT): ALSA_CHANNEL, - vol.Optional(ATTR_INPUT): ALSA_CHANNEL, - }), + vol.Optional(ATTR_SECURITY, default=False): vol.Boolean(), + vol.Optional(ATTR_TOTP): vol.Coerce(str), + vol.Optional(ATTR_PASSWORD): vol.Coerce(str), + vol.Optional(ATTR_SESSIONS, default={}): + vol.Schema({vol.Coerce(str): vol.Coerce(str)}), + vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL, + vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL, }, extra=vol.REMOVE_EXTRA) From 8d468328f381a677956400c941b4614dad05d9a0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Aug 2017 16:54:42 +0200 Subject: [PATCH 10/14] Expose new function to add-ons (#138) * Expose new function to add-ons * Rename `hassio` to `hassio_api` * fix lint * done --- API.md | 14 ++++++++--- hassio/addons/addon.py | 53 ++++++++++++++++++++++++++++++++++++++- hassio/addons/validate.py | 9 +++++-- hassio/api/addons.py | 11 ++++++++ hassio/api/host.py | 1 - hassio/const.py | 1 + hassio/dock/addon.py | 16 ++++++++++++ hassio/updater.py | 1 + 8 files changed, 99 insertions(+), 7 deletions(-) diff --git a/API.md b/API.md index ae5400f95..bbec173cf 100644 --- a/API.md +++ b/API.md @@ -336,7 +336,9 @@ Get all available addons. "privileged": ["NET_ADMIN", "SYS_ADMIN"], "devices": ["/dev/xy"], "url": "null|url", - "logo": "bool" + "logo": "bool", + "audio": "bool", + "hassio_api": "bool" } ], "repositories": [ @@ -373,7 +375,11 @@ Get all available addons. "privileged": ["NET_ADMIN", "SYS_ADMIN"], "devices": ["/dev/xy"], "logo": "bool", - "webui": "null|http(s)://[HOST]:port/xy/zx" + "hassio_api": "bool", + "webui": "null|http(s)://[HOST]:port/xy/zx", + "audio": "bool", + "audio_input": "null|0,0", + "audio_output": "null|0,0" } ``` @@ -389,10 +395,12 @@ Get all available addons. "CONTAINER": "port|[ip, port]" }, "options": {}, + "audio_output": "null|0,0", + "audio_input": "null|0,0" } ``` -For reset custom network settings, set it `null`. +For reset custom network/audio settings, set it `null`. - POST `/addons/{addon}/start` diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index f3bcb9f20..72293adcf 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -19,7 +19,8 @@ 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_WEBUI) + ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI, + ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT) from .util import check_installed from ..dock.addon import DockerAddon from ..tools import write_json_file, read_json_file @@ -244,6 +245,56 @@ class Addon(object): """Return list of privilege.""" return self._mesh.get(ATTR_PRIVILEGED) + @property + def use_hassio_api(self): + """Return True if the add-on access to hassio api.""" + return self._mesh[ATTR_HASSIO_API] + + @property + def with_audio(self): + """Return True if the add-on access to audio.""" + return self._mesh[ATTR_AUDIO] + + @property + def audio_output(self): + """Return ALSA config for output or None.""" + if not self.with_audio: + return + + setting = self.config.audio_output + if self.is_installed and ATTR_AUDIO_OUTPUT in self.data.user[self._id]: + setting = self.data.user[self._id][ATTR_AUDIO_OUTPUT] + return setting + + @audio_output.setter + def audio_output(self, value): + """Set/remove custom audio output settings.""" + if value is None: + self.data.user[self._id].pop(ATTR_AUDIO_OUTPUT, None) + else: + self.data.user[self._id][ATTR_AUDIO_OUTPUT] = value + self.data.save() + + @property + def audio_input(self): + """Return ALSA config for input or None.""" + if not self.with_audio: + return + + setting = self.config.audio_input + if self.is_installed and ATTR_AUDIO_INPUT in self.data.user[self._id]: + setting = self.data.user[self._id][ATTR_AUDIO_INPUT] + return setting + + @audio_input.setter + def audio_input(self, value): + """Set/remove custom audio input settings.""" + if value is None: + self.data.user[self._id].pop(ATTR_AUDIO_INPUT, None) + else: + self.data.user[self._id][ATTR_AUDIO_INPUT] = value + self.data.save() + @property def url(self): """Return url of addon.""" diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 34564cbb2..d7d7def84 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -10,8 +10,9 @@ 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_WEBUI) -from ..validate import NETWORK_PORT, DOCKER_PORTS + ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, + ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API) +from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL MAP_VOLUME = r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$" @@ -73,6 +74,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_MAP, default=[]): [vol.Match(MAP_VOLUME)], vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)], + vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(), + vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(), vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({ vol.Coerce(str): vol.Any(ADDON_ELEMENT, [ @@ -101,6 +104,8 @@ SCHEMA_ADDON_USER = vol.Schema({ vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.Optional(ATTR_NETWORK): DOCKER_PORTS, + vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL, + vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL, }) diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 33bc547a2..3451f81f8 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -12,6 +12,7 @@ from ..const import ( ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK, ATTR_SLUG, ATTR_SOURCE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_ARCH, ATTR_MAINTAINER, ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED, + ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, BOOT_AUTO, BOOT_MANUAL, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY) from ..validate import DOCKER_PORTS @@ -76,6 +77,8 @@ class APIAddons(object): ATTR_DEVICES: self._pretty_devices(addon), ATTR_URL: addon.url, ATTR_LOGO: addon.with_logo, + ATTR_HASSIO_API: addon.use_hassio_api, + ATTR_AUDIO: addon.with_audio, }) data_repositories = [] @@ -123,6 +126,10 @@ class APIAddons(object): ATTR_DEVICES: self._pretty_devices(addon), ATTR_LOGO: addon.with_logo, ATTR_WEBUI: addon.webui, + ATTR_HASSIO_API: addon.use_hassio_api, + ATTR_AUDIO: addon.with_audio, + ATTR_AUDIO_INPUT: addon.audio_input, + ATTR_AUDIO_OUTPUT: addon.audio_output, } @api_process @@ -144,6 +151,10 @@ class APIAddons(object): addon.auto_update = body[ATTR_AUTO_UPDATE] if ATTR_NETWORK in body: addon.ports = body[ATTR_NETWORK] + if ATTR_AUDIO_INPUT in body: + addon.audio_input = body[ATTR_AUDIO_INPUT] + if ATTR_AUDIO_OUTPUT in body: + addon.audio_output = body[ATTR_AUDIO_OUTPUT] return True diff --git a/hassio/api/host.py b/hassio/api/host.py index 074452897..39fbbeb78 100644 --- a/hassio/api/host.py +++ b/hassio/api/host.py @@ -52,7 +52,6 @@ class APIHost(object): if ATTR_AUDIO_OUTPUT in body: self.config.audio_output = body[ATTR_AUDIO_OUTPUT] - if ATTR_AUDIO_INPUT in body: self.config.audio_input = body[ATTR_AUDIO_INPUT] diff --git a/hassio/const.py b/hassio/const.py index 65f7ea853..a5c5ab073 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -96,6 +96,7 @@ ATTR_SYSTEM = 'system' ATTR_SNAPSHOTS = 'snapshots' ATTR_HOMEASSISTANT = 'homeassistant' ATTR_HASSIO = 'hassio' +ATTR_HASSIO_API = 'hassio_api' ATTR_FOLDERS = 'folders' ATTR_SIZE = 'size' ATTR_TYPE = 'type' diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 8d573b58e..e5e3df2b0 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -32,6 +32,11 @@ class DockerAddon(DockerBase): def environment(self): """Return environment for docker add-on.""" addon_env = self.addon.environment or {} + if self.addon.with_audio: + addon_env.update({ + 'ALSA_OUTPUT': self.addon.audio_output, + 'ALSA_INPUT': self.addon.audio_input, + }) return { **addon_env, @@ -46,6 +51,16 @@ class DockerAddon(DockerBase): return {"/tmpfs": "{}".format(options)} return None + @property + def mapping(self): + """Return hosts mapping.""" + if not self.addon.use_hassio_api: + return None + + return { + 'hassio': self.config.api_endpoint, + } + @property def volumes(self): """Generate volumes for mappings.""" @@ -111,6 +126,7 @@ class DockerAddon(DockerBase): detach=True, network_mode=self.addon.network_mode, ports=self.addon.ports, + extra_hosts=self.mapping, devices=self.addon.devices, cap_add=self.addon.privileged, environment=self.environment, diff --git a/hassio/updater.py b/hassio/updater.py index eebb8e0bb..d880e8327 100644 --- a/hassio/updater.py +++ b/hassio/updater.py @@ -62,6 +62,7 @@ class Updater(JsonConfig): """ url = URL_HASSIO_VERSION.format(self.upstream) try: + _LOGGER.info("Fetch update data from %s", url) with async_timeout.timeout(10, loop=self.loop): async with self.websession.get(url) as request: data = await request.json(content_type=None) From b61d5625fe8eb626803d0fb253f194bd60751cfc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Aug 2017 16:59:43 +0200 Subject: [PATCH 11/14] update hass.io to 0.51 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index bade9cb1f..d30cdacd5 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "hassio": "0.50", + "hassio": "0.51", "homeassistant": "0.51-dev", "resinos": "1.0", "resinhup": "0.3", From 6a74893a30b14dad986f091a27d7144be35101c9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Aug 2017 18:01:53 +0200 Subject: [PATCH 12/14] Bugfix docker have no version (#139) --- hassio/api/supervisor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index e8f4846eb..7616fe3ef 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -99,7 +99,7 @@ class APISupervisor(object): raise RuntimeError("Version {} is already in use".format(version)) return await asyncio.shield( - self.supervisor.update(version=version), loop=self.loop) + self.supervisor.update(version), loop=self.loop) @api_process async def reload(self, request): From d1b30a0e95161bd3ef8252ad16b0986e3771fd4d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Aug 2017 18:17:23 +0200 Subject: [PATCH 13/14] fix last version of HomeAssistant (#140) --- hassio/api/homeassistant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index df2c55e3e..d16be1276 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -63,7 +63,7 @@ class APIHomeAssistant(object): async def update(self, request): """Update homeassistant.""" body = await api_validate(SCHEMA_VERSION, request) - version = body.get(ATTR_VERSION, self.config.last_homeassistant) + version = body.get(ATTR_VERSION, self.homeassistant.last_homeassistant) if version == self.homeassistant.version: raise RuntimeError("Version {} is already in use".format(version)) From c39d6357f310600c6c24b5004c8d2dc63dfad0e2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 8 Aug 2017 18:31:37 +0200 Subject: [PATCH 14/14] fix parameter --- hassio/api/homeassistant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index d16be1276..83415b6a0 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -63,7 +63,7 @@ class APIHomeAssistant(object): async def update(self, request): """Update homeassistant.""" body = await api_validate(SCHEMA_VERSION, request) - version = body.get(ATTR_VERSION, self.homeassistant.last_homeassistant) + version = body.get(ATTR_VERSION, self.homeassistant.last_version) if version == self.homeassistant.version: raise RuntimeError("Version {} is already in use".format(version))