Fix version merge conflict

This commit is contained in:
Pascal Vizeli 2017-08-08 21:12:32 +02:00
commit 261bda82db
22 changed files with 488 additions and 239 deletions

38
API.md
View File

@ -203,16 +203,27 @@ Return QR-Code
- POST `/host/reboot` - POST `/host/reboot`
- GET `/host/info` - GET `/host/info`
See HostControl info command.
```json ```json
{ {
"type": "", "type": "",
"version": "", "version": "",
"last_version": "", "last_version": "",
"features": ["shutdown", "reboot", "update", "network_info", "network_control"], "features": ["shutdown", "reboot", "update", "hostname", "network_info", "network_control"],
"hostname": "", "hostname": "",
"os": "" "os": "",
"audio": {
"input": "0,0",
"output": "0,0"
}
}
```
- POST `/host/options`
```json
{
"audio_input": "0,0",
"audio_output": "0,0"
} }
``` ```
@ -259,11 +270,6 @@ Optional:
```json ```json
{ {
"hostname": "", "hostname": "",
"mode": "dhcp|fixed",
"ssid": "",
"ip": "",
"netmask": "",
"gateway": ""
} }
``` ```
@ -330,7 +336,9 @@ Get all available addons.
"privileged": ["NET_ADMIN", "SYS_ADMIN"], "privileged": ["NET_ADMIN", "SYS_ADMIN"],
"devices": ["/dev/xy"], "devices": ["/dev/xy"],
"url": "null|url", "url": "null|url",
"logo": "bool" "logo": "bool",
"audio": "bool",
"hassio_api": "bool"
} }
], ],
"repositories": [ "repositories": [
@ -367,7 +375,11 @@ Get all available addons.
"privileged": ["NET_ADMIN", "SYS_ADMIN"], "privileged": ["NET_ADMIN", "SYS_ADMIN"],
"devices": ["/dev/xy"], "devices": ["/dev/xy"],
"logo": "bool", "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"
} }
``` ```
@ -383,10 +395,12 @@ Get all available addons.
"CONTAINER": "port|[ip, port]" "CONTAINER": "port|[ip, port]"
}, },
"options": {}, "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` - POST `/addons/{addon}/start`

View File

@ -1,11 +1,13 @@
# Hass.io # Hass.io
### First private cloud solution for home automation. ### 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. 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) ![](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 ## Installation

View File

@ -78,7 +78,7 @@ class AddonManager(object):
# don't add built-in repository to config # don't add built-in repository to config
if url not in BUILTIN_REPOSITORIES: 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] tasks = [_add_repository(url) for url in new_rep - old_rep]
if tasks: if tasks:

View File

