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)