@ -19,7 +19,8 @@ from ..const import (
ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT,
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP, ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP,
STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM, 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 .util import check_installed
from ..dock.addon import DockerAddon from ..dock.addon import DockerAddon
from ..tools import write_json_file, read_json_file from ..tools import write_json_file, read_json_file
@ -244,6 +245,56 @@ class Addon(object):
"""Return list of privilege.""" """Return list of privilege."""
return self._mesh.get(ATTR_PRIVILEGED) 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 @property
def url(self): def url(self):
"""Return url of addon.""" """Return url of addon."""
@ -413,14 +464,20 @@ class Addon(object):
return STATE_STOPPED return STATE_STOPPED
@check_installed @check_installed
async def start(self): def start(self):
"""Set options and start addon.""" """Set options and start addon.
return await self.addon_docker.run()
Return a coroutine.
"""
return self.addon_docker.run()
@check_installed @check_installed
async def stop(self): def stop(self):
"""Stop addon.""" """Stop addon.
return await self.addon_docker.stop()
Return a coroutine.
"""
return self.addon_docker.stop()
@check_installed @check_installed
async def update(self, version=None): async def update(self, version=None):
@ -430,7 +487,7 @@ class Addon(object):
if version == self.version_installed: if version == self.version_installed:
_LOGGER.warning( _LOGGER.warning(
"Addon %s is already installed in %s", self._id, version) "Addon %s is already installed in %s", self._id, version)
return True return False
if not await self.addon_docker.update(version): if not await self.addon_docker.update(version):
return False return False
@ -439,14 +496,20 @@ class Addon(object):
return True return True
@check_installed @check_installed
async def restart(self): def restart(self):
"""Restart addon.""" """Restart addon.
return await self.addon_docker.restart()
Return a coroutine.
"""
return self.addon_docker.restart()
@check_installed @check_installed
async def logs(self): def logs(self):
"""Return addons log output.""" """Return addons log output.
return await self.addon_docker.logs()
Return a coroutine.
"""
return self.addon_docker.logs()
@check_installed @check_installed
async def snapshot(self, tar_file): async def snapshot(self, tar_file):

View File

@ -10,8 +10,9 @@ from ..const import (
ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED, ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED,
ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED, ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED,
ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK, ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK,
ATTR_AUTO_UPDATE, ATTR_WEBUI) ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT,
from ..validate import NETWORK_PORT, DOCKER_PORTS 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))?$" MAP_VOLUME = r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$"
@ -38,14 +39,12 @@ STARTUP_ALL = [
PRIVILEGED_ALL = [ PRIVILEGED_ALL = [
"NET_ADMIN", "NET_ADMIN",
"SYS_ADMIN", "SYS_ADMIN",
"SYS_RAWIO"
] ]
def _migrate_startup(value): def _simple_startup(value):
"""Migrate startup schema. """Simple startup schema."""
REMOVE after 0.50-
"""
if value == "before": if value == "before":
return STARTUP_SERVICES return STARTUP_SERVICES
if value == "after": if value == "after":
@ -62,7 +61,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_URL): vol.Url(), vol.Optional(ATTR_URL): vol.Url(),
vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)], vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)],
vol.Required(ATTR_STARTUP): 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.Required(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_PORTS): DOCKER_PORTS, vol.Optional(ATTR_PORTS): DOCKER_PORTS,
@ -75,6 +74,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_MAP, default=[]): [vol.Match(MAP_VOLUME)], vol.Optional(ATTR_MAP, default=[]): [vol.Match(MAP_VOLUME)],
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)], 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_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({ vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [ vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
@ -103,6 +104,8 @@ SCHEMA_ADDON_USER = vol.Schema({
vol.Optional(ATTR_BOOT): vol.Optional(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): DOCKER_PORTS, vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
}) })

View File

@ -37,6 +37,7 @@ class RestAPI(object):
self.webapp.router.add_post('/host/reboot', api_host.reboot) 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/shutdown', api_host.shutdown)
self.webapp.router.add_post('/host/update', api_host.update) 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): def register_network(self, host_control):
"""Register network function.""" """Register network function."""
@ -46,11 +47,11 @@ class RestAPI(object):
self.webapp.router.add_post('/network/options', api_net.options) self.webapp.router.add_post('/network/options', api_net.options)
def register_supervisor(self, supervisor, snapshots, addons, host_control, def register_supervisor(self, supervisor, snapshots, addons, host_control,
websession): updater):
"""Register supervisor function.""" """Register supervisor function."""
api_supervisor = APISupervisor( api_supervisor = APISupervisor(
self.config, self.loop, supervisor, snapshots, addons, 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/ping', api_supervisor.ping)
self.webapp.router.add_get('/supervisor/info', api_supervisor.info) self.webapp.router.add_get('/supervisor/info', api_supervisor.info)

View File

@ -12,6 +12,7 @@ from ..const import (
ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK, ATTR_SLUG, ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK, ATTR_SLUG,
ATTR_SOURCE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_ARCH, ATTR_MAINTAINER, ATTR_SOURCE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_ARCH, ATTR_MAINTAINER,
ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED, ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED,
ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API,
BOOT_AUTO, BOOT_MANUAL, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY) BOOT_AUTO, BOOT_MANUAL, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY)
from ..validate import DOCKER_PORTS from ..validate import DOCKER_PORTS
@ -76,6 +77,8 @@ class APIAddons(object):
ATTR_DEVICES: self._pretty_devices(addon), ATTR_DEVICES: self._pretty_devices(addon),
ATTR_URL: addon.url, ATTR_URL: addon.url,
ATTR_LOGO: addon.with_logo, ATTR_LOGO: addon.with_logo,
ATTR_HASSIO_API: addon.use_hassio_api,
ATTR_AUDIO: addon.with_audio,
}) })
data_repositories = [] data_repositories = []
@ -123,6 +126,10 @@ class APIAddons(object):
ATTR_DEVICES: self._pretty_devices(addon), ATTR_DEVICES: self._pretty_devices(addon),
ATTR_LOGO: addon.with_logo, ATTR_LOGO: addon.with_logo,
ATTR_WEBUI: addon.webui, 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 @api_process
@ -144,6 +151,10 @@ class APIAddons(object):
addon.auto_update = body[ATTR_AUTO_UPDATE] addon.auto_update = body[ATTR_AUTO_UPDATE]
if ATTR_NETWORK in body: if ATTR_NETWORK in body:
addon.ports = body[ATTR_NETWORK] 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 return True
@ -152,20 +163,26 @@ class APIAddons(object):
"""Install addon.""" """Install addon."""
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request, check_installed=False) 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( return await asyncio.shield(
addon.install(version=version), loop=self.loop) addon.install(version=version), loop=self.loop)
@api_process @api_process
async def uninstall(self, request): def uninstall(self, request):
"""Uninstall addon.""" """Uninstall addon.
Return a coroutine.
"""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return await asyncio.shield(addon.uninstall(), loop=self.loop) return asyncio.shield(addon.uninstall(), loop=self.loop)
@api_process @api_process
async def start(self, request): def start(self, request):
"""Start addon.""" """Start addon.
Return a coroutine.
"""
addon = self._extract_addon(request) addon = self._extract_addon(request)
# check options # check options
@ -175,33 +192,45 @@ class APIAddons(object):
except vol.Invalid as ex: except vol.Invalid as ex:
raise RuntimeError(humanize_error(options, ex)) from None 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 @api_process
async def stop(self, request): def stop(self, request):
"""Stop addon.""" """Stop addon.
Return a coroutine.
"""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return await asyncio.shield(addon.stop(), loop=self.loop) return asyncio.shield(addon.stop(), loop=self.loop)
@api_process @api_process
async def update(self, request): async def update(self, request):
"""Update addon.""" """Update addon."""
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(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( return await asyncio.shield(
addon.update(version=version), loop=self.loop) addon.update(version=version), loop=self.loop)
@api_process @api_process
async def restart(self, request): def restart(self, request):
"""Restart addon.""" """Restart addon.
Return a coroutine.
"""
addon = self._extract_addon(request) 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) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request): def logs(self, request):
"""Return logs from addon.""" """Return logs from addon.
Return a coroutine.
"""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return addon.logs() return addon.logs()

View File

@ -63,22 +63,21 @@ class APIHomeAssistant(object):
async def update(self, request): async def update(self, request):
"""Update homeassistant.""" """Update homeassistant."""
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.config.last_homeassistant) version = body.get(ATTR_VERSION, self.homeassistant.last_version)
if self.homeassistant.in_progress: if version == self.homeassistant.version:
raise RuntimeError("Other task is in progress") raise RuntimeError("Version {} is already in use".format(version))
return await asyncio.shield( return await asyncio.shield(
self.homeassistant.update(version), loop=self.loop) self.homeassistant.update(version), loop=self.loop)
@api_process @api_process
async def restart(self, request): def restart(self, request):
"""Restart homeassistant.""" """Restart homeassistant.
if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress")
return await asyncio.shield( Return a coroutine.
self.homeassistant.restart(), loop=self.loop) """
return asyncio.shield(self.homeassistant.restart(), loop=self.loop)
@api_process_raw(CONTENT_TYPE_BINARY) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request): def logs(self, request):

View File

@ -7,7 +7,9 @@ import voluptuous as vol
from .util import api_process_hostcontrol, api_process, api_validate from .util import api_process_hostcontrol, api_process, api_validate
from ..const import ( from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES, 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_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT)
from ..validate import ALSA_CHANNEL
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -15,6 +17,11 @@ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str), vol.Optional(ATTR_VERSION): vol.Coerce(str),
}) })
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
})
class APIHost(object): class APIHost(object):
"""Handle rest api for host functions.""" """Handle rest api for host functions."""
@ -38,14 +45,32 @@ class APIHost(object):
ATTR_OS: self.host_control.os_info, 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_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
@api_process_hostcontrol @api_process_hostcontrol
def reboot(self, request): def reboot(self, request):
"""Reboot host.""" """Reboot host.
Return a coroutine.
"""
return self.host_control.reboot() return self.host_control.reboot()
@api_process_hostcontrol @api_process_hostcontrol
def shutdown(self, request): def shutdown(self, request):
"""Poweroff host.""" """Poweroff host.
Return a coroutine.
"""
return self.host_control.shutdown() return self.host_control.shutdown()
@api_process_hostcontrol @api_process_hostcontrol
@ -55,7 +80,7 @@ class APIHost(object):
version = body.get(ATTR_VERSION, self.host_control.last_version) version = body.get(ATTR_VERSION, self.host_control.last_version)
if version == self.host_control.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( return await asyncio.shield(
self.host_control.update(version=version), loop=self.loop) self.host_control.update(version=version), loop=self.loop)

View File

@ -98,5 +98,5 @@ class APISecurity(object):
session = hashlib.sha256(os.urandom(54)).hexdigest() session = hashlib.sha256(os.urandom(54)).hexdigest()
# store session # store session
self.config.security_sessions = (session, valid_until) self.config.add_security_session(session, valid_until)
return {ATTR_SESSION: session} return {ATTR_SESSION: session}

View File

@ -111,10 +111,13 @@ class APISnapshots(object):
self.snapshots.do_snapshot_partial(**body), loop=self.loop) self.snapshots.do_snapshot_partial(**body), loop=self.loop)
@api_process @api_process
async def restore_full(self, request): def restore_full(self, request):
"""Full-Restore a snapshot.""" """Full-Restore a snapshot.
Return a coroutine.
"""
snapshot = self._extract_snapshot(request) snapshot = self._extract_snapshot(request)
return await asyncio.shield( return asyncio.shield(
self.snapshots.do_restore_full(snapshot), loop=self.loop) self.snapshots.do_restore_full(snapshot), loop=self.loop)
@api_process @api_process
@ -125,7 +128,8 @@ class APISnapshots(object):
return await asyncio.shield( return await asyncio.shield(
self.snapshots.do_restore_partial(snapshot, **body), self.snapshots.do_restore_partial(snapshot, **body),
loop=self.loop) loop=self.loop
)
@api_process @api_process
async def remove(self, request): async def remove(self, request):

View File

@ -10,7 +10,7 @@ from ..const import (
HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_LOGO, ATTR_REPOSITORY, HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_LOGO, ATTR_REPOSITORY,
ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_TIMEZONE, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_TIMEZONE,
ATTR_STATE, CONTENT_TYPE_BINARY) ATTR_STATE, CONTENT_TYPE_BINARY)
from ..tools import validate_timezone from ..validate import validate_timezone
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,7 +30,7 @@ class APISupervisor(object):
"""Handle rest api for supervisor functions.""" """Handle rest api for supervisor functions."""
def __init__(self, config, loop, supervisor, snapshots, addons, def __init__(self, config, loop, supervisor, snapshots, addons,
host_control, websession): host_control, updater):
"""Initialize supervisor rest api part.""" """Initialize supervisor rest api part."""
self.config = config self.config = config
self.loop = loop self.loop = loop
@ -38,7 +38,7 @@ class APISupervisor(object):
self.addons = addons self.addons = addons
self.snapshots = snapshots self.snapshots = snapshots
self.host_control = host_control self.host_control = host_control
self.websession = websession self.updater = updater
@api_process @api_process
async def ping(self, request): async def ping(self, request):
@ -64,8 +64,8 @@ class APISupervisor(object):
return { return {
ATTR_VERSION: HASSIO_VERSION, ATTR_VERSION: HASSIO_VERSION,
ATTR_LAST_VERSION: self.config.last_hassio, ATTR_LAST_VERSION: self.updater.version_hassio,
ATTR_BETA_CHANNEL: self.config.upstream_beta, ATTR_BETA_CHANNEL: self.updater.beta_channel,
ATTR_ARCH: self.config.arch, ATTR_ARCH: self.config.arch,
ATTR_TIMEZONE: self.config.timezone, ATTR_TIMEZONE: self.config.timezone,
ATTR_ADDONS: list_addons, ATTR_ADDONS: list_addons,
@ -78,7 +78,7 @@ class APISupervisor(object):
body = await api_validate(SCHEMA_OPTIONS, request) body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_BETA_CHANNEL in body: 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: if ATTR_TIMEZONE in body:
self.config.timezone = body[ATTR_TIMEZONE] self.config.timezone = body[ATTR_TIMEZONE]
@ -93,10 +93,10 @@ class APISupervisor(object):
async def update(self, request): async def update(self, request):
"""Update supervisor OS.""" """Update supervisor OS."""
body = await api_validate(SCHEMA_VERSION, request) 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: 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( return await asyncio.shield(
self.supervisor.update(version), loop=self.loop) self.supervisor.update(version), loop=self.loop)
@ -107,7 +107,7 @@ class APISupervisor(object):
tasks = [ tasks = [
self.addons.reload(), self.addons.reload(),
self.snapshots.reload(), self.snapshots.reload(),
self.config.fetch_update_infos(self.websession), self.updater.fetch_data(),
self.host_control.load() self.host_control.load()
] ]
results, _ = await asyncio.shield( results, _ = await asyncio.shield(

View File

@ -4,121 +4,60 @@ import logging
import os import os
from pathlib import Path, PurePath from pathlib import Path, PurePath
import voluptuous as vol from .const import (
FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_SECURITY, ATTR_SESSIONS,
from .const import FILE_HASSIO_CONFIG, HASSIO_DATA ATTR_PASSWORD, ATTR_TOTP, ATTR_TIMEZONE, ATTR_API_ENDPOINT,
from .tools import fetch_last_versions, JsonConfig, validate_timezone ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT)
from .tools import JsonConfig
from .validate import SCHEMA_HASSIO_CONFIG
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATETIME_FORMAT = "%Y%m%d %H:%M:%S" DATETIME_FORMAT = "%Y%m%d %H:%M:%S"
HOMEASSISTANT_CONFIG = PurePath("homeassistant") HOMEASSISTANT_CONFIG = PurePath("homeassistant")
HOMEASSISTANT_LAST = 'homeassistant_last'
HASSIO_SSL = PurePath("ssl") HASSIO_SSL = PurePath("ssl")
HASSIO_LAST = 'hassio_last'
ADDONS_CORE = PurePath("addons/core") ADDONS_CORE = PurePath("addons/core")
ADDONS_LOCAL = PurePath("addons/local") ADDONS_LOCAL = PurePath("addons/local")
ADDONS_GIT = PurePath("addons/git") ADDONS_GIT = PurePath("addons/git")
ADDONS_DATA = PurePath("addons/data") ADDONS_DATA = PurePath("addons/data")
ADDONS_CUSTOM_LIST = 'addons_custom_list'
BACKUP_DATA = PurePath("backup") BACKUP_DATA = PurePath("backup")
SHARE_DATA = PurePath("share") SHARE_DATA = PurePath("share")
TMP_DATA = PurePath("tmp") 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): class CoreConfig(JsonConfig):
"""Hold all core config data.""" """Hold all core config data."""
def __init__(self): def __init__(self):
"""Initialize config object.""" """Initialize config object."""
super().__init__(FILE_HASSIO_CONFIG, SCHEMA_CONFIG) super().__init__(FILE_HASSIO_CONFIG, SCHEMA_HASSIO_CONFIG)
self.arch = None 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 @property
def api_endpoint(self): def api_endpoint(self):
"""Return IP address of api endpoint.""" """Return IP address of api endpoint."""
return self._data[API_ENDPOINT] return self._data[ATTR_API_ENDPOINT]
@api_endpoint.setter @api_endpoint.setter
def api_endpoint(self, value): def api_endpoint(self, value):
"""Store IP address of api endpoint.""" """Store IP address of api endpoint."""
self._data[API_ENDPOINT] = value self._data[ATTR_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()
@property @property
def timezone(self): def timezone(self):
"""Return system timezone.""" """Return system timezone."""
return self._data[TIMEZONE] return self._data[ATTR_TIMEZONE]
@timezone.setter @timezone.setter
def timezone(self, value): def timezone(self, value):
"""Set system timezone.""" """Set system timezone."""
self._data[TIMEZONE] = value self._data[ATTR_TIMEZONE] = value
self.save() 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 @property
def path_hassio(self): def path_hassio(self):
"""Return hassio data path.""" """Return hassio data path."""
@ -207,73 +146,95 @@ class CoreConfig(JsonConfig):
@property @property
def addons_repositories(self): def addons_repositories(self):
"""Return list of addons custom repositories.""" """Return list of addons custom repositories."""
return self._data[ADDONS_CUSTOM_LIST] return self._data[ATTR_ADDONS_CUSTOM_LIST]
@addons_repositories.setter def add_addon_repository(self, repo):
def addons_repositories(self, repo):
"""Add a custom repository to list.""" """Add a custom repository to list."""
if repo in self._data[ADDONS_CUSTOM_LIST]: if repo in self._data[ATTR_ADDONS_CUSTOM_LIST]:
return return
self._data[ADDONS_CUSTOM_LIST].append(repo) self._data[ATTR_ADDONS_CUSTOM_LIST].append(repo)
self.save() self.save()
def drop_addon_repository(self, repo): def drop_addon_repository(self, repo):
"""Remove a custom repository from list.""" """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 return
self._data[ADDONS_CUSTOM_LIST].remove(repo) self._data[ATTR_ADDONS_CUSTOM_LIST].remove(repo)
self.save() self.save()
@property @property
def security_initialize(self): def security_initialize(self):
"""Return is security was initialize.""" """Return is security was initialize."""
return self._data[SECURITY_INITIALIZE] return self._data[ATTR_SECURITY]
@security_initialize.setter @security_initialize.setter
def security_initialize(self, value): def security_initialize(self, value):
"""Set is security initialize.""" """Set is security initialize."""
self._data[SECURITY_INITIALIZE] = value self._data[ATTR_SECURITY] = value
self.save() self.save()
@property @property
def security_totp(self): def security_totp(self):
"""Return the TOTP key.""" """Return the TOTP key."""
return self._data.get(SECURITY_TOTP) return self._data.get(ATTR_TOTP)
@security_totp.setter @security_totp.setter
def security_totp(self, value): def security_totp(self, value):
"""Set the TOTP key.""" """Set the TOTP key."""
self._data[SECURITY_TOTP] = value self._data[ATTR_TOTP] = value
self.save() self.save()
@property @property
def security_password(self): def security_password(self):
"""Return the password key.""" """Return the password key."""
return self._data.get(SECURITY_PASSWORD) return self._data.get(ATTR_PASSWORD)
@security_password.setter @security_password.setter
def security_password(self, value): def security_password(self, value):
"""Set the password key.""" """Set the password key."""
self._data[SECURITY_PASSWORD] = value self._data[ATTR_PASSWORD] = value
self.save() self.save()
@property @property
def security_sessions(self): def security_sessions(self):
"""Return api sessions.""" """Return api sessions."""
return {session: datetime.strptime(until, DATETIME_FORMAT) for return {
session, until in self._data[SECURITY_SESSIONS].items()} session: datetime.strptime(until, DATETIME_FORMAT) for
session, until in self._data[ATTR_SESSIONS].items()
}
@security_sessions.setter def add_security_session(self, session, valid):
def security_sessions(self, value):
"""Set the a new session.""" """Set the a new session."""
session, valid = value self._data[ATTR_SESSIONS].update(
if valid is None:
self._data[SECURITY_SESSIONS].pop(session, None)
else:
self._data[SECURITY_SESSIONS].update(
{session: valid.strftime(DATETIME_FORMAT)} {session: valid.strftime(DATETIME_FORMAT)}
) )
self.save()
def drop_security_session(self, session):
"""Delete the a session."""
self._data[ATTR_SESSIONS].pop(session, None)
self.save()
@property
def audio_output(self):
"""Return ALSA audio output card,dev."""
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_OUTPUT] = value
self.save()
@property
def audio_input(self):
"""Return ALSA audio input card,dev."""
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_INPUT] = value
self.save() self.save()

View File

@ -1,12 +1,10 @@
"""Const file for HassIO.""" """Const file for HassIO."""
from pathlib import Path from pathlib import Path
HASSIO_VERSION = '0.50' HASSIO_VERSION = '0.51'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/master/version.json') 'hassio/{}/version.json')
URL_HASSIO_VERSION_BETA = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/dev/version.json')
URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons' 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_ADDONS = Path(HASSIO_DATA, "addons.json")
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json") FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.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_DOCKER = Path("/var/run/docker.sock")
SOCKET_HC = Path("/var/run/hassio-hc.sock") SOCKET_HC = Path("/var/run/hassio-hc.sock")
@ -83,6 +82,7 @@ ATTR_PASSWORD = 'password'
ATTR_TOTP = 'totp' ATTR_TOTP = 'totp'
ATTR_INITIALIZE = 'initialize' ATTR_INITIALIZE = 'initialize'
ATTR_SESSION = 'session' ATTR_SESSION = 'session'
ATTR_SESSIONS = 'sessions'
ATTR_LOCATON = 'location' ATTR_LOCATON = 'location'
ATTR_BUILD = 'build' ATTR_BUILD = 'build'
ATTR_DEVICES = 'devices' ATTR_DEVICES = 'devices'
@ -95,6 +95,8 @@ ATTR_USER = 'user'
ATTR_SYSTEM = 'system' ATTR_SYSTEM = 'system'
ATTR_SNAPSHOTS = 'snapshots' ATTR_SNAPSHOTS = 'snapshots'
ATTR_HOMEASSISTANT = 'homeassistant' ATTR_HOMEASSISTANT = 'homeassistant'
ATTR_HASSIO = 'hassio'
ATTR_HASSIO_API = 'hassio_api'
ATTR_FOLDERS = 'folders' ATTR_FOLDERS = 'folders'
ATTR_SIZE = 'size' ATTR_SIZE = 'size'
ATTR_TYPE = 'type' ATTR_TYPE = 'type'
@ -102,9 +104,15 @@ ATTR_TIMEOUT = 'timeout'
ATTR_AUTO_UPDATE = 'auto_update' ATTR_AUTO_UPDATE = 'auto_update'
ATTR_CUSTOM = 'custom' ATTR_CUSTOM = 'custom'
ATTR_AUDIO = 'audio' ATTR_AUDIO = 'audio'
ATTR_AUDIO_INPUT = 'audio_input'
ATTR_AUDIO_OUTPUT = 'audio_output'
ATTR_INPUT = 'input' ATTR_INPUT = 'input'
ATTR_OUTPUT = 'output'
ATTR_DISK = 'disk' ATTR_DISK = 'disk'
ATTR_SERIAL = 'serial' ATTR_SERIAL = 'serial'
ATTR_SECURITY = 'security'
ATTR_API_ENDPOINT = 'api_endpoint'
ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list'
STARTUP_INITIALIZE = 'initialize' STARTUP_INITIALIZE = 'initialize'
STARTUP_SYSTEM = 'system' STARTUP_SYSTEM = 'system'

View File

@ -19,6 +19,7 @@ from .homeassistant import HomeAssistant
from .scheduler import Scheduler from .scheduler import Scheduler
from .dock.supervisor import DockerSupervisor from .dock.supervisor import DockerSupervisor
from .snapshots import SnapshotsManager from .snapshots import SnapshotsManager
from .updater import Updater
from .tasks import ( from .tasks import (
hassio_update, homeassistant_watchdog, api_sessions_cleanup, addons_update) hassio_update, homeassistant_watchdog, api_sessions_cleanup, addons_update)
from .tools import get_local_ip, fetch_timezone from .tools import get_local_ip, fetch_timezone
@ -35,6 +36,7 @@ class HassIO(object):
self.loop = loop self.loop = loop
self.config = config self.config = config
self.websession = aiohttp.ClientSession(loop=loop) self.websession = aiohttp.ClientSession(loop=loop)
self.updater = Updater(config, loop, self.websession)
self.scheduler = Scheduler(loop) self.scheduler = Scheduler(loop)
self.api = RestAPI(config, loop) self.api = RestAPI(config, loop)
self.hardware = Hardware() self.hardware = Hardware()
@ -46,7 +48,7 @@ class HassIO(object):
# init homeassistant # init homeassistant
self.homeassistant = HomeAssistant( self.homeassistant = HomeAssistant(
config, loop, self.dock, self.websession) config, loop, self.dock, self.updater)
# init HostControl # init HostControl
self.host_control = HostControl(loop) self.host_control = HostControl(loop)
@ -87,7 +89,7 @@ class HassIO(object):
self.api.register_network(self.host_control) self.api.register_network(self.host_control)
self.api.register_supervisor( self.api.register_supervisor(
self.supervisor, self.snapshots, self.addons, self.host_control, self.supervisor, self.snapshots, self.addons, self.host_control,
self.websession) self.updater)
self.api.register_homeassistant(self.homeassistant) self.api.register_homeassistant(self.homeassistant)
self.api.register_addons(self.addons) self.api.register_addons(self.addons)
self.api.register_security() self.api.register_security()
@ -113,7 +115,7 @@ class HassIO(object):
# schedule self update task # schedule self update task
self.scheduler.register_task( self.scheduler.register_task(
hassio_update(self.config, self.supervisor, self.websession), hassio_update(self.supervisor, self.updater),
RUN_UPDATE_SUPERVISOR_TASKS) RUN_UPDATE_SUPERVISOR_TASKS)
# schedule snapshot update tasks # schedule snapshot update tasks
@ -128,7 +130,7 @@ class HassIO(object):
# on release channel, try update itself # on release channel, try update itself
# on beta channel, only read new versions # on beta channel, only read new versions
await asyncio.wait( await asyncio.wait(
[hassio_update(self.config, self.supervisor, self.websession)()], [hassio_update(self.supervisor, self.updater)()],
loop=self.loop loop=self.loop
) )

View File

@ -32,6 +32,11 @@ class DockerAddon(DockerBase):
def environment(self): def environment(self):
"""Return environment for docker add-on.""" """Return environment for docker add-on."""
addon_env = self.addon.environment or {} 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 { return {
**addon_env, **addon_env,
@ -46,6 +51,16 @@ class DockerAddon(DockerBase):
return {"/tmpfs": "{}".format(options)} return {"/tmpfs": "{}".format(options)}
return None 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 @property
def volumes(self): def volumes(self):
"""Generate volumes for mappings.""" """Generate volumes for mappings."""
@ -107,10 +122,11 @@ class DockerAddon(DockerBase):
self.dock.containers.run( self.dock.containers.run(
self.image, self.image,
name=self.name, name=self.name,
hostname=self.name, hostname=self.addon.slug,
detach=True, detach=True,
network_mode=self.addon.network_mode, network_mode=self.addon.network_mode,
ports=self.addon.ports, ports=self.addon.ports,
extra_hosts=self.mapping,
devices=self.addon.devices, devices=self.addon.devices,
cap_add=self.addon.privileged, cap_add=self.addon.privileged,
environment=self.environment, environment=self.environment,

View File

@ -16,12 +16,12 @@ _LOGGER = logging.getLogger(__name__)
class HomeAssistant(JsonConfig): class HomeAssistant(JsonConfig):
"""Hass core object for handle it.""" """Hass core object for handle it."""
def __init__(self, config, loop, dock, websession): def __init__(self, config, loop, dock, updater):
"""Initialize hass object.""" """Initialize hass object."""
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG) super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
self.config = config self.config = config
self.loop = loop self.loop = loop
self.websession = websession self.updater = updater
self.docker = DockerHomeAssistant(config, loop, dock, self) self.docker = DockerHomeAssistant(config, loop, dock, self)
async def prepare(self): async def prepare(self):
@ -45,7 +45,7 @@ class HomeAssistant(JsonConfig):
"""Return last available version of homeassistant.""" """Return last available version of homeassistant."""
if self.is_custom_image: if self.is_custom_image:
return self._data.get(ATTR_LAST_VERSION) return self._data.get(ATTR_LAST_VERSION)
return self.config.last_homeassistant return self.updater.version_homeassistant
@property @property
def image(self): def image(self):
@ -101,7 +101,7 @@ class HomeAssistant(JsonConfig):
while True: while True:
# read homeassistant tag and install it # read homeassistant tag and install it
if not self.last_version: if not self.last_version:
await self.config.fetch_update_infos(self.websession) await self.updater.fetch_data()
tag = self.last_version tag = self.last_version
if tag and await self.docker.install(tag): if tag and await self.docker.install(tag):
@ -113,13 +113,15 @@ class HomeAssistant(JsonConfig):
_LOGGER.info("HomeAssistant docker now installed") _LOGGER.info("HomeAssistant docker now installed")
await self.docker.cleanup() await self.docker.cleanup()
def update(self, version=None): async def update(self, version=None):
"""Update HomeAssistant version. """Update HomeAssistant version."""
Return a coroutine.
"""
version = version or self.last_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): def run(self):
"""Run HomeAssistant docker. """Run HomeAssistant docker.

View File

@ -13,7 +13,7 @@ def api_sessions_cleanup(config):
now = datetime.now() now = datetime.now()
for session, until_valid in config.security_sessions.items(): for session, until_valid in config.security_sessions.items():
if now >= until_valid: if now >= until_valid:
config.security_sessions = (session, None) config.drop_security_session(session)
return _api_sessions_cleanup return _api_sessions_cleanup
@ -43,21 +43,21 @@ def addons_update(loop, addons):
return _addons_update return _addons_update
def hassio_update(config, supervisor, websession): def hassio_update(supervisor, updater):
"""Create scheduler task for update of supervisor hassio.""" """Create scheduler task for update of supervisor hassio."""
async def _hassio_update(): async def _hassio_update():
"""Check and run update of supervisor hassio.""" """Check and run update of supervisor hassio."""
await config.fetch_update_infos(websession) await updater.fetch_data()
if config.last_hassio == supervisor.version: if updater.version_hassio == supervisor.version:
return return
# don't perform a update on beta/dev channel # 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!") _LOGGER.warning("Ignore Hass.IO update on beta upstream!")
return return
_LOGGER.info("Found new HassIO version %s.", config.last_hassio) _LOGGER.info("Found new HassIO version %s.", updater.version_hassio)
await supervisor.update(config.last_hassio) await supervisor.update(updater.version_hassio)
return _hassio_update return _hassio_update

View File

@ -1,41 +1,21 @@
"""Tools file for HassIO.""" """Tools file for HassIO."""
import asyncio import asyncio
from contextlib import suppress from contextlib import suppress
from datetime import datetime
import json import json
import logging import logging
import socket import socket
import aiohttp import aiohttp
import async_timeout import async_timeout
import pytz
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
FREEGEOIP_URL = "https://freegeoip.io/json/" 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): def get_local_ip(loop):
"""Retrieve local IP address. """Retrieve local IP address.
@ -76,19 +56,6 @@ def read_json_file(jsonfile):
return json.loads(cfile.read()) 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): async def fetch_timezone(websession):
"""Read timezone from freegeoip.""" """Read timezone from freegeoip."""
data = {} data = {}
@ -140,3 +107,27 @@ class JsonConfig(object):
_LOGGER.error("Can't store config in %s", self._file) _LOGGER.error("Can't store config in %s", self._file)
return False return False
return True 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

86
hassio/updater.py Normal file
View File

@ -0,0 +1,86 @@
"""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:
_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)
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()

View File

@ -1,11 +1,31 @@
"""Validate functions.""" """Validate functions."""
import voluptuous as vol 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_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)) NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
HASS_DEVICES = [vol.Match(r"^[^/]*$")] 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): def convert_to_docker_ports(data):
@ -40,3 +60,26 @@ SCHEMA_HASS_CONFIG = vol.Schema({
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str), vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str),
vol.Inclusive(ATTR_LAST_VERSION, '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=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)

View File

@ -1,5 +1,5 @@
{ {
"hassio": "0.50", "hassio": "0.51",
"homeassistant": "0.50.2", "homeassistant": "0.50.2",
"resinos": "1.0", "resinos": "1.0",
"resinhup": "0.3", "resinhup": "0.3",