From 67dcf1563bcba88f8cdaa3ec2160ca1be240a615 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 18 Sep 2018 21:20:10 +0200 Subject: [PATCH 01/13] Bump version to 132 --- hassio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/const.py b/hassio/const.py index abc3a2aac..b3ef7a5da 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -2,7 +2,7 @@ from pathlib import Path from ipaddress import ip_network -HASSIO_VERSION = '131' +HASSIO_VERSION = '132' URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_VERSION = \ From 267791833e95f84ec1b52cd10f6f50dbfa39c027 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 18 Sep 2018 23:47:47 +0200 Subject: [PATCH 02/13] Update docstrings, comments and log messages (#707) --- hassio/addons/__init__.py | 34 +++---- hassio/addons/addon.py | 166 ++++++++++++++++----------------- hassio/addons/build.py | 14 +-- hassio/addons/data.py | 16 ++-- hassio/addons/git.py | 32 +++---- hassio/addons/repository.py | 10 +- hassio/addons/utils.py | 4 +- hassio/addons/validate.py | 4 +- hassio/api/__init__.py | 30 +++--- hassio/api/addons.py | 40 ++++---- hassio/api/discovery.py | 5 +- hassio/api/hardware.py | 4 +- hassio/api/hassos.py | 6 +- hassio/api/homeassistant.py | 18 ++-- hassio/api/host.py | 4 +- hassio/api/proxy.py | 38 ++++---- hassio/api/security.py | 2 +- hassio/api/services.py | 4 +- hassio/api/snapshots.py | 4 +- hassio/api/supervisor.py | 14 +-- hassio/api/utils.py | 4 +- hassio/dbus/systemd.py | 2 +- hassio/docker/__init__.py | 18 ++-- hassio/docker/addon.py | 50 +++++----- hassio/docker/hassos_cli.py | 10 +- hassio/docker/homeassistant.py | 16 ++-- hassio/docker/interface.py | 62 ++++++------ hassio/docker/network.py | 12 +-- hassio/docker/stats.py | 4 +- hassio/docker/supervisor.py | 12 +-- hassio/host/__init__.py | 4 +- hassio/host/alsa.py | 4 +- hassio/host/apparmor.py | 8 +- hassio/host/control.py | 4 +- hassio/host/info.py | 2 +- hassio/host/services.py | 2 +- hassio/misc/hardware.py | 2 +- hassio/misc/scheduler.py | 14 +-- hassio/services/__init__.py | 1 - hassio/services/data.py | 4 +- hassio/services/discovery.py | 14 +-- hassio/services/mqtt.py | 14 +-- hassio/services/validate.py | 1 - hassio/snapshots/snapshot.py | 26 +++--- hassio/snapshots/utils.py | 2 +- hassio/snapshots/validate.py | 1 - 46 files changed, 370 insertions(+), 372 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index d22884a2a..50c60b443 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -1,4 +1,4 @@ -"""Init file for HassIO addons.""" +"""Init file for Hass.io add-ons.""" import asyncio import logging @@ -14,10 +14,10 @@ BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL)) class AddonManager(CoreSysAttributes): - """Manage addons inside HassIO.""" + """Manage add-ons inside Hass.io.""" def __init__(self, coresys): - """Initialize docker base wrapper.""" + """Initialize Docker base wrapper.""" self.coresys = coresys self.data = AddonsData(coresys) self.addons_obj = {} @@ -25,18 +25,18 @@ class AddonManager(CoreSysAttributes): @property def list_addons(self): - """Return a list of all addons.""" + """Return a list of all add-ons.""" return list(self.addons_obj.values()) @property def list_installed(self): - """Return a list of installed addons.""" + """Return a list of installed add-ons.""" return [addon for addon in self.addons_obj.values() if addon.is_installed] @property def list_repositories(self): - """Return list of addon repositories.""" + """Return list of add-on repositories.""" return list(self.repositories_obj.values()) def get(self, addon_slug): @@ -44,32 +44,32 @@ class AddonManager(CoreSysAttributes): return self.addons_obj.get(addon_slug) def from_uuid(self, uuid): - """Return an add-on from uuid.""" + """Return an add-on from UUID.""" for addon in self.list_addons: if addon.is_installed and uuid == addon.uuid: return addon return None def from_token(self, token): - """Return an add-on from hassio token.""" + """Return an add-on from Hass.io token.""" for addon in self.list_addons: if addon.is_installed and token == addon.hassio_token: return addon return None async def load(self): - """Startup addon management.""" + """Start up add-on management.""" self.data.reload() - # init hassio built-in repositories + # Init Hass.io built-in repositories repositories = \ set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES - # init custom repositories & load addons + # Init custom repositories and load add-ons await self.load_repositories(repositories) async def reload(self): - """Update addons from repo and reload list.""" + """Update add-ons from repository and reload list.""" tasks = [repository.update() for repository in self.repositories_obj.values()] if tasks: @@ -113,14 +113,14 @@ class AddonManager(CoreSysAttributes): await self.load_addons() async def load_addons(self): - """Update/add internal addon store.""" + """Update/add internal add-on store.""" all_addons = set(self.data.system) | set(self.data.cache) # calc diff add_addons = all_addons - set(self.addons_obj) del_addons = set(self.addons_obj) - all_addons - _LOGGER.info("Load addons: %d all - %d new - %d remove", + _LOGGER.info("Load add-ons: %d all - %d new - %d remove", len(all_addons), len(add_addons), len(del_addons)) # new addons @@ -139,14 +139,14 @@ class AddonManager(CoreSysAttributes): self.addons_obj.pop(addon_slug) async def boot(self, stage): - """Boot addons with mode auto.""" + """Boot add-ons with mode auto.""" tasks = [] for addon in self.addons_obj.values(): if addon.is_installed and addon.boot == BOOT_AUTO and \ addon.startup == stage: tasks.append(addon.start()) - _LOGGER.info("Startup %s run %d addons", stage, len(tasks)) + _LOGGER.info("Startup %s run %d add-ons", stage, len(tasks)) if tasks: await asyncio.wait(tasks) await asyncio.sleep(self.sys_config.wait_boot) @@ -160,6 +160,6 @@ class AddonManager(CoreSysAttributes): addon.startup == stage: tasks.append(addon.stop()) - _LOGGER.info("Shutdown %s stop %d addons", stage, len(tasks)) + _LOGGER.info("Shutdown %s stop %d add-ons", stage, len(tasks)) if tasks: await asyncio.wait(tasks) diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index b58ea9404..22299143a 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -1,4 +1,4 @@ -"""Init file for HassIO addons.""" +"""Init file for Hass.io add-ons.""" from contextlib import suppress from copy import deepcopy import logging @@ -43,7 +43,7 @@ RE_WEBUI = re.compile( class Addon(CoreSysAttributes): - """Hold data for addon inside HassIO.""" + """Hold data for add-on inside Hass.io.""" def __init__(self, coresys, slug): """Initialize data holder.""" @@ -59,27 +59,27 @@ class Addon(CoreSysAttributes): @property def slug(self): - """Return slug/id of addon.""" + """Return slug/id of add-on.""" return self._id @property def _mesh(self): - """Return addon data from system or cache.""" + """Return add-on data from system or cache.""" return self._data.system.get(self._id, self._data.cache.get(self._id)) @property def _data(self): - """Return addons data storage.""" + """Return add-ons data storage.""" return self.sys_addons.data @property def is_installed(self): - """Return True if an addon is installed.""" + """Return True if an add-on is installed.""" return self._id in self._data.system @property def is_detached(self): - """Return True if addon is detached.""" + """Return True if add-on is detached.""" return self._id not in self._data.cache @property @@ -97,19 +97,19 @@ class Addon(CoreSysAttributes): self._data.save_data() def _set_uninstall(self): - """Set addon as uninstalled.""" + """Set add-on as uninstalled.""" self._data.system.pop(self._id, None) self._data.user.pop(self._id, None) self._data.save_data() def _set_update(self, version): - """Update version of addon.""" + """Update version of add-on.""" self._data.system[self._id] = deepcopy(self._data.cache[self._id]) self._data.user[self._id][ATTR_VERSION] = version self._data.save_data() def _restore_data(self, user, system): - """Restore data to addon.""" + """Restore data to add-on.""" self._data.user[self._id] = deepcopy(user) self._data.system[self._id] = deepcopy(system) self._data.save_data() @@ -126,7 +126,7 @@ class Addon(CoreSysAttributes): @options.setter def options(self, value): - """Store user addon options.""" + """Store user add-on options.""" if value is None: self._data.user[self._id][ATTR_OPTIONS] = {} else: @@ -158,7 +158,7 @@ class Addon(CoreSysAttributes): @property def name(self): - """Return name of addon.""" + """Return name of add-on.""" return self._mesh[ATTR_NAME] @property @@ -175,14 +175,14 @@ class Addon(CoreSysAttributes): @property def hassio_token(self): - """Return access token for hass.io API.""" + """Return access token for Hass.io API.""" if self.is_installed: return self._data.user[self._id].get(ATTR_ACCESS_TOKEN) return None @property def description(self): - """Return description of addon.""" + """Return description of add-on.""" return self._mesh[ATTR_DESCRIPTON] @property @@ -200,31 +200,31 @@ class Addon(CoreSysAttributes): @property def repository(self): - """Return repository of addon.""" + """Return repository of add-on.""" return self._mesh[ATTR_REPOSITORY] @property def last_version(self): - """Return version of addon.""" + """Return version of add-on.""" if self._id in self._data.cache: return self._data.cache[self._id][ATTR_VERSION] return self.version_installed @property def protected(self): - """Return if addon is in protected mode.""" + """Return if add-on is in protected mode.""" if self.is_installed: return self._data.user[self._id][ATTR_PROTECTED] return True @protected.setter def protected(self, value): - """Set addon in protected mode.""" + """Set add-on in protected mode.""" self._data.user[self._id][ATTR_PROTECTED] = value @property def startup(self): - """Return startup type of addon.""" + """Return startup type of add-on.""" return self._mesh.get(ATTR_STARTUP) @property @@ -249,7 +249,7 @@ class Addon(CoreSysAttributes): @property def ports(self): - """Return ports of addon.""" + """Return ports of add-on.""" if self.host_network or ATTR_PORTS not in self._mesh: return None @@ -260,7 +260,7 @@ class Addon(CoreSysAttributes): @ports.setter def ports(self, value): - """Set custom ports of addon.""" + """Set custom ports of add-on.""" if value is None: self._data.user[self._id].pop(ATTR_NETWORK, None) else: @@ -304,42 +304,42 @@ class Addon(CoreSysAttributes): @property def host_network(self): - """Return True if addon run on host network.""" + """Return True if add-on run on host network.""" return self._mesh[ATTR_HOST_NETWORK] @property def host_pid(self): - """Return True if addon run on host PID namespace.""" + """Return True if add-on run on host PID namespace.""" return self._mesh[ATTR_HOST_PID] @property def host_ipc(self): - """Return True if addon run on host IPC namespace.""" + """Return True if add-on run on host IPC namespace.""" return self._mesh[ATTR_HOST_IPC] @property def host_dbus(self): - """Return True if addon run on host DBUS.""" + """Return True if add-on run on host D-BUS.""" return self._mesh[ATTR_HOST_DBUS] @property def devices(self): - """Return devices of addon.""" + """Return devices of add-on.""" return self._mesh.get(ATTR_DEVICES) @property def auto_uart(self): - """Return True if we should map all uart device.""" + """Return True if we should map all UART device.""" return self._mesh.get(ATTR_AUTO_UART) @property def tmpfs(self): - """Return tmpfs of addon.""" + """Return tmpfs of add-on.""" return self._mesh.get(ATTR_TMPFS) @property def environment(self): - """Return environment of addon.""" + """Return environment of add-on.""" return self._mesh.get(ATTR_ENVIRONMENT) @property @@ -349,7 +349,7 @@ class Addon(CoreSysAttributes): @property def apparmor(self): - """Return True if apparmor is enabled.""" + """Return True if AppArmor is enabled.""" if not self._mesh.get(ATTR_APPARMOR): return SECURITY_DISABLE elif self.sys_host.apparmor.exists(self.slug): @@ -358,22 +358,22 @@ class Addon(CoreSysAttributes): @property def legacy(self): - """Return if the add-on don't support hass labels.""" + """Return if the add-on don't support Home Assistant labels.""" return self._mesh.get(ATTR_LEGACY) @property def access_docker_api(self): - """Return if the add-on need read-only docker API access.""" + """Return if the add-on need read-only Docker API access.""" return self._mesh.get(ATTR_DOCKER_API) @property def access_hassio_api(self): - """Return True if the add-on access to hassio api.""" + """Return True if the add-on access to Hass.io REASTful API.""" return self._mesh[ATTR_HASSIO_API] @property def access_homeassistant_api(self): - """Return True if the add-on access to Home-Assistant api proxy.""" + """Return True if the add-on access to Home Assistant API proxy.""" return self._mesh[ATTR_HOMEASSISTANT_API] @property @@ -388,7 +388,7 @@ class Addon(CoreSysAttributes): @property def with_gpio(self): - """Return True if the add-on access to gpio interface.""" + """Return True if the add-on access to GPIO interface.""" return self._mesh[ATTR_GPIO] @property @@ -445,7 +445,7 @@ class Addon(CoreSysAttributes): @property def url(self): - """Return url of addon.""" + """Return URL of add-on.""" return self._mesh.get(ATTR_URL) @property @@ -470,10 +470,10 @@ class Addon(CoreSysAttributes): @property def image(self): - """Return image name of addon.""" + """Return image name of add-on.""" addon_data = self._mesh - # Repository with dockerhub images + # Repository with Dockerhub images if ATTR_IMAGE in addon_data: return addon_data[ATTR_IMAGE].format(arch=self.sys_arch) @@ -484,12 +484,12 @@ class Addon(CoreSysAttributes): @property def need_build(self): - """Return True if this addon need a local build.""" + """Return True if this add-on need a local build.""" return ATTR_IMAGE not in self._mesh @property def map_volumes(self): - """Return a dict of {volume: policy} from addon.""" + """Return a dict of {volume: policy} from add-on.""" volumes = {} for volume in self._mesh[ATTR_MAP]: result = RE_VOLUME.match(volume) @@ -499,37 +499,37 @@ class Addon(CoreSysAttributes): @property def path_data(self): - """Return addon data path inside supervisor.""" + """Return add-on data path inside Supervisor.""" return Path(self.sys_config.path_addons_data, self._id) @property def path_extern_data(self): - """Return addon data path external for docker.""" + """Return add-on data path external for Docker.""" return PurePath(self.sys_config.path_extern_addons_data, self._id) @property def path_options(self): - """Return path to addons options.""" + """Return path to add-on options.""" return Path(self.path_data, "options.json") @property def path_location(self): - """Return path to this addon.""" + """Return path to this add-on.""" return Path(self._mesh[ATTR_LOCATON]) @property def path_icon(self): - """Return path to addon icon.""" + """Return path to add-on icon.""" return Path(self.path_location, 'icon.png') @property def path_logo(self): - """Return path to addon logo.""" + """Return path to add-on logo.""" return Path(self.path_location, 'logo.png') @property def path_changelog(self): - """Return path to addon changelog.""" + """Return path to add-on changelog.""" return Path(self.path_location, 'CHANGELOG.md') @property @@ -544,15 +544,15 @@ class Addon(CoreSysAttributes): @property def path_extern_asound(self): - """Return path to asound config for docker.""" + """Return path to asound config for Docker.""" return Path(self.sys_config.path_extern_tmp, f"{self.slug}_asound") def save_data(self): - """Save data of addon.""" + """Save data of add-on.""" self.sys_addons.data.save_data() def write_options(self): - """Return True if addon options is written to data.""" + """Return True if add-on options is written to data.""" schema = self.schema options = self.options @@ -560,10 +560,10 @@ class Addon(CoreSysAttributes): schema(options) write_json_file(self.path_options, options) except vol.Invalid as ex: - _LOGGER.error("Addon %s have wrong options: %s", self._id, + _LOGGER.error("Add-on %s have wrong options: %s", self._id, humanize_error(options, ex)) except (OSError, json.JSONDecodeError) as err: - _LOGGER.error("Addon %s can't write options: %s", self._id, err) + _LOGGER.error("Add-on %s can't write options: %s", self._id, err) else: return True @@ -578,7 +578,7 @@ class Addon(CoreSysAttributes): with self.path_asound.open('w') as config_file: config_file.write(asound_config) except OSError as err: - _LOGGER.error("Addon %s can't write asound: %s", self._id, err) + _LOGGER.error("Add-on %s can't write asound: %s", self._id, err) return False return True @@ -606,7 +606,7 @@ class Addon(CoreSysAttributes): @property def schema(self): - """Create a schema for addon options.""" + """Create a schema for add-on options.""" raw_schema = self._mesh[ATTR_SCHEMA] if isinstance(raw_schema, bool): @@ -614,7 +614,7 @@ class Addon(CoreSysAttributes): return vol.Schema(vol.All(dict, validate_options(raw_schema))) def test_update_schema(self): - """Check if the exists config valid after update.""" + """Check if the existing configuration is valid after update.""" if not self.is_installed or self.is_detached: return True @@ -644,19 +644,19 @@ class Addon(CoreSysAttributes): return True async def install(self): - """Install an addon.""" + """Install an add-on.""" if self.sys_arch not in self.supported_arch: _LOGGER.error( - "Addon %s not supported on %s", self._id, self.sys_arch) + "Add-on %s not supported on %s", self._id, self.sys_arch) return False if self.is_installed: - _LOGGER.error("Addon %s is already installed", self._id) + _LOGGER.error("Add-on %s is already installed", self._id) return False if not self.path_data.is_dir(): _LOGGER.info( - "Create Home-Assistant addon data folder %s", self.path_data) + "Create Home Assistant add-on data folder %s", self.path_data) self.path_data.mkdir() # Setup/Fix AppArmor profile @@ -670,13 +670,13 @@ class Addon(CoreSysAttributes): @check_installed async def uninstall(self): - """Remove an addon.""" + """Remove an add-on.""" if not await self.instance.remove(): return False if self.path_data.is_dir(): _LOGGER.info( - "Remove Home-Assistant addon data folder %s", self.path_data) + "Remove Home Assistant add-on data folder %s", self.path_data) await remove_data(self.path_data) # Cleanup audio settings @@ -684,7 +684,7 @@ class Addon(CoreSysAttributes): with suppress(OSError): self.path_asound.unlink() - # Cleanup apparmor profile + # Cleanup AppArmor profile if self.sys_host.apparmor.exists(self.slug): with suppress(HostAppArmorError): await self.sys_host.apparmor.remove_profile(self.slug) @@ -693,7 +693,7 @@ class Addon(CoreSysAttributes): return True async def state(self): - """Return running state of addon.""" + """Return running state of add-on.""" if not self.is_installed: return STATE_NONE @@ -703,9 +703,9 @@ class Addon(CoreSysAttributes): @check_installed async def start(self): - """Set options and start addon.""" + """Set options and start add-on.""" if await self.instance.is_running(): - _LOGGER.warning("%s allready running!", self.slug) + _LOGGER.warning("%s already running!", self.slug) return # Access Token @@ -724,7 +724,7 @@ class Addon(CoreSysAttributes): @check_installed def stop(self): - """Stop addon. + """Stop add-on. Return a coroutine. """ @@ -732,11 +732,11 @@ class Addon(CoreSysAttributes): @check_installed async def update(self): - """Update addon.""" + """Update add-on.""" last_state = await self.state() if self.last_version == self.version_installed: - _LOGGER.warning("No update available for Addon %s", self._id) + _LOGGER.warning("No update available for add-on %s", self._id) return False if not await self.instance.update(self.last_version): @@ -753,13 +753,13 @@ class Addon(CoreSysAttributes): @check_installed async def restart(self): - """Restart addon.""" + """Restart add-on.""" await self.stop() return await self.start() @check_installed def logs(self): - """Return addons log output. + """Return add-ons log output. Return a coroutine. """ @@ -775,11 +775,11 @@ class Addon(CoreSysAttributes): @check_installed async def rebuild(self): - """Performe a rebuild of local build addon.""" + """Perform a rebuild of local build add-on.""" last_state = await self.state() if not self.need_build: - _LOGGER.error("Can't rebuild a none local build addon!") + _LOGGER.error("Can't rebuild a none local build add-on!") return False # remove docker container but not addon config @@ -808,7 +808,7 @@ class Addon(CoreSysAttributes): @check_installed async def snapshot(self, tar_file): - """Snapshot state of an addon.""" + """Snapshot state of an add-on.""" with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp: # store local image if self.need_build and not await \ @@ -822,7 +822,7 @@ class Addon(CoreSysAttributes): ATTR_STATE: await self.state(), } - # store local configs/state + # Store local configs/state try: write_json_file(Path(temp, 'addon.json'), data) except (OSError, json.JSONDecodeError) as err: @@ -846,7 +846,7 @@ class Addon(CoreSysAttributes): snapshot.add(self.path_data, arcname="data") try: - _LOGGER.info("Build snapshot for addon %s", self._id) + _LOGGER.info("Build snapshot for add-on %s", self._id) await self.sys_run_in_executor(_write_tarfile) except (tarfile.TarError, OSError) as err: _LOGGER.error("Can't write tarfile %s: %s", tar_file, err) @@ -856,7 +856,7 @@ class Addon(CoreSysAttributes): return True async def restore(self, tar_file): - """Restore state of an addon.""" + """Restore state of an add-on.""" with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp: # extract snapshot def _extract_tarfile(): @@ -870,13 +870,13 @@ class Addon(CoreSysAttributes): _LOGGER.error("Can't read tarfile %s: %s", tar_file, err) return False - # read snapshot data + # Read snapshot data try: data = read_json_file(Path(temp, 'addon.json')) except (OSError, json.JSONDecodeError) as err: _LOGGER.error("Can't read addon.json: %s", err) - # validate + # Validate try: data = SCHEMA_ADDON_SNAPSHOT(data) except vol.Invalid as err: @@ -884,11 +884,11 @@ class Addon(CoreSysAttributes): self._id, humanize_error(data, err)) return False - # restore data / reload addon + # Restore data or reload add-on _LOGGER.info("Restore config for addon %s", self._id) self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM]) - # check version / restore image + # Check version / restore image version = data[ATTR_VERSION] if not await self.instance.exists(): _LOGGER.info("Restore image for addon %s", self._id) @@ -902,7 +902,7 @@ class Addon(CoreSysAttributes): else: await self.instance.stop() - # restore data + # Restore data def _restore_data(): """Restore data.""" shutil.copytree(str(Path(temp, "data")), str(self.path_data)) @@ -926,9 +926,9 @@ class Addon(CoreSysAttributes): _LOGGER.error("Can't restore AppArmor profile") return False - # run addon + # Run add-on if data[ATTR_STATE] == STATE_STARTED: return await self.start() - _LOGGER.info("Finish restore for addon %s", self._id) + _LOGGER.info("Finish restore for add-on %s", self._id) return True diff --git a/hassio/addons/build.py b/hassio/addons/build.py index 4d9a37618..b857b01ef 100644 --- a/hassio/addons/build.py +++ b/hassio/addons/build.py @@ -1,4 +1,4 @@ -"""HassIO addons build environment.""" +"""Hass.io add-on build environment.""" from pathlib import Path from .validate import SCHEMA_BUILD_CONFIG, BASE_IMAGE @@ -8,10 +8,10 @@ from ..utils.json import JsonConfig class AddonBuild(JsonConfig, CoreSysAttributes): - """Handle build options for addons.""" + """Handle build options for add-ons.""" def __init__(self, coresys, slug): - """Initialize addon builder.""" + """Initialize Hass.io add-on builder.""" self.coresys = coresys self._id = slug @@ -24,12 +24,12 @@ class AddonBuild(JsonConfig, CoreSysAttributes): @property def addon(self): - """Return addon of build data.""" + """Return add-on of build data.""" return self.sys_addons.get(self._id) @property def base_image(self): - """Base images for this addon.""" + """Base images for this add-on.""" return self._data[ATTR_BUILD_FROM].get( self.sys_arch, BASE_IMAGE[self.sys_arch]) @@ -40,11 +40,11 @@ class AddonBuild(JsonConfig, CoreSysAttributes): @property def additional_args(self): - """Return additional docker build arguments.""" + """Return additional Docker build arguments.""" return self._data[ATTR_ARGS] def get_docker_args(self, version): - """Create a dict with docker build arguments.""" + """Create a dict with Docker build arguments.""" args = { 'path': str(self.addon.path_location), 'tag': f"{self.addon.image}:{version}", diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 185f2f33a..86ad63cce 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -1,4 +1,4 @@ -"""Init file for HassIO addons.""" +"""Init file for Hass.io add-on data.""" import logging import json from pathlib import Path @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) class AddonsData(JsonConfig, CoreSysAttributes): - """Hold data for addons inside HassIO.""" + """Hold data for Add-ons inside Hass.io.""" def __init__(self, coresys): """Initialize data holder.""" @@ -30,26 +30,26 @@ class AddonsData(JsonConfig, CoreSysAttributes): @property def user(self): - """Return local addon user data.""" + """Return local add-on user data.""" return self._data[ATTR_USER] @property def system(self): - """Return local addon data.""" + """Return local add-on data.""" return self._data[ATTR_SYSTEM] @property def cache(self): - """Return addon data from cache/repositories.""" + """Return add-on data from cache/repositories.""" return self._cache @property def repositories(self): - """Return addon data from repositories.""" + """Return add-on data from repositories.""" return self._repositories def reload(self): - """Read data from addons repository.""" + """Read data from add-on repository.""" self._cache = {} self._repositories = {} @@ -94,7 +94,7 @@ class AddonsData(JsonConfig, CoreSysAttributes): self._read_addons_folder(path, slug) def _read_addons_folder(self, path, repository): - """Read data from addons folder.""" + """Read data from add-ons folder.""" for addon in path.glob("**/config.json"): try: addon_config = read_json_file(addon) diff --git a/hassio/addons/git.py b/hassio/addons/git.py index 71f80160b..4fb16dc51 100644 --- a/hassio/addons/git.py +++ b/hassio/addons/git.py @@ -1,4 +1,4 @@ -"""Init file for HassIO addons git.""" +"""Init file for Hass.io add-on Git.""" import asyncio import logging import functools as ft @@ -16,10 +16,10 @@ _LOGGER = logging.getLogger(__name__) class GitRepo(CoreSysAttributes): - """Manage addons git repo.""" + """Manage Add-on Git repository.""" def __init__(self, coresys, path, url): - """Initialize git base wrapper.""" + """Initialize Git base wrapper.""" self.coresys = coresys self.repo = None self.path = path @@ -38,13 +38,13 @@ class GitRepo(CoreSysAttributes): return self._data[ATTR_BRANCH] async def load(self): - """Init git addon repo.""" + """Init Git add-on repository.""" if not self.path.is_dir(): return await self.clone() async with self.lock: try: - _LOGGER.info("Load addon %s repository", self.path) + _LOGGER.info("Load add-on %s repository", self.path) self.repo = await self.sys_run_in_executor( git.Repo, str(self.path)) @@ -57,7 +57,7 @@ class GitRepo(CoreSysAttributes): return True async def clone(self): - """Clone git addon repo.""" + """Clone git add-on repository.""" async with self.lock: git_args = { attribute: value @@ -70,7 +70,7 @@ class GitRepo(CoreSysAttributes): } try: - _LOGGER.info("Clone addon %s repository", self.url) + _LOGGER.info("Clone add-on %s repository", self.url) self.repo = await self.sys_run_in_executor(ft.partial( git.Repo.clone_from, self.url, str(self.path), **git_args @@ -78,20 +78,20 @@ class GitRepo(CoreSysAttributes): except (git.InvalidGitRepositoryError, git.NoSuchPathError, git.GitCommandError) as err: - _LOGGER.error("Can't clone %s repo: %s.", self.url, err) + _LOGGER.error("Can't clone %s repository: %s.", self.url, err) self._remove() return False return True async def pull(self): - """Pull git addon repo.""" + """Pull Git add-on repo.""" if self.lock.locked(): - _LOGGER.warning("It is already a task in progress.") + _LOGGER.warning("It is already a task in progress") return False async with self.lock: - _LOGGER.info("Update addon %s repository", self.url) + _LOGGER.info("Update add-on %s repository", self.url) branch = self.repo.active_branch.name try: @@ -130,19 +130,19 @@ class GitRepo(CoreSysAttributes): class GitRepoHassIO(GitRepo): - """HassIO addons repository.""" + """Hass.io add-ons repository.""" def __init__(self, coresys): - """Initialize git hassio addon repository.""" + """Initialize Git Hass.io add-on repository.""" super().__init__( coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS) class GitRepoCustom(GitRepo): - """Custom addons repository.""" + """Custom add-ons repository.""" def __init__(self, coresys, url): - """Initialize git hassio addon repository.""" + """Initialize custom Git Hass.io addo-n repository.""" path = Path( coresys.config.path_addons_git, get_hash_from_repository(url)) @@ -151,5 +151,5 @@ class GitRepoCustom(GitRepo): def remove(self): """Remove a custom repository.""" - _LOGGER.info("Remove custom addon repository %s", self.url) + _LOGGER.info("Remove custom add-on repository %s", self.url) self._remove() diff --git a/hassio/addons/repository.py b/hassio/addons/repository.py index 37d75ea75..35fc695a0 100644 --- a/hassio/addons/repository.py +++ b/hassio/addons/repository.py @@ -1,4 +1,4 @@ -"""Represent a HassIO repository.""" +"""Represent a Hass.io repository.""" from .git import GitRepoHassIO, GitRepoCustom from .utils import get_hash_from_repository from ..const import ( @@ -9,7 +9,7 @@ UNKNOWN = 'unknown' class Repository(CoreSysAttributes): - """Repository in HassIO.""" + """Repository in Hass.io.""" def __init__(self, coresys, repository): """Initialize repository object.""" @@ -44,7 +44,7 @@ class Repository(CoreSysAttributes): @property def url(self): - """Return url of repository.""" + """Return URL of repository.""" return self._mesh.get(ATTR_URL, self.source) @property @@ -59,13 +59,13 @@ class Repository(CoreSysAttributes): return True async def update(self): - """Update addon repository.""" + """Update add-on repository.""" if self.git: return await self.git.pull() return True def remove(self): - """Remove addon repository.""" + """Remove add-on repository.""" if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL): raise RuntimeError("Can't remove built-in repositories!") diff --git a/hassio/addons/utils.py b/hassio/addons/utils.py index e937e5bc1..26e3d3cd6 100644 --- a/hassio/addons/utils.py +++ b/hassio/addons/utils.py @@ -1,4 +1,4 @@ -"""Util addons functions.""" +"""Util add-ons functions.""" import asyncio import hashlib import logging @@ -78,7 +78,7 @@ def extract_hash_from_path(path): def check_installed(method): - """Wrap function with check if addon is installed.""" + """Wrap function with check if add-on is installed.""" async def wrap_check(addon, *args, **kwargs): """Return False if not installed or the function.""" if not addon.is_installed: diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 794c00d9c..44695309f 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -1,4 +1,4 @@ -"""Validate addons options schema.""" +"""Validate add-ons options schema.""" import logging import re import uuid @@ -218,7 +218,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema({ def validate_options(raw_schema): """Validate schema.""" def validate(struct): - """Create schema validator for addons options.""" + """Create schema validator for add-ons options.""" options = {} # read options diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 94d04833b..a9b6ba9de 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -1,4 +1,4 @@ -"""Init file for HassIO rest api.""" +"""Init file for Hass.io RESTful API.""" import logging from pathlib import Path @@ -21,10 +21,10 @@ _LOGGER = logging.getLogger(__name__) class RestAPI(CoreSysAttributes): - """Handle rest api for hassio.""" + """Handle RESTful API for Hass.io.""" def __init__(self, coresys): - """Initialize docker base wrapper.""" + """Initialize Docker base wrapper.""" self.coresys = coresys self.security = SecurityMiddleware(coresys) self.webapp = web.Application( @@ -49,7 +49,7 @@ class RestAPI(CoreSysAttributes): self._register_services() def _register_host(self): - """Register hostcontrol function.""" + """Register hostcontrol functions.""" api_host = APIHost() api_host.coresys = self.coresys @@ -69,7 +69,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_hassos(self): - """Register hassos function.""" + """Register HassOS functions.""" api_hassos = APIHassOS() api_hassos.coresys = self.coresys @@ -81,7 +81,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_hardware(self): - """Register hardware function.""" + """Register hardware functions.""" api_hardware = APIHardware() api_hardware.coresys = self.coresys @@ -91,7 +91,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_supervisor(self): - """Register supervisor function.""" + """Register Supervisor functions.""" api_supervisor = APISupervisor() api_supervisor.coresys = self.coresys @@ -106,7 +106,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_homeassistant(self): - """Register homeassistant function.""" + """Register Home Assistant functions.""" api_hass = APIHomeAssistant() api_hass.coresys = self.coresys @@ -123,7 +123,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_proxy(self): - """Register HomeAssistant API Proxy.""" + """Register Home Assistant API Proxy.""" api_proxy = APIProxy() api_proxy.coresys = self.coresys @@ -137,7 +137,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_addons(self): - """Register homeassistant function.""" + """Register Add-on functions.""" api_addons = APIAddons() api_addons.coresys = self.coresys @@ -163,7 +163,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_snapshots(self): - """Register snapshots function.""" + """Register snapshots functions.""" api_snapshots = APISnapshots() api_snapshots.coresys = self.coresys @@ -183,6 +183,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_services(self): + """Register services functions.""" api_services = APIServices() api_services.coresys = self.coresys @@ -194,6 +195,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_discovery(self): + """Register discovery functions.""" api_discovery = APIDiscovery() api_discovery.coresys = self.coresys @@ -206,7 +208,7 @@ class RestAPI(CoreSysAttributes): ]) def _register_panel(self): - """Register panel for homeassistant.""" + """Register panel for Home Assistant.""" panel_dir = Path(__file__).parent.joinpath("panel") def create_response(panel_file): @@ -234,7 +236,7 @@ class RestAPI(CoreSysAttributes): self.webapp.add_routes([web.static('/app', panel_dir)]) async def start(self): - """Run rest api webserver.""" + """Run RESTful API webserver.""" await self._runner.setup() self._site = web.TCPSite( self._runner, host="0.0.0.0", port=80, shutdown_timeout=5) @@ -248,7 +250,7 @@ class RestAPI(CoreSysAttributes): _LOGGER.info("Start API on %s", self.sys_docker.network.supervisor) async def stop(self): - """Stop rest api webserver.""" + """Stop RESTful API webserver.""" if not self._site: return diff --git a/hassio/api/addons.py b/hassio/api/addons.py index e95ec063a..2d1fb34a8 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -1,4 +1,4 @@ -"""Init file for HassIO homeassistant rest api.""" +"""Init file for Hass.io Home Assistant RESTful API.""" import asyncio import logging @@ -48,7 +48,7 @@ SCHEMA_SECURITY = vol.Schema({ class APIAddons(CoreSysAttributes): - """Handle rest api for addons functions.""" + """Handle RESTful API for add-on functions.""" def _extract_addon(self, request, check_installed=True): """Return addon, throw an exception it it doesn't exist.""" @@ -77,7 +77,7 @@ class APIAddons(CoreSysAttributes): @api_process async def list(self, request): - """Return all addons / repositories .""" + """Return all add-ons or repositories.""" data_addons = [] for addon in self.sys_addons.list_addons: data_addons.append({ @@ -112,13 +112,13 @@ class APIAddons(CoreSysAttributes): @api_process async def reload(self, request): - """Reload all addons data.""" + """Reload all add-on data.""" await asyncio.shield(self.sys_addons.reload()) return True @api_process async def info(self, request): - """Return addon information.""" + """Return add-on information.""" addon = self._extract_addon(request, check_installed=False) return { @@ -167,7 +167,7 @@ class APIAddons(CoreSysAttributes): @api_process async def options(self, request): - """Store user options for addon.""" + """Store user options for add-on.""" addon = self._extract_addon(request) addon_schema = SCHEMA_OPTIONS.extend({ @@ -194,7 +194,7 @@ class APIAddons(CoreSysAttributes): @api_process async def security(self, request): - """Store security options for addon.""" + """Store security options for add-on.""" addon = self._extract_addon(request) # Have Access @@ -233,19 +233,19 @@ class APIAddons(CoreSysAttributes): @api_process def install(self, request): - """Install addon.""" + """Install add-on.""" addon = self._extract_addon(request, check_installed=False) return asyncio.shield(addon.install()) @api_process def uninstall(self, request): - """Uninstall addon.""" + """Uninstall add-on.""" addon = self._extract_addon(request) return asyncio.shield(addon.uninstall()) @api_process def start(self, request): - """Start addon.""" + """Start add-on.""" addon = self._extract_addon(request) # check options @@ -259,13 +259,13 @@ class APIAddons(CoreSysAttributes): @api_process def stop(self, request): - """Stop addon.""" + """Stop add-on.""" addon = self._extract_addon(request) return asyncio.shield(addon.stop()) @api_process def update(self, request): - """Update addon.""" + """Update add-on.""" addon = self._extract_addon(request) if addon.last_version == addon.version_installed: @@ -275,13 +275,13 @@ class APIAddons(CoreSysAttributes): @api_process def restart(self, request): - """Restart addon.""" + """Restart add-on.""" addon = self._extract_addon(request) return asyncio.shield(addon.restart()) @api_process def rebuild(self, request): - """Rebuild local build addon.""" + """Rebuild local build add-on.""" addon = self._extract_addon(request) if not addon.need_build: raise RuntimeError("Only local build addons are supported") @@ -290,13 +290,13 @@ class APIAddons(CoreSysAttributes): @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request): - """Return logs from addon.""" + """Return logs from add-on.""" addon = self._extract_addon(request) return addon.logs() @api_process_raw(CONTENT_TYPE_PNG) async def icon(self, request): - """Return icon from addon.""" + """Return icon from add-on.""" addon = self._extract_addon(request, check_installed=False) if not addon.with_icon: raise RuntimeError("No icon found!") @@ -306,7 +306,7 @@ class APIAddons(CoreSysAttributes): @api_process_raw(CONTENT_TYPE_PNG) async def logo(self, request): - """Return logo from addon.""" + """Return logo from add-on.""" addon = self._extract_addon(request, check_installed=False) if not addon.with_logo: raise RuntimeError("No logo found!") @@ -316,7 +316,7 @@ class APIAddons(CoreSysAttributes): @api_process_raw(CONTENT_TYPE_TEXT) async def changelog(self, request): - """Return changelog from addon.""" + """Return changelog from add-on.""" addon = self._extract_addon(request, check_installed=False) if not addon.with_changelog: raise RuntimeError("No changelog found!") @@ -326,10 +326,10 @@ class APIAddons(CoreSysAttributes): @api_process async def stdin(self, request): - """Write to stdin of addon.""" + """Write to stdin of add-on.""" addon = self._extract_addon(request) if not addon.with_stdin: - raise RuntimeError("STDIN not supported by addon") + raise RuntimeError("STDIN not supported by add-on") data = await request.read() return await asyncio.shield(addon.write_stdin(data)) diff --git a/hassio/api/discovery.py b/hassio/api/discovery.py index 7ab5f8d2a..ff08517db 100644 --- a/hassio/api/discovery.py +++ b/hassio/api/discovery.py @@ -1,5 +1,4 @@ -"""Init file for HassIO network rest api.""" - +"""Init file for Hass.io network RESTful API.""" import voluptuous as vol from .utils import api_process, api_validate @@ -17,7 +16,7 @@ SCHEMA_DISCOVERY = vol.Schema({ class APIDiscovery(CoreSysAttributes): - """Handle rest api for discovery functions.""" + """Handle RESTful API for discovery functions.""" def _extract_message(self, request): """Extract discovery message from URL.""" diff --git a/hassio/api/hardware.py b/hassio/api/hardware.py index 7830b9675..f9676209d 100644 --- a/hassio/api/hardware.py +++ b/hassio/api/hardware.py @@ -1,4 +1,4 @@ -"""Init file for HassIO hardware rest api.""" +"""Init file for Hass.io hardware RESTful API.""" import logging from .utils import api_process @@ -10,7 +10,7 @@ _LOGGER = logging.getLogger(__name__) class APIHardware(CoreSysAttributes): - """Handle rest api for hardware functions.""" + """Handle RESTful API for hardware functions.""" @api_process async def info(self, request): diff --git a/hassio/api/hassos.py b/hassio/api/hassos.py index ac8166027..eb2d8ccdd 100644 --- a/hassio/api/hassos.py +++ b/hassio/api/hassos.py @@ -1,4 +1,4 @@ -"""Init file for Hass.io hassos rest api.""" +"""Init file for Hass.io HassOS RESTful API.""" import asyncio import logging @@ -18,11 +18,11 @@ SCHEMA_VERSION = vol.Schema({ class APIHassOS(CoreSysAttributes): - """Handle rest api for hassos functions.""" + """Handle RESTful API for HassOS functions.""" @api_process async def info(self, request): - """Return hassos information.""" + """Return HassOS information.""" return { ATTR_VERSION: self.sys_hassos.version, ATTR_VERSION_CLI: self.sys_hassos.version_cli, diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index e0a1b158c..baf530cdf 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -1,4 +1,4 @@ -"""Init file for HassIO homeassistant rest api.""" +"""Init file for Hass.io Home Assistant RESTful API.""" import asyncio import logging @@ -39,7 +39,7 @@ SCHEMA_VERSION = vol.Schema({ class APIHomeAssistant(CoreSysAttributes): - """Handle rest api for homeassistant functions.""" + """Handle RESTful API for Home Assistant functions.""" @api_process async def info(self, request): @@ -59,7 +59,7 @@ class APIHomeAssistant(CoreSysAttributes): @api_process async def options(self, request): - """Set homeassistant options.""" + """Set Home Assistant options.""" body = await api_validate(SCHEMA_OPTIONS, request) if ATTR_IMAGE in body and ATTR_LAST_VERSION in body: @@ -108,7 +108,7 @@ class APIHomeAssistant(CoreSysAttributes): @api_process async def update(self, request): - """Update homeassistant.""" + """Update Home Assistant.""" body = await api_validate(SCHEMA_VERSION, request) version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version) @@ -116,27 +116,27 @@ class APIHomeAssistant(CoreSysAttributes): @api_process def stop(self, request): - """Stop homeassistant.""" + """Stop Home Assistant.""" return asyncio.shield(self.sys_homeassistant.stop()) @api_process def start(self, request): - """Start homeassistant.""" + """Start Home Assistant.""" return asyncio.shield(self.sys_homeassistant.start()) @api_process def restart(self, request): - """Restart homeassistant.""" + """Restart Home Assistant.""" return asyncio.shield(self.sys_homeassistant.restart()) @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request): - """Return homeassistant docker logs.""" + """Return Home Assistant Docker logs.""" return self.sys_homeassistant.logs() @api_process async def check(self, request): - """Check config of homeassistant.""" + """Check configuration of Home Assistant.""" result = await self.sys_homeassistant.check_config() if not result.valid: raise RuntimeError(result.log) diff --git a/hassio/api/host.py b/hassio/api/host.py index 49352dfc9..7d04e9112 100644 --- a/hassio/api/host.py +++ b/hassio/api/host.py @@ -1,4 +1,4 @@ -"""Init file for HassIO host rest api.""" +"""Init file for Hass.io host RESTful API.""" import asyncio import logging @@ -21,7 +21,7 @@ SCHEMA_OPTIONS = vol.Schema({ class APIHost(CoreSysAttributes): - """Handle rest api for host functions.""" + """Handle RESTful API for host functions.""" @api_process async def info(self, request): diff --git a/hassio/api/proxy.py b/hassio/api/proxy.py index 526528566..4e96c6918 100644 --- a/hassio/api/proxy.py +++ b/hassio/api/proxy.py @@ -1,4 +1,4 @@ -"""Utils for HomeAssistant Proxy.""" +"""Utils for Home Assistant Proxy.""" import asyncio from contextlib import asynccontextmanager import logging @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) class APIProxy(CoreSysAttributes): - """API Proxy for Home-Assistant.""" + """API Proxy for Home Assistant.""" def _check_access(self, request): """Check the Hass.io token.""" @@ -30,7 +30,7 @@ class APIProxy(CoreSysAttributes): addon = self.sys_addons.from_uuid(hassio_token) if not addon: - _LOGGER.warning("Unknown HomeAssistant API access!") + _LOGGER.warning("Unknown Home Assistant API access!") elif not addon.access_homeassistant_api: _LOGGER.warning("Not permitted API access: %s", addon.slug) else: @@ -41,7 +41,7 @@ class APIProxy(CoreSysAttributes): @asynccontextmanager async def _api_client(self, request, path, timeout=300): - """Return a client request with proxy origin for Home-Assistant.""" + """Return a client request with proxy origin for Home Assistant.""" try: # read data with async_timeout.timeout(30): @@ -76,7 +76,7 @@ class APIProxy(CoreSysAttributes): """Proxy HomeAssistant EventStream Requests.""" self._check_access(request) - _LOGGER.info("Home-Assistant EventStream start") + _LOGGER.info("Home Assistant EventStream start") async with self._api_client(request, 'stream', timeout=None) as client: response = web.StreamResponse() response.content_type = request.headers.get(CONTENT_TYPE) @@ -93,12 +93,12 @@ class APIProxy(CoreSysAttributes): finally: client.close() - _LOGGER.info("Home-Assistant EventStream close") + _LOGGER.info("Home Assistant EventStream close") return response async def api(self, request): - """Proxy HomeAssistant API Requests.""" + """Proxy Home Assistant API Requests.""" self._check_access(request) # Normal request @@ -112,14 +112,14 @@ class APIProxy(CoreSysAttributes): ) async def _websocket_client(self): - """Initialize a websocket api connection.""" + """Initialize a WebSocket API connection.""" url = f"{self.sys_homeassistant.api_url}/api/websocket" try: client = await self.sys_websession_ssl.ws_connect( url, heartbeat=60, verify_ssl=False) - # handle authentication + # Handle authentication data = await client.receive_json() if data.get('type') == 'auth_ok': @@ -128,7 +128,7 @@ class APIProxy(CoreSysAttributes): if data.get('type') != 'auth_required': # Invalid protocol _LOGGER.error( - 'Got unexpected response from HA websocket: %s', data) + "Got unexpected response from HA WebSocket: %s", data) raise HTTPBadGateway() if self.sys_homeassistant.refresh_token: @@ -157,15 +157,15 @@ class APIProxy(CoreSysAttributes): raise HomeAssistantAuthError() except (RuntimeError, ValueError) as err: - _LOGGER.error("Client error on websocket API %s.", err) + _LOGGER.error("Client error on WebSocket API %s.", err) except HomeAssistantAuthError as err: - _LOGGER.error("Failed authentication to HomeAssistant websocket") + _LOGGER.error("Failed authentication to Home Assistant WebSocket") raise HTTPBadGateway() async def websocket(self, request): - """Initialize a websocket api connection.""" - _LOGGER.info("Home-Assistant Websocket API request initialze") + """Initialize a WebSocket API connection.""" + _LOGGER.info("Home Assistant WebSocket API request initialize") # init server server = web.WebSocketResponse(heartbeat=60) @@ -189,14 +189,14 @@ class APIProxy(CoreSysAttributes): addon = self.sys_addons.from_uuid(hassio_token) if not addon or not addon.access_homeassistant_api: - _LOGGER.warning("Unauthorized websocket access!") + _LOGGER.warning("Unauthorized WebSocket access!") await server.send_json({ 'type': 'auth_invalid', 'message': 'Invalid access', }) return server - _LOGGER.info("Websocket access from %s", addon.slug) + _LOGGER.info("WebSocket access from %s", addon.slug) await server.send_json({ 'type': 'auth_ok', @@ -209,7 +209,7 @@ class APIProxy(CoreSysAttributes): # init connection to hass client = await self._websocket_client() - _LOGGER.info("Home-Assistant Websocket API request running") + _LOGGER.info("Home Assistant WebSocket API request running") try: client_read = None server_read = None @@ -243,7 +243,7 @@ class APIProxy(CoreSysAttributes): pass except RuntimeError as err: - _LOGGER.info("Home-Assistant Websocket API error: %s", err) + _LOGGER.info("Home Assistant WebSocket API error: %s", err) finally: if client_read: @@ -255,5 +255,5 @@ class APIProxy(CoreSysAttributes): await client.close() await server.close() - _LOGGER.info("Home-Assistant Websocket API connection is closed") + _LOGGER.info("Home Assistant WebSocket API connection is closed") return server diff --git a/hassio/api/security.py b/hassio/api/security.py index c2b800f71..a729e49c8 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -88,7 +88,7 @@ class SecurityMiddleware(CoreSysAttributes): # UUID check need removed with 131 if hassio_token in (self.sys_homeassistant.uuid, self.sys_homeassistant.hassio_token): - _LOGGER.debug("%s access from Home-Assistant", request.path) + _LOGGER.debug("%s access from Home Assistant", request.path) request_from = 'homeassistant' # Host diff --git a/hassio/api/services.py b/hassio/api/services.py index 240079a0b..79618b5bc 100644 --- a/hassio/api/services.py +++ b/hassio/api/services.py @@ -1,4 +1,4 @@ -"""Init file for HassIO network rest api.""" +"""Init file for Hass.io network RESTful API.""" from .utils import api_process, api_validate from ..const import ( @@ -7,7 +7,7 @@ from ..coresys import CoreSysAttributes class APIServices(CoreSysAttributes): - """Handle rest api for services functions.""" + """Handle RESTful API for services functions.""" def _extract_service(self, request): """Return service, throw an exception if it doesn't exist.""" diff --git a/hassio/api/snapshots.py b/hassio/api/snapshots.py index fc3be36e5..6ca5aa4fd 100644 --- a/hassio/api/snapshots.py +++ b/hassio/api/snapshots.py @@ -1,4 +1,4 @@ -"""Init file for HassIO snapshot rest api.""" +"""Init file for Hass.io snapshot RESTful API.""" import asyncio import logging from pathlib import Path @@ -46,7 +46,7 @@ SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({ class APISnapshots(CoreSysAttributes): - """Handle rest api for snapshot functions.""" + """Handle RESTful API for snapshot functions.""" def _extract_snapshot(self, request): """Return snapshot, throw an exception if it doesn't exist.""" diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 299b7eca1..88e0f9526 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -1,4 +1,4 @@ -"""Init file for HassIO supervisor rest api.""" +"""Init file for Hass.io Supervisor RESTful API.""" import asyncio import logging @@ -30,11 +30,11 @@ SCHEMA_VERSION = vol.Schema({ class APISupervisor(CoreSysAttributes): - """Handle rest api for supervisor functions.""" + """Handle RESTful API for Supervisor functions.""" @api_process async def ping(self, request): - """Return ok for signal that the api is ready.""" + """Return ok for signal that the API is ready.""" return True @api_process @@ -68,7 +68,7 @@ class APISupervisor(CoreSysAttributes): @api_process async def options(self, request): - """Set supervisor options.""" + """Set Supervisor options.""" body = await api_validate(SCHEMA_OPTIONS, request) if ATTR_CHANNEL in body: @@ -107,7 +107,7 @@ class APISupervisor(CoreSysAttributes): @api_process async def update(self, request): - """Update supervisor OS.""" + """Update Supervisor OS.""" body = await api_validate(SCHEMA_VERSION, request) version = body.get(ATTR_VERSION, self.sys_updater.version_hassio) @@ -119,7 +119,7 @@ class APISupervisor(CoreSysAttributes): @api_process async def reload(self, request): - """Reload addons, config etc.""" + """Reload add-ons, configuration, etc.""" tasks = [ self.sys_updater.reload(), ] @@ -134,5 +134,5 @@ class APISupervisor(CoreSysAttributes): @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request): - """Return supervisor docker logs.""" + """Return supervisor Docker logs.""" return self.sys_supervisor.logs() diff --git a/hassio/api/utils.py b/hassio/api/utils.py index f4f5f9e32..7472ea014 100644 --- a/hassio/api/utils.py +++ b/hassio/api/utils.py @@ -1,4 +1,4 @@ -"""Init file for HassIO util for rest api.""" +"""Init file for Hass.io util for RESTful API.""" import json import logging @@ -27,7 +27,7 @@ def json_loads(data): def api_process(method): """Wrap function with true/false calls to rest api.""" async def wrap_api(api, *args, **kwargs): - """Return api information.""" + """Return API information.""" try: answer = await method(api, *args, **kwargs) except HassioError: diff --git a/hassio/dbus/systemd.py b/hassio/dbus/systemd.py index c1efe178a..9a1be1140 100644 --- a/hassio/dbus/systemd.py +++ b/hassio/dbus/systemd.py @@ -16,7 +16,7 @@ class Systemd(DBusInterface): """Systemd function handler.""" async def connect(self): - """Connect do bus.""" + """Connect to D-Bus.""" try: self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT) except DBusError: diff --git a/hassio/docker/__init__.py b/hassio/docker/__init__.py index aaf633473..378a648f6 100644 --- a/hassio/docker/__init__.py +++ b/hassio/docker/__init__.py @@ -1,4 +1,4 @@ -"""Init file for HassIO docker object.""" +"""Init file for Hass.io Docker object.""" from contextlib import suppress import logging @@ -15,13 +15,13 @@ CommandReturn = attr.make_class('CommandReturn', ['exit_code', 'output']) class DockerAPI: - """Docker hassio wrapper. + """Docker Hass.io wrapper. This class is not AsyncIO safe! """ def __init__(self): - """Initialize docker base wrapper.""" + """Initialize Docker base wrapper.""" self.docker = docker.DockerClient( base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto', timeout=900) @@ -29,21 +29,21 @@ class DockerAPI: @property def images(self): - """Return api images.""" + """Return API images.""" return self.docker.images @property def containers(self): - """Return api containers.""" + """Return API containers.""" return self.docker.containers @property def api(self): - """Return api containers.""" + """Return API containers.""" return self.docker.api def run(self, image, **kwargs): - """"Create a docker and run it. + """"Create a Docker container and run it. Need run inside executor. """ @@ -51,7 +51,7 @@ class DockerAPI: network_mode = kwargs.get('network_mode') hostname = kwargs.get('hostname') - # setup network + # Setup network kwargs['dns_search'] = ["."] if network_mode: kwargs['dns'] = [str(self.network.supervisor)] @@ -59,7 +59,7 @@ class DockerAPI: else: kwargs['network'] = None - # create container + # Create container try: container = self.docker.containers.create(image, **kwargs) except docker.errors.DockerException as err: diff --git a/hassio/docker/addon.py b/hassio/docker/addon.py index 53aa9e96a..6b73de736 100644 --- a/hassio/docker/addon.py +++ b/hassio/docker/addon.py @@ -1,4 +1,4 @@ -"""Init file for HassIO addon docker object.""" +"""Init file for Hass.io add-on Docker object.""" import logging import os from pathlib import Path @@ -19,45 +19,45 @@ AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm" class DockerAddon(DockerInterface): - """Docker hassio wrapper for HomeAssistant.""" + """Docker Hass.io wrapper for Home Assistant.""" def __init__(self, coresys, slug): - """Initialize docker homeassistant wrapper.""" + """Initialize Docker Home Assistant wrapper.""" super().__init__(coresys) self._id = slug @property def addon(self): - """Return addon of docker image.""" + """Return add-on of Docker image.""" return self.sys_addons.get(self._id) @property def image(self): - """Return name of docker image.""" + """Return name of Docker image.""" return self.addon.image @property def timeout(self): - """Return timeout for docker actions.""" + """Return timeout for Docker actions.""" return self.addon.timeout @property def version(self): - """Return version of docker image.""" + """Return version of Docker image.""" if not self.addon.legacy: return super().version return self.addon.version_installed @property def arch(self): - """Return arch of docker image.""" + """Return arch of Docker image.""" if not self.addon.legacy: return super().arch return self.sys_arch @property def name(self): - """Return name of docker container.""" + """Return name of Docker container.""" return "addon_{}".format(self.addon.slug) @property @@ -74,12 +74,12 @@ class DockerAddon(DockerInterface): @property def hostname(self): - """Return slug/id of addon.""" + """Return slug/id of add-on.""" return self.addon.slug.replace('_', '-') @property def environment(self): - """Return environment for docker add-on.""" + """Return environment for Docker add-on.""" addon_env = self.addon.environment or {} # Need audio settings @@ -114,7 +114,7 @@ class DockerAddon(DockerInterface): @property def ports(self): - """Filter None from addon ports.""" + """Filter None from add-on ports.""" if not self.addon.ports: return None @@ -126,7 +126,7 @@ class DockerAddon(DockerInterface): @property def security_opt(self): - """Controlling security opt.""" + """Controlling security options.""" security = [] # AppArmor @@ -144,7 +144,7 @@ class DockerAddon(DockerInterface): @property def tmpfs(self): - """Return tmpfs for docker add-on.""" + """Return tmpfs for Docker add-on.""" options = self.addon.tmpfs if options: return {"/tmpfs": f"{options}"} @@ -160,14 +160,14 @@ class DockerAddon(DockerInterface): @property def network_mode(self): - """Return network mode for addon.""" + """Return network mode for add-on.""" if self.addon.host_network: return 'host' return None @property def pid_mode(self): - """Return PID mode for addon.""" + """Return PID mode for add-on.""" if not self.addon.protected and self.addon.host_pid: return 'host' return None @@ -242,7 +242,7 @@ class DockerAddon(DockerInterface): }, }) - # Host dbus system + # Host D-Bus system if self.addon.host_dbus: volumes.update({ "/var/run/dbus": { @@ -259,7 +259,7 @@ class DockerAddon(DockerInterface): return volumes def _run(self): - """Run docker image. + """Run Docker image. Need run inside executor. """ @@ -269,7 +269,7 @@ class DockerAddon(DockerInterface): # Security check if not self.addon.protected: _LOGGER.warning( - "%s run with disabled proteced mode!", self.addon.name) + "%s run with disabled protected mode!", self.addon.name) # cleanup self._stop() @@ -296,13 +296,13 @@ class DockerAddon(DockerInterface): ) if ret: - _LOGGER.info("Start docker addon %s with version %s", + _LOGGER.info("Start Docker add-on %s with version %s", self.image, self.version) return ret def _install(self, tag): - """Pull docker image or build it. + """Pull Docker image or build it. Need run inside executor. """ @@ -312,7 +312,7 @@ class DockerAddon(DockerInterface): return super()._install(tag) def _build(self, tag): - """Build a docker container. + """Build a Docker container. Need run inside executor. """ @@ -329,7 +329,7 @@ class DockerAddon(DockerInterface): # Update meta data self._meta = image.attrs - except (docker.errors.DockerException) as err: + except docker.errors.DockerException as err: _LOGGER.error("Can't build %s:%s: %s", self.image, tag, err) return False @@ -403,7 +403,7 @@ class DockerAddon(DockerInterface): return False try: - # load needed docker objects + # Load needed docker objects container = self.sys_docker.containers.get(self.name) socket = container.attach_socket(params={'stdin': 1, 'stream': 1}) except docker.errors.DockerException as err: @@ -411,7 +411,7 @@ class DockerAddon(DockerInterface): return False try: - # write to stdin + # Write to stdin data += b"\n" os.write(socket.fileno(), data) socket.close() diff --git a/hassio/docker/hassos_cli.py b/hassio/docker/hassos_cli.py index e76e9d8a8..4b2bccbb3 100644 --- a/hassio/docker/hassos_cli.py +++ b/hassio/docker/hassos_cli.py @@ -10,11 +10,11 @@ _LOGGER = logging.getLogger(__name__) class DockerHassOSCli(DockerInterface, CoreSysAttributes): - """Docker hassio wrapper for HassOS Cli.""" + """Docker Hass.io wrapper for HassOS Cli.""" @property def image(self): - """Return name of HassOS cli image.""" + """Return name of HassOS CLI image.""" return f"homeassistant/{self.sys_arch}-hassio-cli" def _stop(self): @@ -22,16 +22,16 @@ class DockerHassOSCli(DockerInterface, CoreSysAttributes): return True def _attach(self): - """Attach to running docker container. + """Attach to running Docker container. Need run inside executor. """ try: image = self.sys_docker.images.get(self.image) except docker.errors.DockerException: - _LOGGER.warning("Can't find a HassOS cli %s", self.image) + _LOGGER.warning("Can't find a HassOS CLI %s", self.image) else: self._meta = image.attrs - _LOGGER.info("Found HassOS cli %s with version %s", + _LOGGER.info("Found HassOS CLI %s with version %s", self.image, self.version) diff --git a/hassio/docker/homeassistant.py b/hassio/docker/homeassistant.py index e912a674f..e219fb163 100644 --- a/hassio/docker/homeassistant.py +++ b/hassio/docker/homeassistant.py @@ -1,4 +1,4 @@ -"""Init file for HassIO docker object.""" +"""Init file for Hass.io Docker object.""" import logging import docker @@ -12,35 +12,35 @@ HASS_DOCKER_NAME = 'homeassistant' class DockerHomeAssistant(DockerInterface): - """Docker hassio wrapper for HomeAssistant.""" + """Docker Hass.io wrapper for Home Assistant.""" @property def machine(self): - """Return machine of Home-Assistant docker image.""" + """Return machine of Home Assistant Docker image.""" if self._meta and LABEL_MACHINE in self._meta['Config']['Labels']: return self._meta['Config']['Labels'][LABEL_MACHINE] return None @property def image(self): - """Return name of docker image.""" + """Return name of Docker image.""" return self.sys_homeassistant.image @property def name(self): - """Return name of docker container.""" + """Return name of Docker container.""" return HASS_DOCKER_NAME @property def devices(self): - """Create list of special device to map into docker.""" + """Create list of special device to map into Docker.""" devices = [] for device in self.sys_hardware.serial_devices: devices.append(f"{device}:{device}:rwm") return devices or None def _run(self): - """Run docker image. + """Run Docker image. Need run inside executor. """ @@ -108,7 +108,7 @@ class DockerHomeAssistant(DockerInterface): ) def is_initialize(self): - """Return True if docker container exists.""" + """Return True if Docker container exists.""" return self.sys_run_in_executor(self._is_initialize) def _is_initialize(self): diff --git a/hassio/docker/interface.py b/hassio/docker/interface.py index ae3f2e0be..533ab0f69 100644 --- a/hassio/docker/interface.py +++ b/hassio/docker/interface.py @@ -1,4 +1,4 @@ -"""Interface class for HassIO docker object.""" +"""Interface class for Hass.io Docker object.""" import asyncio from contextlib import suppress import logging @@ -14,27 +14,27 @@ _LOGGER = logging.getLogger(__name__) class DockerInterface(CoreSysAttributes): - """Docker hassio interface.""" + """Docker Hass.io interface.""" def __init__(self, coresys): - """Initialize docker base wrapper.""" + """Initialize Docker base wrapper.""" self.coresys = coresys self._meta = None self.lock = asyncio.Lock(loop=coresys.loop) @property def timeout(self): - """Return timeout for docker actions.""" + """Return timeout for Docker actions.""" return 30 @property def name(self): - """Return name of docker container.""" + """Return name of Docker container.""" return None @property def meta_config(self): - """Return meta data of config for container/image.""" + """Return meta data of configuration for container/image.""" if not self._meta: return {} return self._meta.get('Config', {}) @@ -46,17 +46,17 @@ class DockerInterface(CoreSysAttributes): @property def image(self): - """Return name of docker image.""" + """Return name of Docker image.""" return self.meta_config.get('Image') @property def version(self): - """Return version of docker image.""" + """Return version of Docker image.""" return self.meta_labels.get(LABEL_VERSION) @property def arch(self): - """Return arch of docker image.""" + """Return arch of Docker image.""" return self.meta_labels.get(LABEL_ARCH) @property @@ -70,7 +70,7 @@ class DockerInterface(CoreSysAttributes): return self.sys_run_in_executor(self._install, tag) def _install(self, tag): - """Pull docker image. + """Pull Docker image. Need run inside executor. """ @@ -88,11 +88,11 @@ class DockerInterface(CoreSysAttributes): return True def exists(self): - """Return True if docker image exists in local repo.""" + """Return True if Docker image exists in local repository.""" return self.sys_run_in_executor(self._exists) def _exists(self): - """Return True if docker image exists in local repo. + """Return True if Docker image exists in local repository. Need run inside executor. """ @@ -105,14 +105,14 @@ class DockerInterface(CoreSysAttributes): return True def is_running(self): - """Return True if docker is Running. + """Return True if Docker is running. Return a Future. """ return self.sys_run_in_executor(self._is_running) def _is_running(self): - """Return True if docker is Running. + """Return True if Docker is running. Need run inside executor. """ @@ -134,7 +134,7 @@ class DockerInterface(CoreSysAttributes): @process_lock def attach(self): - """Attach to running docker container.""" + """Attach to running Docker container.""" return self.sys_run_in_executor(self._attach) def _attach(self): @@ -157,11 +157,11 @@ class DockerInterface(CoreSysAttributes): @process_lock def run(self): - """Run docker image.""" + """Run Docker image.""" return self.sys_run_in_executor(self._run) def _run(self): - """Run docker image. + """Run Docker image. Need run inside executor. """ @@ -169,7 +169,7 @@ class DockerInterface(CoreSysAttributes): @process_lock def stop(self): - """Stop/remove docker container.""" + """Stop/remove Docker container.""" return self.sys_run_in_executor(self._stop) def _stop(self): @@ -183,19 +183,19 @@ class DockerInterface(CoreSysAttributes): return False if container.status == 'running': - _LOGGER.info("Stop %s docker application", self.image) + _LOGGER.info("Stop %s Docker application", self.image) with suppress(docker.errors.DockerException): container.stop(timeout=self.timeout) with suppress(docker.errors.DockerException): - _LOGGER.info("Clean %s docker application", self.image) + _LOGGER.info("Clean %s Docker application", self.image) container.remove(force=True) return True @process_lock def remove(self): - """Remove docker images.""" + """Remove Docker images.""" return self.sys_run_in_executor(self._remove) def _remove(self): @@ -203,11 +203,11 @@ class DockerInterface(CoreSysAttributes): Need run inside executor. """ - # cleanup container + # Cleanup container self._stop() _LOGGER.info( - "Remove docker %s with latest and %s", self.image, self.version) + "Remove Docker %s with latest and %s", self.image, self.version) try: with suppress(docker.errors.ImageNotFound): @@ -227,7 +227,7 @@ class DockerInterface(CoreSysAttributes): @process_lock def update(self, tag): - """Update a docker image.""" + """Update a Docker image.""" return self.sys_run_in_executor(self._update, tag) def _update(self, tag): @@ -236,27 +236,27 @@ class DockerInterface(CoreSysAttributes): Need run inside executor. """ _LOGGER.info( - "Update docker %s with %s:%s", self.version, self.image, tag) + "Update Docker %s with %s:%s", self.version, self.image, tag) - # update docker image + # Update docker image if not self._install(tag): return False - # stop container & cleanup + # Stop container & cleanup self._stop() self._cleanup() return True def logs(self): - """Return docker logs of container. + """Return Docker logs of container. Return a Future. """ return self.sys_run_in_executor(self._logs) def _logs(self): - """Return docker logs of container. + """Return Docker logs of container. Need run inside executor. """ @@ -268,7 +268,7 @@ class DockerInterface(CoreSysAttributes): try: return container.logs(tail=100, stdout=True, stderr=True) except docker.errors.DockerException as err: - _LOGGER.warning("Can't grap logs from %s: %s", self.image, err) + _LOGGER.warning("Can't grep logs from %s: %s", self.image, err) @process_lock def cleanup(self): @@ -291,7 +291,7 @@ class DockerInterface(CoreSysAttributes): continue with suppress(docker.errors.DockerException): - _LOGGER.info("Cleanup docker images: %s", image.tags) + _LOGGER.info("Cleanup Docker images: %s", image.tags) self.sys_docker.images.remove(image.id, force=True) return True diff --git a/hassio/docker/network.py b/hassio/docker/network.py index 08120c3ce..e46fa09a9 100644 --- a/hassio/docker/network.py +++ b/hassio/docker/network.py @@ -1,4 +1,4 @@ -"""Internal network manager for HassIO.""" +"""Internal network manager for Hass.io.""" import logging import docker @@ -9,13 +9,13 @@ _LOGGER = logging.getLogger(__name__) class DockerNetwork: - """Internal HassIO Network. + """Internal Hass.io Network. This class is not AsyncIO safe! """ def __init__(self, dock): - """Initialize internal hassio network.""" + """Initialize internal Hass.io network.""" self.docker = dock self.network = self._get_network() @@ -44,7 +44,7 @@ class DockerNetwork: try: return self.docker.networks.get(DOCKER_NETWORK) except docker.errors.NotFound: - _LOGGER.info("Can't find HassIO network, create new network") + _LOGGER.info("Can't find Hass.io network, create new network") ipam_pool = docker.types.IPAMPool( subnet=str(DOCKER_NETWORK_MASK), @@ -61,7 +61,7 @@ class DockerNetwork: }) def attach_container(self, container, alias=None, ipv4=None): - """Attach container to hassio network. + """Attach container to Hass.io network. Need run inside executor. """ @@ -77,7 +77,7 @@ class DockerNetwork: return True def detach_default_bridge(self, container): - """Detach default docker bridge. + """Detach default Docker bridge. Need run inside executor. """ diff --git a/hassio/docker/stats.py b/hassio/docker/stats.py index 300cfccd0..5901b42d2 100644 --- a/hassio/docker/stats.py +++ b/hassio/docker/stats.py @@ -1,4 +1,4 @@ -"""Calc & represent docker stats data.""" +"""Calc and represent docker stats data.""" from contextlib import suppress @@ -6,7 +6,7 @@ class DockerStats: """Hold stats data from container inside.""" def __init__(self, stats): - """Initialize docker stats.""" + """Initialize Docker stats.""" self._cpu = 0.0 self._network_rx = 0 self._network_tx = 0 diff --git a/hassio/docker/supervisor.py b/hassio/docker/supervisor.py index 66b6d4d94..fe327d05d 100644 --- a/hassio/docker/supervisor.py +++ b/hassio/docker/supervisor.py @@ -1,4 +1,4 @@ -"""Init file for HassIO docker object.""" +"""Init file for Hass.io Docker object.""" import logging import os @@ -11,11 +11,11 @@ _LOGGER = logging.getLogger(__name__) class DockerSupervisor(DockerInterface, CoreSysAttributes): - """Docker hassio wrapper for Supervisor.""" + """Docker Hass.io wrapper for Supervisor.""" @property def name(self): - """Return name of docker container.""" + """Return name of Docker container.""" return os.environ['SUPERVISOR_NAME'] def _attach(self): @@ -29,14 +29,14 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes): return False self._meta = container.attrs - _LOGGER.info("Attach to supervisor %s with version %s", + _LOGGER.info("Attach to Supervisor %s with version %s", self.image, self.version) - # if already attach + # If already attach if container in self.sys_docker.network.containers: return True - # attach to network + # Attach to network return self.sys_docker.network.attach_container( container, alias=['hassio'], ipv4=self.sys_docker.network.supervisor) diff --git a/hassio/host/__init__.py b/hassio/host/__init__.py index b64e89664..c9cc606fe 100644 --- a/hassio/host/__init__.py +++ b/hassio/host/__init__.py @@ -1,4 +1,4 @@ -"""Host function like audio/dbus/systemd.""" +"""Host function like audio, D-Bus or systemd.""" from contextlib import suppress import logging @@ -35,7 +35,7 @@ class HostManager(CoreSysAttributes): @property def apparmor(self): - """Return host apparmor handler.""" + """Return host AppArmor handler.""" return self._apparmor @property diff --git a/hassio/host/alsa.py b/hassio/host/alsa.py index 3de03f29f..e6210e1a6 100644 --- a/hassio/host/alsa.py +++ b/hassio/host/alsa.py @@ -1,4 +1,4 @@ -"""Host Audio-support.""" +"""Host Audio support.""" import logging import json from pathlib import Path @@ -19,7 +19,7 @@ class AlsaAudio(CoreSysAttributes): """Handle Audio ALSA host data.""" def __init__(self, coresys): - """Initialize Alsa audio system.""" + """Initialize ALSA audio system.""" self.coresys = coresys self._data = { ATTR_INPUT: {}, diff --git a/hassio/host/apparmor.py b/hassio/host/apparmor.py index 2d8193b5f..51cfa2751 100644 --- a/hassio/host/apparmor.py +++ b/hassio/host/apparmor.py @@ -13,7 +13,7 @@ SYSTEMD_SERVICES = {'hassos-apparmor.service', 'hassio-apparmor.service'} class AppArmorControl(CoreSysAttributes): - """Handle host apparmor controls.""" + """Handle host AppArmor controls.""" def __init__(self, coresys): """Initialize host power handling.""" @@ -23,7 +23,7 @@ class AppArmorControl(CoreSysAttributes): @property def available(self): - """Return True if AppArmor is availabe on host.""" + """Return True if AppArmor is available on host.""" return self._service is not None def exists(self, profile): @@ -62,12 +62,12 @@ class AppArmorControl(CoreSysAttributes): if self.available: await self._reload_service() else: - _LOGGER.info("AppArmor is not enabled on Host") + _LOGGER.info("AppArmor is not enabled on host") async def load_profile(self, profile_name, profile_file): """Load/Update a new/exists profile into AppArmor.""" if not validate_profile(profile_name, profile_file): - _LOGGER.error("profile is not valid with name %s", profile_name) + _LOGGER.error("Profile is not valid with name %s", profile_name) raise HostAppArmorError() # Copy to AppArmor folder diff --git a/hassio/host/control.py b/hassio/host/control.py index ab4d994ff..5ae94aa02 100644 --- a/hassio/host/control.py +++ b/hassio/host/control.py @@ -24,7 +24,7 @@ class SystemControl(CoreSysAttributes): if flag == HOSTNAME and self.sys_dbus.hostname.is_connected: return - _LOGGER.error("No %s dbus connection available", flag) + _LOGGER.error("No %s D-Bus connection available", flag) raise HostNotSupportedError() async def reboot(self): @@ -51,6 +51,6 @@ class SystemControl(CoreSysAttributes): """Set local a new Hostname.""" self._check_dbus(HOSTNAME) - _LOGGER.info("Set Hostname %s", hostname) + _LOGGER.info("Set hostname %s", hostname) await self.sys_dbus.hostname.set_static_hostname(hostname) await self.sys_host.info.update() diff --git a/hassio/host/info.py b/hassio/host/info.py index b21eb67cc..60e2945fd 100644 --- a/hassio/host/info.py +++ b/hassio/host/info.py @@ -48,7 +48,7 @@ class InfoCenter(CoreSysAttributes): async def update(self): """Update properties over dbus.""" if not self.sys_dbus.hostname.is_connected: - _LOGGER.error("No hostname dbus connection available") + _LOGGER.error("No hostname D-Bus connection available") raise HostNotSupportedError() _LOGGER.info("Update local host information") diff --git a/hassio/host/services.py b/hassio/host/services.py index d200ce510..bc4667403 100644 --- a/hassio/host/services.py +++ b/hassio/host/services.py @@ -95,5 +95,5 @@ class ServiceInfo: @staticmethod def read_from(unit): - """Parse data from dbus into this object.""" + """Parse data from D-Bus into this object.""" return ServiceInfo(unit[0], unit[1], unit[3]) diff --git a/hassio/misc/hardware.py b/hassio/misc/hardware.py index 3f60e739d..eeebf8160 100644 --- a/hassio/misc/hardware.py +++ b/hassio/misc/hardware.py @@ -24,7 +24,7 @@ RE_TTY = re.compile(r"tty[A-Z]+") class Hardware: - """Represent an interface to procfs, sysfs and udev.""" + """Representation of an interface to procfs, sysfs and udev.""" def __init__(self): """Init hardware object.""" diff --git a/hassio/misc/scheduler.py b/hassio/misc/scheduler.py index 44401422c..e87c79bf8 100644 --- a/hassio/misc/scheduler.py +++ b/hassio/misc/scheduler.py @@ -1,4 +1,4 @@ -"""Schedule for HassIO.""" +"""Schedule for Hass.io.""" import logging from datetime import date, datetime, time, timedelta @@ -11,7 +11,7 @@ TASK = 'task' class Scheduler: - """Schedule task inside HassIO.""" + """Schedule task inside Hass.io.""" def __init__(self, loop): """Initialize task schedule.""" @@ -22,18 +22,18 @@ class Scheduler: def register_task(self, coro_callback, interval, repeat=True): """Schedule a coroutine. - The coroutien need to be a callback without arguments. + The coroutine need to be a callback without arguments. """ task_id = hash(coro_callback) - # generate data + # Generate data opts = { CALL: coro_callback, INTERVAL: interval, REPEAT: repeat, } - # schedule task + # Schedule task self._data[task_id] = opts self._schedule_task(interval, task_id) @@ -60,7 +60,7 @@ class Scheduler: tomorrow = datetime.combine( date.today() + timedelta(days=1), interval) - # check if we run it today or next day + # Check if we run it today or next day if today > datetime.today(): calc = today else: @@ -68,7 +68,7 @@ class Scheduler: job = self.loop.call_at(calc.timestamp(), self._run_task, task_id) else: - _LOGGER.fatal("Unknow interval %s (type: %s) for scheduler %s", + _LOGGER.fatal("Unknown interval %s (type: %s) for scheduler %s", interval, type(interval), task_id) # Store job diff --git a/hassio/services/__init__.py b/hassio/services/__init__.py index 9d5abe4be..33c056725 100644 --- a/hassio/services/__init__.py +++ b/hassio/services/__init__.py @@ -1,5 +1,4 @@ """Handle internal services discovery.""" - from .discovery import Discovery # noqa from .mqtt import MQTTService from .data import ServicesData diff --git a/hassio/services/data.py b/hassio/services/data.py index 525ff5c46..c2fe2d630 100644 --- a/hassio/services/data.py +++ b/hassio/services/data.py @@ -14,10 +14,10 @@ class ServicesData(JsonConfig): @property def discovery(self): - """Return discovery data for home-assistant.""" + """Return discovery data for Home Assistant.""" return self._data[ATTR_DISCOVERY] @property def mqtt(self): - """Return settings for mqtt service.""" + """Return settings for MQTT service.""" return self._data[SERVICE_MQTT] diff --git a/hassio/services/discovery.py b/hassio/services/discovery.py index dd43505ab..51b7b96a9 100644 --- a/hassio/services/discovery.py +++ b/hassio/services/discovery.py @@ -1,4 +1,4 @@ -"""Handle discover message for Home-Assistant.""" +"""Handle discover message for Home Assistant.""" import logging from uuid import uuid4 @@ -12,7 +12,7 @@ EVENT_DISCOVERY_DEL = 'hassio_discovery_del' class Discovery(CoreSysAttributes): - """Home-Assistant Discovery handler.""" + """Home Assistant Discovery handler.""" def __init__(self, coresys): """Initialize discovery handler.""" @@ -53,29 +53,29 @@ class Discovery(CoreSysAttributes): return self.message_obj.values() def send(self, provider, component, platform=None, config=None): - """Send a discovery message to Home-Assistant.""" + """Send a discovery message to Home Assistant.""" message = Message(provider, component, platform, config) # Already exists? for exists_message in self.message_obj: if exists_message == message: - _LOGGER.warning("Found douplicate discovery message from %s", + _LOGGER.warning("Found duplicate discovery message from %s", provider) return exists_message - _LOGGER.info("Send discovery to Home-Assistant %s/%s from %s", + _LOGGER.info("Send discovery to Home Assistant %s/%s from %s", component, platform, provider) self.message_obj[message.uuid] = message self.save() - # send event to Home-Assistant + # Send event to Home Assistant self.sys_create_task(self.sys_homeassistant.send_event( EVENT_DISCOVERY_ADD, {ATTR_UUID: message.uuid})) return message def remove(self, message): - """Remove a discovery message from Home-Assistant.""" + """Remove a discovery message from Home Assistant.""" self.message_obj.pop(message.uuid, None) self.save() diff --git a/hassio/services/mqtt.py b/hassio/services/mqtt.py index 6e2e519c8..55f41c232 100644 --- a/hassio/services/mqtt.py +++ b/hassio/services/mqtt.py @@ -1,4 +1,4 @@ -"""Provide MQTT Service.""" +"""Provide the MQTT Service.""" import logging from .interface import ServiceInterface @@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) class MQTTService(ServiceInterface): - """Provide mqtt services.""" + """Provide MQTT services.""" @property def slug(self): @@ -35,7 +35,7 @@ class MQTTService(ServiceInterface): @property def hass_config(self): - """Return Home-Assistant mqtt config.""" + """Return Home Assistant MQTT config.""" if not self.enabled: return None @@ -54,18 +54,18 @@ class MQTTService(ServiceInterface): def set_service_data(self, provider, data): """Write the data into service object.""" if self.enabled: - _LOGGER.error("It is already a mqtt in use from %s", self.provider) + _LOGGER.error("It is already a MQTT in use from %s", self.provider) return False self._data.update(data) self._data[ATTR_PROVIDER] = provider if provider == 'homeassistant': - _LOGGER.info("Use mqtt settings from Home-Assistant") + _LOGGER.info("Use MQTT settings from Home Assistant") self.save() return True - # discover mqtt to homeassistant + # Discover MQTT to Home Assistant message = self.sys_discovery.send( provider, SERVICE_MQTT, None, self.hass_config) @@ -76,7 +76,7 @@ class MQTTService(ServiceInterface): def del_service_data(self, provider): """Remove the data from service object.""" if not self.enabled: - _LOGGER.warning("Can't remove not exists services.") + _LOGGER.warning("Can't remove not exists services") return False discovery_id = self._data.get(ATTR_DISCOVERY_ID) diff --git a/hassio/services/validate.py b/hassio/services/validate.py index 4a0567d44..14a4da1b8 100644 --- a/hassio/services/validate.py +++ b/hassio/services/validate.py @@ -1,5 +1,4 @@ """Validate services schema.""" - import voluptuous as vol from ..const import ( diff --git a/hassio/snapshots/snapshot.py b/hassio/snapshots/snapshot.py index 11a6b5e68..7fc3aff0a 100644 --- a/hassio/snapshots/snapshot.py +++ b/hassio/snapshots/snapshot.py @@ -1,4 +1,4 @@ -"""Represent a snapshot file.""" +"""Representation of a snapshot file.""" import asyncio from base64 import b64decode, b64encode import json @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) class Snapshot(CoreSysAttributes): - """A signle hassio snapshot.""" + """A single Hass.io snapshot.""" def __init__(self, coresys, tar_file): """Initialize a snapshot.""" @@ -72,7 +72,7 @@ class Snapshot(CoreSysAttributes): @property def addon_list(self): - """Return a list of addons slugs.""" + """Return a list of add-ons slugs.""" return [addon_data[ATTR_SLUG] for addon_data in self.addons] @property @@ -92,12 +92,12 @@ class Snapshot(CoreSysAttributes): @property def homeassistant_version(self): - """Return snapshot homeassistant version.""" + """Return snapshot Home Assistant version.""" return self._data[ATTR_HOMEASSISTANT].get(ATTR_VERSION) @property def homeassistant(self): - """Return snapshot homeassistant data.""" + """Return snapshot Home Assistant data.""" return self._data[ATTR_HOMEASSISTANT] @property @@ -119,7 +119,7 @@ class Snapshot(CoreSysAttributes): def new(self, slug, name, date, sys_type, password=None): """Initialize a new snapshot.""" - # init metadata + # Init metadata self._data[ATTR_SLUG] = slug self._data[ATTR_NAME] = name self._data[ATTR_DATE] = date @@ -306,16 +306,16 @@ class Snapshot(CoreSysAttributes): await asyncio.wait(tasks) async def store_folders(self, folder_list=None): - """Backup hassio data into snapshot.""" + """Backup Hass.io data into snapshot.""" folder_list = set(folder_list or ALL_FOLDERS) def _folder_save(name): - """Intenal function to snapshot a folder.""" + """Internal function to snapshot a folder.""" slug_name = name.replace("/", "_") tar_name = Path(self._tmp.name, f"{slug_name}.tar.gz") origin_dir = Path(self.sys_config.path_hassio, name) - # Check if exsits + # Check if exists if not origin_dir.is_dir(): _LOGGER.warning("Can't find snapshot folder %s", name) return @@ -338,7 +338,7 @@ class Snapshot(CoreSysAttributes): await asyncio.wait(tasks) async def restore_folders(self, folder_list=None): - """Backup hassio data into snapshot.""" + """Backup Hass.io data into snapshot.""" folder_list = set(folder_list or self.folders) def _folder_restore(name): @@ -356,7 +356,7 @@ class Snapshot(CoreSysAttributes): if origin_dir.is_dir(): remove_folder(origin_dir) - # Performe a restore + # Perform a restore try: _LOGGER.info("Restore folder %s", name) with SecureTarFile(tar_name, 'r', key=self._key) as tar_file: @@ -372,7 +372,7 @@ class Snapshot(CoreSysAttributes): await asyncio.wait(tasks) def store_homeassistant(self): - """Read all data from homeassistant object.""" + """Read all data from Home Assistant object.""" self.homeassistant[ATTR_VERSION] = self.sys_homeassistant.version self.homeassistant[ATTR_WATCHDOG] = self.sys_homeassistant.watchdog self.homeassistant[ATTR_BOOT] = self.sys_homeassistant.boot @@ -393,7 +393,7 @@ class Snapshot(CoreSysAttributes): self._encrypt_data(self.sys_homeassistant.api_password) def restore_homeassistant(self): - """Write all data to homeassistant object.""" + """Write all data to the Home Assistant object.""" self.sys_homeassistant.watchdog = self.homeassistant[ATTR_WATCHDOG] self.sys_homeassistant.boot = self.homeassistant[ATTR_BOOT] self.sys_homeassistant.wait_boot = self.homeassistant[ATTR_WAIT_BOOT] diff --git a/hassio/snapshots/utils.py b/hassio/snapshots/utils.py index 4a842256f..2f9d73bce 100644 --- a/hassio/snapshots/utils.py +++ b/hassio/snapshots/utils.py @@ -1,4 +1,4 @@ -"""Util addons functions.""" +"""Util add-on functions.""" import hashlib import shutil import re diff --git a/hassio/snapshots/validate.py b/hassio/snapshots/validate.py index 8f8e39c34..546fbe6b7 100644 --- a/hassio/snapshots/validate.py +++ b/hassio/snapshots/validate.py @@ -1,5 +1,4 @@ """Validate some things around restore.""" - import voluptuous as vol from ..const import ( From 52da7605f5498443c98f72b02b1079c07be5b112 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 24 Sep 2018 15:11:33 +0200 Subject: [PATCH 03/13] Enable Security API (#710) * Enable Security API * Update addons.py * Update proxy.py * Update __init__.py * Update security.py * Fix lint --- hassio/addons/__init__.py | 7 ------- hassio/api/addons.py | 8 -------- hassio/api/proxy.py | 8 -------- hassio/api/security.py | 16 ++++++++++++---- 4 files changed, 12 insertions(+), 27 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 50c60b443..4fb01969b 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -43,13 +43,6 @@ class AddonManager(CoreSysAttributes): """Return an add-on from slug.""" return self.addons_obj.get(addon_slug) - def from_uuid(self, uuid): - """Return an add-on from UUID.""" - for addon in self.list_addons: - if addon.is_installed and uuid == addon.uuid: - return addon - return None - def from_token(self, token): """Return an add-on from Hass.io token.""" for addon in self.list_addons: diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 2d1fb34a8..65028cd14 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -24,7 +24,6 @@ from ..const import ( CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM) from ..coresys import CoreSysAttributes from ..validate import DOCKER_PORTS, ALSA_DEVICE -from ..exceptions import APINotSupportedError _LOGGER = logging.getLogger(__name__) @@ -196,13 +195,6 @@ class APIAddons(CoreSysAttributes): async def security(self, request): """Store security options for add-on.""" addon = self._extract_addon(request) - - # Have Access - # REMOVE: don't needed anymore - if addon.slug == request[REQUEST_FROM]: - _LOGGER.error("Can't self modify his security!") - raise APINotSupportedError() - body = await api_validate(SCHEMA_SECURITY, request) if ATTR_PROTECTED in body: diff --git a/hassio/api/proxy.py b/hassio/api/proxy.py index 4e96c6918..c88042c71 100644 --- a/hassio/api/proxy.py +++ b/hassio/api/proxy.py @@ -25,10 +25,6 @@ class APIProxy(CoreSysAttributes): hassio_token = request.headers.get(HEADER_HA_ACCESS) addon = self.sys_addons.from_token(hassio_token) - # REMOVE 132 - if not addon: - addon = self.sys_addons.from_uuid(hassio_token) - if not addon: _LOGGER.warning("Unknown Home Assistant API access!") elif not addon.access_homeassistant_api: @@ -184,10 +180,6 @@ class APIProxy(CoreSysAttributes): response.get('access_token')) addon = self.sys_addons.from_token(hassio_token) - # REMOVE 132 - if not addon: - addon = self.sys_addons.from_uuid(hassio_token) - if not addon or not addon.access_homeassistant_api: _LOGGER.warning("Unauthorized WebSocket access!") await server.send_json({ diff --git a/hassio/api/security.py b/hassio/api/security.py index a729e49c8..6197c495d 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -12,6 +12,14 @@ from ..coresys import CoreSysAttributes _LOGGER = logging.getLogger(__name__) + +# Block Anytime +BLACKLIST = re.compile( + r"^(?:" + r"|/homeassistant/api/hassio/.*" + r")$" +) + # Free to call or have own security concepts NO_SECURITY_CHECK = re.compile( r"^(?:" @@ -74,6 +82,10 @@ class SecurityMiddleware(CoreSysAttributes): request_from = None hassio_token = request.headers.get(HEADER_TOKEN) + # Blacklist + if BLACKLIST.match(request.path): + raise HTTPForbidden() + # Ignore security check if NO_SECURITY_CHECK.match(request.path): _LOGGER.debug("Passthrough %s", request.path) @@ -100,9 +112,6 @@ class SecurityMiddleware(CoreSysAttributes): addon = None if hassio_token and not request_from: addon = self.sys_addons.from_token(hassio_token) - # REMOVE 132 - if not addon: - addon = self.sys_addons.from_uuid(hassio_token) # Check Add-on API access if addon and ADDONS_API_BYPASS.match(request.path): @@ -115,7 +124,6 @@ class SecurityMiddleware(CoreSysAttributes): request_from = addon.slug else: _LOGGER.warning("%s no role for %s", request.path, addon.slug) - request_from = addon.slug # REMOVE: 132 if request_from: request[REQUEST_FROM] = request_from From c91bac2527cb0dc66990caa2885ac3e21cfffb75 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 24 Sep 2018 17:03:21 +0200 Subject: [PATCH 04/13] Add log to blacklist / reduce free calls (#713) --- hassio/api/security.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hassio/api/security.py b/hassio/api/security.py index 6197c495d..f766e1cc8 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -33,9 +33,7 @@ NO_SECURITY_CHECK = re.compile( # Can called by every add-on ADDONS_API_BYPASS = re.compile( r"^(?:" - r"|/homeassistant/info" - r"|/supervisor/info" - r"|/addons(?:/self/(?!security)[^/]+)?" + r"|/addons/self/(?!security)[^/]+)?" r")$" ) @@ -44,6 +42,7 @@ ADDONS_ROLE_ACCESS = { ROLE_DEFAULT: re.compile( r"^(?:" r"|/[^/]+/info" + r"|addons" r")$" ), ROLE_HOMEASSISTANT: re.compile( @@ -84,6 +83,7 @@ class SecurityMiddleware(CoreSysAttributes): # Blacklist if BLACKLIST.match(request.path): + _LOGGER.warning("%s is blacklisted!", request.path) raise HTTPForbidden() # Ignore security check From 2f4e114f255ef5c2935e5c11b939aab12f81aa34 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 25 Sep 2018 12:51:47 +0200 Subject: [PATCH 05/13] Fix wrong regex --- hassio/api/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/api/security.py b/hassio/api/security.py index f766e1cc8..8476c8daa 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -42,7 +42,7 @@ ADDONS_ROLE_ACCESS = { ROLE_DEFAULT: re.compile( r"^(?:" r"|/[^/]+/info" - r"|addons" + r"|/addons" r")$" ), ROLE_HOMEASSISTANT: re.compile( From f2a5512bbfb1ad7e231d4d03799bbf4cf1f957b1 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 25 Sep 2018 13:46:48 +0200 Subject: [PATCH 06/13] Fix not exists label bug (#717) --- hassio/docker/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/docker/interface.py b/hassio/docker/interface.py index 533ab0f69..febee39ab 100644 --- a/hassio/docker/interface.py +++ b/hassio/docker/interface.py @@ -42,7 +42,7 @@ class DockerInterface(CoreSysAttributes): @property def meta_labels(self): """Return meta data of labels for container/image.""" - return self.meta_config.get('Labels', {}) + return self.meta_config.get('Labels') or {} @property def image(self): From 61eefea358892d09951c1ea4375053abf8d18428 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 26 Sep 2018 11:39:45 +0200 Subject: [PATCH 07/13] Add version endpoint (#718) * Add version endpoint * Update API.md * Update const.py * Create version.py * Update __init__.py * Update security.py * Update version.py --- API.md | 14 ++++++++++++++ hassio/api/__init__.py | 11 +++++++++++ hassio/api/security.py | 1 + hassio/api/version.py | 26 ++++++++++++++++++++++++++ hassio/const.py | 1 + 5 files changed, 53 insertions(+) create mode 100644 hassio/api/version.py diff --git a/API.md b/API.md index 4a9955a03..d5fb57bcd 100644 --- a/API.md +++ b/API.md @@ -660,3 +660,17 @@ This service performs an auto discovery to Home-Assistant. ``` - DEL `/services/mqtt` + +### Misc + +- GET `/version` +```json +{ + "supervisor": "version", + "homeassistant": "version", + "hassos": "null|version", + "machine": "type", + "arch": "arch", + "channel": "stable|beta|dev" +} +``` diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index a9b6ba9de..637290f93 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -14,6 +14,7 @@ from .proxy import APIProxy from .supervisor import APISupervisor from .snapshots import APISnapshots from .services import APIServices +from .version import APIVersion from .security import SecurityMiddleware from ..coresys import CoreSysAttributes @@ -47,6 +48,7 @@ class RestAPI(CoreSysAttributes): self._register_snapshots() self._register_discovery() self._register_services() + self._register_version() def _register_host(self): """Register hostcontrol functions.""" @@ -90,6 +92,15 @@ class RestAPI(CoreSysAttributes): web.get('/hardware/audio', api_hardware.audio), ]) + def _register_version(self): + """Register version functions.""" + api_version = APIVersion() + api_version.coresys = self.coresys + + self.webapp.add_routes([ + web.get('/version', api_version.info), + ]) + def _register_supervisor(self): """Register Supervisor functions.""" api_supervisor = APISupervisor() diff --git a/hassio/api/security.py b/hassio/api/security.py index 8476c8daa..90982acaf 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -34,6 +34,7 @@ NO_SECURITY_CHECK = re.compile( ADDONS_API_BYPASS = re.compile( r"^(?:" r"|/addons/self/(?!security)[^/]+)?" + r"|/version" r")$" ) diff --git a/hassio/api/version.py b/hassio/api/version.py new file mode 100644 index 000000000..4757f3119 --- /dev/null +++ b/hassio/api/version.py @@ -0,0 +1,26 @@ +"""Init file for Hass.io version RESTful API.""" +import logging + +from .utils import api_process +from ..const import ( + ATTR_HOMEASSISTANT, ATTR_SUPERVISOR, ATTR_MACHINE, ATTR_ARCH, ATTR_HASSOS, + ATTR_CHANNEL) +from ..coresys import CoreSysAttributes + +_LOGGER = logging.getLogger(__name__) + + +class APIVersion(CoreSysAttributes): + """Handle RESTful API for version functions.""" + + @api_process + async def info(self, request): + """Show version info.""" + return { + ATTR_SUPERVISOR: self.sys_supervisor.version, + ATTR_HOMEASSISTANT: self.sys_homeassistant.version, + ATTR_HASSOS: self.sys_hassos.version, + ATTR_MACHINE: self.sys_machine, + ATTR_ARCH: self.sys_arch, + ATTR_CHANNEL: self.sys_channel, + } diff --git a/hassio/const.py b/hassio/const.py index b3ef7a5da..0ed613821 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -185,6 +185,7 @@ ATTR_FULL_ACCESS = 'full_access' ATTR_PROTECTED = 'protected' ATTR_RATING = 'rating' ATTR_HASSIO_ROLE = 'hassio_role' +ATTR_SUPERVISOR = 'supervisor' SERVICE_MQTT = 'mqtt' From 4a9dcb540ef47e466da1d153a75a6c6d6e71154e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 27 Sep 2018 14:35:40 +0200 Subject: [PATCH 08/13] Add support for long live token (#719) * Add support for long live token * Update proxy.py --- hassio/api/proxy.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/hassio/api/proxy.py b/hassio/api/proxy.py index c88042c71..af6d38af6 100644 --- a/hassio/api/proxy.py +++ b/hassio/api/proxy.py @@ -7,7 +7,7 @@ import aiohttp from aiohttp import web from aiohttp.web_exceptions import ( HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized) -from aiohttp.hdrs import CONTENT_TYPE +from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION import async_timeout from ..const import HEADER_HA_ACCESS @@ -22,9 +22,13 @@ class APIProxy(CoreSysAttributes): def _check_access(self, request): """Check the Hass.io token.""" - hassio_token = request.headers.get(HEADER_HA_ACCESS) - addon = self.sys_addons.from_token(hassio_token) + if AUTHORIZATION in request.headers: + bearer = request.headers[AUTHORIZATION] + hassio_token = bearer.split(' ')[-1] + else: + hassio_token = request.headers.get(HEADER_HA_ACCESS) + addon = self.sys_addons.from_token(hassio_token) if not addon: _LOGGER.warning("Unknown Home Assistant API access!") elif not addon.access_homeassistant_api: From 4ef8c9d633e61ea2560d154af69bdca1491019fa Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Sep 2018 14:34:43 +0200 Subject: [PATCH 09/13] Change API for new UI & Add machine support (#720) * Change API for new UI * Update API.md * Update validate.py * Update addon.py * Update API.md * Update addons.py * fix lint * Update security.py * Update version.py * Update security.py * Update security.py --- API.md | 5 ++++- hassio/addons/addon.py | 20 ++++++++++++++++++-- hassio/addons/validate.py | 8 ++++++++ hassio/api/addons.py | 7 +++++-- hassio/api/security.py | 10 +++++----- hassio/api/version.py | 2 +- 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/API.md b/API.md index d5fb57bcd..2138ff022 100644 --- a/API.md +++ b/API.md @@ -428,11 +428,11 @@ Get all available addons. "name": "xy bla", "slug": "xy", "description": "description", - "arch": ["armhf", "aarch64", "i386", "amd64"], "repository": "core|local|REP_ID", "version": "LAST_VERSION", "installed": "none|INSTALL_VERSION", "detached": "bool", + "available": "bool", "build": "bool", "url": "null|url", "icon": "bool", @@ -463,6 +463,9 @@ Get all available addons. "auto_update": "bool", "url": "null|url of addon", "detached": "bool", + "available": "bool", + "arch": ["armhf", "aarch64", "i386", "amd64"], + "machine": "[raspberrypi2, tinker]", "repository": "12345678|null", "version": "null|VERSION_INSTALLED", "last_version": "LAST_VERSION", diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 22299143a..e46a6db0d 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -13,7 +13,8 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from .validate import ( - validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE) + validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE, + MACHINE_ALL) from .utils import check_installed, remove_data from ..const import ( ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP, @@ -27,6 +28,7 @@ from ..const import ( ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE, + ATTR_MACHINE, SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT) from ..coresys import CoreSysAttributes from ..docker.addon import DockerAddon @@ -82,6 +84,15 @@ class Addon(CoreSysAttributes): """Return True if add-on is detached.""" return self._id not in self._data.cache + @property + def available(self): + """Return True if this add-on is available on this platform.""" + if self.sys_arch not in self.supported_arch: + return False + if self.sys_machine not in self.supported_machine: + return False + return True + @property def version_installed(self): """Return installed version.""" @@ -468,6 +479,11 @@ class Addon(CoreSysAttributes): """Return list of supported arch.""" return self._mesh[ATTR_ARCH] + @property + def supported_machine(self): + """Return list of supported machine.""" + return self._mesh.get(ATTR_MACHINE) or MACHINE_ALL + @property def image(self): """Return image name of add-on.""" @@ -645,7 +661,7 @@ class Addon(CoreSysAttributes): async def install(self): """Install an add-on.""" - if self.sys_arch not in self.supported_arch: + if not self.available: _LOGGER.error( "Add-on %s not supported on %s", self._id, self.sys_arch) return False diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 44695309f..8ef12eb26 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -20,6 +20,7 @@ from ..const import ( ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED, ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE, + ATTR_MACHINE, PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO, PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE, PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, @@ -57,6 +58,12 @@ ARCH_ALL = [ ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386 ] +MACHINE_ALL = [ + 'intel-nuc', 'qemux86', 'qemux86-64', 'qemuarm', 'qemuarm-64', + 'raspberrypi', 'raspberrypi2', 'raspberrypi3', 'raspberrypi3-64', + 'odroid-cu2', 'odroid-xu', +] + STARTUP_ALL = [ STARTUP_ONCE, STARTUP_INITIALIZE, STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION @@ -105,6 +112,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Required(ATTR_DESCRIPTON): vol.Coerce(str), vol.Optional(ATTR_URL): vol.Url(), vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)], + vol.Optional(ATTR_MACHINE): [vol.In(MACHINE_ALL)], vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.In(STARTUP_ALL)), vol.Required(ATTR_BOOT): diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 65028cd14..947751c26 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -20,7 +20,7 @@ from ..const import ( ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID, - ATTR_HASSIO_ROLE, + ATTR_HASSIO_ROLE, ATTR_MACHINE, ATTR_AVAILABLE, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM) from ..coresys import CoreSysAttributes from ..validate import DOCKER_PORTS, ALSA_DEVICE @@ -85,7 +85,7 @@ class APIAddons(CoreSysAttributes): ATTR_DESCRIPTON: addon.description, ATTR_VERSION: addon.last_version, ATTR_INSTALLED: addon.version_installed, - ATTR_ARCH: addon.supported_arch, + ATTR_AVAILABLE: addon.available, ATTR_DETACHED: addon.is_detached, ATTR_REPOSITORY: addon.repository, ATTR_BUILD: addon.need_build, @@ -134,8 +134,11 @@ class APIAddons(CoreSysAttributes): ATTR_RATING: rating_security(addon), ATTR_BOOT: addon.boot, ATTR_OPTIONS: addon.options, + ATTR_ARCH: addon.supported_arch, + ATTR_MACHINE: addon.supported_machine, ATTR_URL: addon.url, ATTR_DETACHED: addon.is_detached, + ATTR_AVAILABLE: addon.available, ATTR_BUILD: addon.need_build, ATTR_NETWORK: addon.ports, ATTR_HOST_NETWORK: addon.host_network, diff --git a/hassio/api/security.py b/hassio/api/security.py index 90982acaf..1462fb0b5 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -33,7 +33,7 @@ NO_SECURITY_CHECK = re.compile( # Can called by every add-on ADDONS_API_BYPASS = re.compile( r"^(?:" - r"|/addons/self/(?!security)[^/]+)?" + r"|/addons/self/(?!security)[^/]+" r"|/version" r")$" ) @@ -58,13 +58,13 @@ ADDONS_ROLE_ACCESS = { r"|/hardware/.+" r"|/hassos/.+" r"|/supervisor/.+" - r"|/addons/.+/(?!security|options).+" - r"|/addons(?:/self/(?!security).+)" + r"|/addons/[^/]+/(?!security|options).+" + r"|/addons(?:/self/(?!security).+)?" r"|/snapshots.*" r")$" ), ROLE_ADMIN: re.compile( - r".+" + r".*" ), } @@ -130,5 +130,5 @@ class SecurityMiddleware(CoreSysAttributes): request[REQUEST_FROM] = request_from return await handler(request) - _LOGGER.warning("Invalid token for access %s", request.path) + _LOGGER.error("Invalid token for access %s", request.path) raise HTTPForbidden() diff --git a/hassio/api/version.py b/hassio/api/version.py index 4757f3119..7e5485dc5 100644 --- a/hassio/api/version.py +++ b/hassio/api/version.py @@ -22,5 +22,5 @@ class APIVersion(CoreSysAttributes): ATTR_HASSOS: self.sys_hassos.version, ATTR_MACHINE: self.sys_machine, ATTR_ARCH: self.sys_arch, - ATTR_CHANNEL: self.sys_channel, + ATTR_CHANNEL: self.sys_updater.channel, } From e5451973bd47bd5ff31f2aba8836e82f62911af9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 29 Sep 2018 19:49:08 +0200 Subject: [PATCH 10/13] Overwork Services/Discovery (#725) * Update homeassistant.py * Update validate.py * Update exceptions.py * Update services.py * Update discovery.py * fix gitignore * Fix handling for discovery * use object in ref * lock down discovery API * fix api * Design * Fix API * fix lint * fix * Fix security layer * add provide layer * fix access * change rating * fix rights * Fix API error handling * raise error * fix rights * api * fix handling * fix * debug * debug json * Fix validator * fix error * new url * fix schema --- .gitignore | 3 ++ API.md | 25 +++++----- hassio/addons/addon.py | 13 +++--- hassio/addons/repository.py | 3 +- hassio/addons/utils.py | 4 -- hassio/addons/validate.py | 6 +-- hassio/api/__init__.py | 8 ++-- hassio/api/addons.py | 30 +++++++----- hassio/api/discovery.py | 44 ++++++++++++++---- hassio/api/homeassistant.py | 5 +- hassio/api/security.py | 14 +++--- hassio/api/services.py | 38 +++++++++++---- hassio/api/snapshots.py | 3 +- hassio/api/supervisor.py | 7 +-- hassio/api/utils.py | 14 +++--- hassio/const.py | 10 ++-- hassio/exceptions.py | 18 ++++++-- hassio/homeassistant.py | 13 ------ hassio/services/discovery.py | 89 +++++++++++++++++++++--------------- hassio/services/interface.py | 15 ++++-- hassio/services/mqtt.py | 59 ++++-------------------- hassio/services/validate.py | 44 ++++++++++++++---- 22 files changed, 263 insertions(+), 202 deletions(-) diff --git a/.gitignore b/.gitignore index 26ad120ea..41395a7ea 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ ENV/ # pylint .pylint.d/ + +# VS Code +.vscode/ diff --git a/API.md b/API.md index 2138ff022..b8d719a39 100644 --- a/API.md +++ b/API.md @@ -499,8 +499,8 @@ Get all available addons. "audio": "bool", "audio_input": "null|0,0", "audio_output": "null|0,0", - "services": "null|['mqtt']", - "discovery": "null|['component/platform']" + "services_role": "['service:access']", + "discovery": "['service']" } ``` @@ -576,12 +576,13 @@ Write data to add-on stdin ### Service discovery -- GET `/services/discovery` +- GET `/discovery` ```json { "discovery": [ { - "provider": "name", + "addon": "slug", + "service": "name", "uuid": "uuid", "component": "component", "platform": "null|platform", @@ -591,10 +592,11 @@ Write data to add-on stdin } ``` -- GET `/services/discovery/{UUID}` +- GET `/discovery/{UUID}` ```json { - "provider": "name", + "addon": "slug", + "service": "name", "uuid": "uuid", "component": "component", "platform": "null|platform", @@ -602,9 +604,10 @@ Write data to add-on stdin } ``` -- POST `/services/discovery` +- POST `/discovery` ```json { + "service": "name", "component": "component", "platform": "null|platform", "config": {} @@ -618,7 +621,7 @@ return: } ``` -- DEL `/services/discovery/{UUID}` +- DEL `/discovery/{UUID}` - GET `/services` ```json @@ -627,7 +630,7 @@ return: { "slug": "name", "available": "bool", - "provider": "null|name|list" + "providers": "list" } ] } @@ -635,12 +638,10 @@ return: #### MQTT -This service performs an auto discovery to Home-Assistant. - - GET `/services/mqtt` ```json { - "provider": "name", + "addon": "name", "host": "xy", "port": "8883", "ssl": "bool", diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index e46a6db0d..7bcef24b9 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -239,24 +239,23 @@ class Addon(CoreSysAttributes): return self._mesh.get(ATTR_STARTUP) @property - def services(self): + def services_role(self): """Return dict of services with rights.""" raw_services = self._mesh.get(ATTR_SERVICES) if not raw_services: - return None + return {} - formated_services = {} + services = {} for data in raw_services: service = RE_SERVICE.match(data) - formated_services[service.group('service')] = \ - service.group('rights') or 'ro' + services[service.group('service')] = service.group('rights') - return formated_services + return services @property def discovery(self): """Return list of discoverable components/platforms.""" - return self._mesh.get(ATTR_DISCOVERY) + return self._mesh.get(ATTR_DISCOVERY, []) @property def ports(self): diff --git a/hassio/addons/repository.py b/hassio/addons/repository.py index 35fc695a0..89273c48a 100644 --- a/hassio/addons/repository.py +++ b/hassio/addons/repository.py @@ -4,6 +4,7 @@ from .utils import get_hash_from_repository from ..const import ( REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER) from ..coresys import CoreSysAttributes +from ..exceptions import APIError UNKNOWN = 'unknown' @@ -67,6 +68,6 @@ class Repository(CoreSysAttributes): def remove(self): """Remove add-on repository.""" if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL): - raise RuntimeError("Can't remove built-in repositories!") + raise APIError("Can't remove built-in repositories!") self.git.remove() diff --git a/hassio/addons/utils.py b/hassio/addons/utils.py index 26e3d3cd6..ac272ba46 100644 --- a/hassio/addons/utils.py +++ b/hassio/addons/utils.py @@ -28,10 +28,6 @@ def rating_security(addon): elif addon.apparmor == SECURITY_PROFILE: rating += 1 - # API Access - if addon.access_hassio_api or addon.access_homeassistant_api: - rating += -1 - # Privileged options if addon.privileged in (PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE): diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 8ef12eb26..9d2a2b705 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -26,13 +26,13 @@ from ..const import ( PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN) from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE +from ..services.validate import DISCOVERY_SERVICES _LOGGER = logging.getLogger(__name__) RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$") -RE_SERVICE = re.compile(r"^(?Pmqtt)(?::(?Prw|:ro))?$") -RE_DISCOVERY = re.compile(r"^(?P\w*)(?:/(?P\w*>))?$") +RE_SERVICE = re.compile(r"^(?Pmqtt):(?Pprovide|want|need)$") V_STR = 'str' V_INT = 'int' @@ -143,7 +143,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(), vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(), vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)], - vol.Optional(ATTR_DISCOVERY): [vol.Match(RE_DISCOVERY)], + vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)], vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({ vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [ diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index 637290f93..4f86a58ac 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -211,11 +211,11 @@ class RestAPI(CoreSysAttributes): api_discovery.coresys = self.coresys self.webapp.add_routes([ - web.get('/services/discovery', api_discovery.list), - web.get('/services/discovery/{uuid}', api_discovery.get_discovery), - web.delete('/services/discovery/{uuid}', + web.get('/discovery', api_discovery.list), + web.get('/discovery/{uuid}', api_discovery.get_discovery), + web.delete('/discovery/{uuid}', api_discovery.del_discovery), - web.post('/services/discovery', api_discovery.set_discovery), + web.post('/discovery', api_discovery.set_discovery), ]) def _register_panel(self): diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 947751c26..4b2ae3a21 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -55,7 +55,7 @@ class APIAddons(CoreSysAttributes): # Lookup itself if addon_slug == 'self': - addon_slug = request.get(REQUEST_FROM) + return request.get(REQUEST_FROM) addon = self.sys_addons.get(addon_slug) if not addon: @@ -66,14 +66,6 @@ class APIAddons(CoreSysAttributes): return addon - @staticmethod - def _pretty_devices(addon): - """Return a simplified device list.""" - dev_list = addon.devices - if not dev_list: - return None - return [row.split(':')[0] for row in dev_list] - @api_process async def list(self, request): """Return all add-ons or repositories.""" @@ -148,7 +140,7 @@ class APIAddons(CoreSysAttributes): ATTR_PRIVILEGED: addon.privileged, ATTR_FULL_ACCESS: addon.with_full_access, ATTR_APPARMOR: addon.apparmor, - ATTR_DEVICES: self._pretty_devices(addon), + ATTR_DEVICES: _pretty_devices(addon), ATTR_ICON: addon.with_icon, ATTR_LOGO: addon.with_logo, ATTR_CHANGELOG: addon.with_changelog, @@ -163,7 +155,7 @@ class APIAddons(CoreSysAttributes): ATTR_AUDIO: addon.with_audio, ATTR_AUDIO_INPUT: addon.audio_input, ATTR_AUDIO_OUTPUT: addon.audio_output, - ATTR_SERVICES: addon.services, + ATTR_SERVICES: _pretty_services(addon), ATTR_DISCOVERY: addon.discovery, } @@ -328,3 +320,19 @@ class APIAddons(CoreSysAttributes): data = await request.read() return await asyncio.shield(addon.write_stdin(data)) + + +def _pretty_devices(addon): + """Return a simplified device list.""" + dev_list = addon.devices + if not dev_list: + return None + return [row.split(':')[0] for row in dev_list] + + +def _pretty_services(addon): + """Return a simplified services role list.""" + services = [] + for name, access in addon.services_role.items(): + services.append(f"{name}:{access}") + return services diff --git a/hassio/api/discovery.py b/hassio/api/discovery.py index ff08517db..f32b623c6 100644 --- a/hassio/api/discovery.py +++ b/hassio/api/discovery.py @@ -3,15 +3,18 @@ import voluptuous as vol from .utils import api_process, api_validate from ..const import ( - ATTR_PROVIDER, ATTR_UUID, ATTR_COMPONENT, ATTR_PLATFORM, ATTR_CONFIG, - ATTR_DISCOVERY, REQUEST_FROM) + ATTR_ADDON, ATTR_UUID, ATTR_COMPONENT, ATTR_PLATFORM, ATTR_CONFIG, + ATTR_DISCOVERY, ATTR_SERVICE, REQUEST_FROM) from ..coresys import CoreSysAttributes +from ..exceptions import APIError, APIForbidden +from ..services.validate import SERVICE_ALL SCHEMA_DISCOVERY = vol.Schema({ + vol.Required(ATTR_SERVICE): vol.In(SERVICE_ALL), vol.Required(ATTR_COMPONENT): vol.Coerce(str), - vol.Optional(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)), - vol.Optional(ATTR_CONFIG): vol.Any(None, dict), + vol.Optional(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_CONFIG): vol.Maybe(dict), }) @@ -22,16 +25,24 @@ class APIDiscovery(CoreSysAttributes): """Extract discovery message from URL.""" message = self.sys_discovery.get(request.match_info.get('uuid')) if not message: - raise RuntimeError("Discovery message not found") + raise APIError("Discovery message not found") return message + def _check_permission_ha(self, request): + """Check permission for API call / Home Assistant.""" + if request[REQUEST_FROM] != self.sys_homeassistant: + raise APIForbidden("Only HomeAssistant can use this API!") + @api_process async def list(self, request): """Show register services.""" + self._check_permission_ha(request) + discovery = [] for message in self.sys_discovery.list_messages: discovery.append({ - ATTR_PROVIDER: message.provider, + ATTR_ADDON: message.addon, + ATTR_SERVICE: message.service, ATTR_UUID: message.uuid, ATTR_COMPONENT: message.component, ATTR_PLATFORM: message.platform, @@ -44,8 +55,14 @@ class APIDiscovery(CoreSysAttributes): async def set_discovery(self, request): """Write data into a discovery pipeline.""" body = await api_validate(SCHEMA_DISCOVERY, request) - message = self.sys_discovery.send( - provider=request[REQUEST_FROM], **body) + addon = request[REQUEST_FROM] + + # Access? + if body[ATTR_SERVICE] not in addon.discovery: + raise APIForbidden(f"Can't use discovery!") + + # Process discovery message + message = self.sys_discovery.send(addon, **body) return {ATTR_UUID: message.uuid} @@ -54,8 +71,12 @@ class APIDiscovery(CoreSysAttributes): """Read data into a discovery message.""" message = self._extract_message(request) + # HomeAssistant? + self._check_permission_ha(request) + return { - ATTR_PROVIDER: message.provider, + ATTR_ADDON: message.addon, + ATTR_SERVICE: message.service, ATTR_UUID: message.uuid, ATTR_COMPONENT: message.component, ATTR_PLATFORM: message.platform, @@ -66,6 +87,11 @@ class APIDiscovery(CoreSysAttributes): async def del_discovery(self, request): """Delete data into a discovery message.""" message = self._extract_message(request) + addon = request[REQUEST_FROM] + + # Permission + if message.addon != addon.slug: + raise APIForbidden(f"Can't remove discovery message") self.sys_discovery.remove(message) return True diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index baf530cdf..78d92b3f7 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -13,6 +13,7 @@ from ..const import ( ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY) from ..coresys import CoreSysAttributes from ..validate import NETWORK_PORT, DOCKER_IMAGE +from ..exceptions import APIError _LOGGER = logging.getLogger(__name__) @@ -94,7 +95,7 @@ class APIHomeAssistant(CoreSysAttributes): """Return resource information.""" stats = await self.sys_homeassistant.stats() if not stats: - raise RuntimeError("No stats available") + raise APIError("No stats available") return { ATTR_CPU_PERCENT: stats.cpu_percent, @@ -139,6 +140,6 @@ class APIHomeAssistant(CoreSysAttributes): """Check configuration of Home Assistant.""" result = await self.sys_homeassistant.check_config() if not result.valid: - raise RuntimeError(result.log) + raise APIError(result.log) return True diff --git a/hassio/api/security.py b/hassio/api/security.py index 1462fb0b5..40dbe9efb 100644 --- a/hassio/api/security.py +++ b/hassio/api/security.py @@ -26,7 +26,6 @@ NO_SECURITY_CHECK = re.compile( r"|/homeassistant/api/.*" r"|/homeassistant/websocket" r"|/supervisor/ping" - r"|/services.*" r")$" ) @@ -35,6 +34,8 @@ ADDONS_API_BYPASS = re.compile( r"^(?:" r"|/addons/self/(?!security)[^/]+" r"|/version" + r"|/services.*" + r"|/discovery.*" r")$" ) @@ -58,8 +59,7 @@ ADDONS_ROLE_ACCESS = { r"|/hardware/.+" r"|/hassos/.+" r"|/supervisor/.+" - r"|/addons/[^/]+/(?!security|options).+" - r"|/addons(?:/self/(?!security).+)?" + r"|/addons/[^/]+/(?!security).+" r"|/snapshots.*" r")$" ), @@ -102,12 +102,12 @@ class SecurityMiddleware(CoreSysAttributes): if hassio_token in (self.sys_homeassistant.uuid, self.sys_homeassistant.hassio_token): _LOGGER.debug("%s access from Home Assistant", request.path) - request_from = 'homeassistant' + request_from = self.sys_homeassistant # Host if hassio_token == self.sys_machine_id: _LOGGER.debug("%s access from Host", request.path) - request_from = 'host' + request_from = self.sys_host # Add-on addon = None @@ -117,12 +117,12 @@ class SecurityMiddleware(CoreSysAttributes): # Check Add-on API access if addon and ADDONS_API_BYPASS.match(request.path): _LOGGER.debug("Passthrough %s from %s", request.path, addon.slug) - request_from = addon.slug + request_from = addon elif addon and addon.access_hassio_api: # Check Role if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path): _LOGGER.info("%s access from %s", request.path, addon.slug) - request_from = addon.slug + request_from = addon else: _LOGGER.warning("%s no role for %s", request.path, addon.slug) diff --git a/hassio/api/services.py b/hassio/api/services.py index 79618b5bc..e17d43769 100644 --- a/hassio/api/services.py +++ b/hassio/api/services.py @@ -2,8 +2,10 @@ from .utils import api_process, api_validate from ..const import ( - ATTR_AVAILABLE, ATTR_PROVIDER, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM) + ATTR_AVAILABLE, ATTR_PROVIDERS, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM, + PROVIDE_SERVICE) from ..coresys import CoreSysAttributes +from ..exceptions import APIError, APIForbidden class APIServices(CoreSysAttributes): @@ -13,7 +15,7 @@ class APIServices(CoreSysAttributes): """Return service, throw an exception if it doesn't exist.""" service = self.sys_services.get(request.match_info.get('service')) if not service: - raise RuntimeError("Service does not exist") + raise APIError("Service does not exist") return service @@ -25,7 +27,7 @@ class APIServices(CoreSysAttributes): services.append({ ATTR_SLUG: service.slug, ATTR_AVAILABLE: service.enabled, - ATTR_PROVIDER: service.provider, + ATTR_PROVIDERS: service.providers, }) return {ATTR_SERVICES: services} @@ -35,21 +37,39 @@ class APIServices(CoreSysAttributes): """Write data into a service.""" service = self._extract_service(request) body = await api_validate(service.schema, request) + addon = request[REQUEST_FROM] - return service.set_service_data(request[REQUEST_FROM], body) + _check_access(request, service.slug) + service.set_service_data(addon, body) @api_process async def get_service(self, request): """Read data into a service.""" service = self._extract_service(request) - return { - ATTR_AVAILABLE: service.enabled, - service.slug: service.get_service_data(), - } + # Access + _check_access(request, service.slug) + + if not service.enabled: + raise APIError("Service not enabled") + return service.get_service_data() @api_process async def del_service(self, request): """Delete data into a service.""" service = self._extract_service(request) - return service.del_service_data(request[REQUEST_FROM]) + addon = request[REQUEST_FROM] + + # Access + _check_access(request, service.slug, True) + service.del_service_data(addon) + + +def _check_access(request, service, provide=False): + """Raise error if the rights are wrong.""" + addon = request[REQUEST_FROM] + if not addon.services_role.get(service): + raise APIForbidden(f"No access to {service} service!") + + if provide and addon.services_role.get(service) != PROVIDE_SERVICE: + raise APIForbidden(f"No access to write {service} service!") diff --git a/hassio/api/snapshots.py b/hassio/api/snapshots.py index 6ca5aa4fd..612f7f35b 100644 --- a/hassio/api/snapshots.py +++ b/hassio/api/snapshots.py @@ -14,6 +14,7 @@ from ..const import ( ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE, ATTR_SNAPSHOTS, ATTR_PASSWORD, ATTR_PROTECTED, CONTENT_TYPE_TAR) from ..coresys import CoreSysAttributes +from ..exceptions import APIError _LOGGER = logging.getLogger(__name__) @@ -52,7 +53,7 @@ class APISnapshots(CoreSysAttributes): """Return snapshot, throw an exception if it doesn't exist.""" snapshot = self.sys_snapshots.get(request.match_info.get('snapshot')) if not snapshot: - raise RuntimeError("Snapshot does not exist") + raise APIError("Snapshot does not exist") return snapshot @api_process diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 88e0f9526..5f1fa2e4b 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -14,6 +14,7 @@ from ..const import ( ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON) from ..coresys import CoreSysAttributes from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES, CHANNELS +from ..exceptions import APIError _LOGGER = logging.getLogger(__name__) @@ -93,7 +94,7 @@ class APISupervisor(CoreSysAttributes): """Return resource information.""" stats = await self.sys_supervisor.stats() if not stats: - raise RuntimeError("No stats available") + raise APIError("No stats available") return { ATTR_CPU_PERCENT: stats.cpu_percent, @@ -112,7 +113,7 @@ class APISupervisor(CoreSysAttributes): version = body.get(ATTR_VERSION, self.sys_updater.version_hassio) if version == self.sys_supervisor.version: - raise RuntimeError("Version {} is already in use".format(version)) + raise APIError("Version {} is already in use".format(version)) return await asyncio.shield( self.sys_supervisor.update(version)) @@ -128,7 +129,7 @@ class APISupervisor(CoreSysAttributes): for result in results: if result.exception() is not None: - raise RuntimeError("Some reload task fails!") + raise APIError("Some reload task fails!") return True diff --git a/hassio/api/utils.py b/hassio/api/utils.py index 7472ea014..408c194a6 100644 --- a/hassio/api/utils.py +++ b/hassio/api/utils.py @@ -9,7 +9,7 @@ from voluptuous.humanize import humanize_error from ..const import ( JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR, CONTENT_TYPE_BINARY) -from ..exceptions import HassioError +from ..exceptions import HassioError, APIError, APIForbidden _LOGGER = logging.getLogger(__name__) @@ -21,7 +21,7 @@ def json_loads(data): try: return json.loads(data) except json.JSONDecodeError: - raise RuntimeError("Invalid json") + raise APIError("Invalid json") def api_process(method): @@ -30,10 +30,10 @@ def api_process(method): """Return API information.""" try: answer = await method(api, *args, **kwargs) - except HassioError: - return api_return_error() - except RuntimeError as err: + except (APIError, APIForbidden) as err: return api_return_error(message=str(err)) + except HassioError: + return api_return_error(message="Unknown Error, see logs") if isinstance(answer, dict): return api_return_ok(data=answer) @@ -55,7 +55,7 @@ def api_process_raw(content): try: msg_data = await method(api, *args, **kwargs) msg_type = content - except RuntimeError as err: + except (APIError, APIForbidden) as err: msg_data = str(err).encode() msg_type = CONTENT_TYPE_BINARY except HassioError: @@ -90,6 +90,6 @@ async def api_validate(schema, request): try: data = schema(data) except vol.Invalid as ex: - raise RuntimeError(humanize_error(data, ex)) from None + raise APIError(humanize_error(data, ex)) from None return data diff --git a/hassio/const.py b/hassio/const.py index 0ed613821..a88d58f98 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -74,6 +74,7 @@ ATTR_TYPE = 'type' ATTR_SOURCE = 'source' ATTR_FEATURES = 'features' ATTR_ADDONS = 'addons' +ATTR_PROVIDERS = 'providers' ATTR_VERSION = 'version' ATTR_VERSION_LATEST = 'version_latest' ATTR_AUTO_UART = 'auto_uart' @@ -107,8 +108,6 @@ ATTR_MAINTAINER = 'maintainer' ATTR_PASSWORD = 'password' ATTR_TOTP = 'totp' ATTR_INITIALIZE = 'initialize' -ATTR_SESSION = 'session' -ATTR_SESSIONS = 'sessions' ATTR_LOCATON = 'location' ATTR_BUILD = 'build' ATTR_DEVICES = 'devices' @@ -154,7 +153,7 @@ ATTR_MEMORY_LIMIT = 'memory_limit' ATTR_MEMORY_USAGE = 'memory_usage' ATTR_BLK_READ = 'blk_read' ATTR_BLK_WRITE = 'blk_write' -ATTR_PROVIDER = 'provider' +ATTR_ADDON = 'addon' ATTR_AVAILABLE = 'available' ATTR_HOST = 'host' ATTR_USERNAME = 'username' @@ -163,8 +162,8 @@ ATTR_DISCOVERY = 'discovery' ATTR_PLATFORM = 'platform' ATTR_COMPONENT = 'component' ATTR_CONFIG = 'config' -ATTR_DISCOVERY_ID = 'discovery_id' ATTR_SERVICES = 'services' +ATTR_SERVICE = 'service' ATTR_DISCOVERY = 'discovery' ATTR_PROTECTED = 'protected' ATTR_CRYPTO = 'crypto' @@ -188,6 +187,9 @@ ATTR_HASSIO_ROLE = 'hassio_role' ATTR_SUPERVISOR = 'supervisor' SERVICE_MQTT = 'mqtt' +PROVIDE_SERVICE = 'provide' +NEED_SERVICE = 'need' +WANT_SERVICE = 'want' STARTUP_INITIALIZE = 'initialize' STARTUP_SYSTEM = 'system' diff --git a/hassio/exceptions.py b/hassio/exceptions.py index 89b2207cb..288d5559f 100644 --- a/hassio/exceptions.py +++ b/hassio/exceptions.py @@ -81,13 +81,25 @@ class HostAppArmorError(HostError): # API -class APIError(HassioError): +class APIError(HassioError, RuntimeError): """API errors.""" pass -class APINotSupportedError(HassioNotSupportedError): - """API not supported error.""" +class APIForbidden(APIError): + """API forbidden error.""" + pass + + +# Service / Discovery + +class DiscoveryError(HassioError): + """Discovery Errors.""" + pass + + +class ServicesError(HassioError): + """Services Errors.""" pass diff --git a/hassio/homeassistant.py b/hassio/homeassistant.py index b2370f4ed..717694923 100644 --- a/hassio/homeassistant.py +++ b/hassio/homeassistant.py @@ -439,19 +439,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): _LOGGER.warning("Home Assistant API config mismatch: %d", err) return False - async def send_event(self, event_type, event_data=None): - """Send event to Home-Assistant.""" - with suppress(HomeAssistantAPIError): - async with self.make_request( - 'get', f'api/events/{event_type}' - ) as resp: - if resp.status in (200, 201): - return - err = resp.status - - _LOGGER.warning("Home Assistant event %s fails: %s", event_type, err) - return HomeAssistantError() - async def _block_till_run(self): """Block until Home-Assistant is booting up or startup timeout.""" start_time = time.monotonic() diff --git a/hassio/services/discovery.py b/hassio/services/discovery.py index 51b7b96a9..f9b0b2467 100644 --- a/hassio/services/discovery.py +++ b/hassio/services/discovery.py @@ -1,14 +1,20 @@ """Handle discover message for Home Assistant.""" import logging +from contextlib import suppress from uuid import uuid4 -from ..const import ATTR_UUID +import attr +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from .validate import DISCOVERY_SERVICES from ..coresys import CoreSysAttributes +from ..exceptions import DiscoveryError, HomeAssistantAPIError _LOGGER = logging.getLogger(__name__) -EVENT_DISCOVERY_ADD = 'hassio_discovery_add' -EVENT_DISCOVERY_DEL = 'hassio_discovery_del' +CMD_NEW = 'post' +CMD_DEL = 'delete' class Discovery(CoreSysAttributes): @@ -32,7 +38,7 @@ class Discovery(CoreSysAttributes): """Write discovery message into data file.""" messages = [] for message in self.message_obj.values(): - messages.append(message.raw()) + messages.append(attr.asdict(message)) self._data.clear() self._data.extend(messages) @@ -52,26 +58,31 @@ class Discovery(CoreSysAttributes): """Return list of available discovery messages.""" return self.message_obj.values() - def send(self, provider, component, platform=None, config=None): + def send(self, addon, service, component, platform, config): """Send a discovery message to Home Assistant.""" - message = Message(provider, component, platform, config) + try: + DISCOVERY_SERVICES[service](config) + except vol.Invalid as err: + _LOGGER.error( + "Invalid discovery %s config", humanize_error(config, err)) + raise DiscoveryError() from None + + # Create message + message = Message(addon.slug, service, component, platform, config) # Already exists? - for exists_message in self.message_obj: - if exists_message == message: - _LOGGER.warning("Found duplicate discovery message from %s", - provider) - return exists_message + for old_message in self.message_obj: + if old_message != message: + continue + _LOGGER.warning("Duplicate discovery message from %s", addon.slug) + return old_message _LOGGER.info("Send discovery to Home Assistant %s/%s from %s", - component, platform, provider) + component, platform, addon.slug) self.message_obj[message.uuid] = message self.save() - # Send event to Home Assistant - self.sys_create_task(self.sys_homeassistant.send_event( - EVENT_DISCOVERY_ADD, {ATTR_UUID: message.uuid})) - + self.sys_create_task(self._push_discovery(message.uuid, CMD_NEW)) return message def remove(self, message): @@ -79,29 +90,31 @@ class Discovery(CoreSysAttributes): self.message_obj.pop(message.uuid, None) self.save() - # send event to Home-Assistant - self.sys_create_task(self.sys_homeassistant.send_event( - EVENT_DISCOVERY_DEL, {ATTR_UUID: message.uuid})) + _LOGGER.info("Delete discovery to Home Assistant %s/%s from %s", + message.component, message.platform, message.addon) + self.sys_create_task(self._push_discovery(message.uuid, CMD_DEL)) + + async def _push_discovery(self, uuid, command): + """Send a discovery request.""" + if not await self.sys_homeassistant.check_api_state(): + _LOGGER.info("Discovery %s mesage ignore", uuid) + return + + with suppress(HomeAssistantAPIError): + async with self.sys_homeassistant.make_request( + command, f"api/hassio_push/discovery/{uuid}"): + _LOGGER.info("Discovery %s message send", uuid) + return + + _LOGGER.warning("Discovery %s message fail", uuid) +@attr.s class Message: """Represent a single Discovery message.""" - - def __init__(self, provider, component, platform, config, uuid=None): - """Initialize discovery message.""" - self.provider = provider - self.component = component - self.platform = platform - self.config = config - self.uuid = uuid or uuid4().hex - - def raw(self): - """Return raw discovery message.""" - return self.__dict__ - - def __eq__(self, other): - """Compare with other message.""" - for attribute in ('provider', 'component', 'platform', 'config'): - if getattr(self, attribute) != getattr(other, attribute): - return False - return True + addon = attr.ib() + service = attr.ib() + component = attr.ib() + platform = attr.ib() + config = attr.ib() + uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False) diff --git a/hassio/services/interface.py b/hassio/services/interface.py index 4d4e79da0..7215963e1 100644 --- a/hassio/services/interface.py +++ b/hassio/services/interface.py @@ -1,6 +1,7 @@ """Interface for single service.""" from ..coresys import CoreSysAttributes +from ..const import PROVIDE_SERVICE class ServiceInterface(CoreSysAttributes): @@ -26,9 +27,13 @@ class ServiceInterface(CoreSysAttributes): return None @property - def provider(self): - """Return name of service provider.""" - return None + def providers(self): + """Return name of service providers addon.""" + addons = [] + for addon in self.sys_addons.list_installed: + if addon.services_role.get(self.slug) == PROVIDE_SERVICE: + addons.append(addon.slug) + return addons @property def enabled(self): @@ -45,10 +50,10 @@ class ServiceInterface(CoreSysAttributes): return self._data return None - def set_service_data(self, provider, data): + def set_service_data(self, addon, data): """Write the data into service object.""" raise NotImplementedError() - def del_service_data(self, provider): + def del_service_data(self, addon): """Remove the data from service object.""" raise NotImplementedError() diff --git a/hassio/services/mqtt.py b/hassio/services/mqtt.py index 55f41c232..05f98c292 100644 --- a/hassio/services/mqtt.py +++ b/hassio/services/mqtt.py @@ -3,9 +3,8 @@ import logging from .interface import ServiceInterface from .validate import SCHEMA_SERVICE_MQTT -from ..const import ( - ATTR_PROVIDER, SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_USERNAME, - ATTR_PASSWORD, ATTR_PROTOCOL, ATTR_DISCOVERY_ID) +from ..const import ATTR_ADDON, SERVICE_MQTT +from ..exceptions import ServicesError _LOGGER = logging.getLogger(__name__) @@ -28,62 +27,24 @@ class MQTTService(ServiceInterface): """Return data schema of this service.""" return SCHEMA_SERVICE_MQTT - @property - def provider(self): - """Return name of service provider.""" - return self._data.get(ATTR_PROVIDER) - - @property - def hass_config(self): - """Return Home Assistant MQTT config.""" - if not self.enabled: - return None - - hass_config = { - 'host': self._data[ATTR_HOST], - 'port': self._data[ATTR_PORT], - 'protocol': self._data[ATTR_PROTOCOL] - } - if ATTR_USERNAME in self._data: - hass_config['user']: self._data[ATTR_USERNAME] - if ATTR_PASSWORD in self._data: - hass_config['password']: self._data[ATTR_PASSWORD] - - return hass_config - - def set_service_data(self, provider, data): + def set_service_data(self, addon, data): """Write the data into service object.""" if self.enabled: - _LOGGER.error("It is already a MQTT in use from %s", self.provider) - return False + _LOGGER.error( + "It is already a MQTT in use from %s", self._data[ATTR_ADDON]) + raise ServicesError() self._data.update(data) - self._data[ATTR_PROVIDER] = provider + self._data[ATTR_ADDON] = addon.slug - if provider == 'homeassistant': - _LOGGER.info("Use MQTT settings from Home Assistant") - self.save() - return True - - # Discover MQTT to Home Assistant - message = self.sys_discovery.send( - provider, SERVICE_MQTT, None, self.hass_config) - - self._data[ATTR_DISCOVERY_ID] = message.uuid + _LOGGER.info("Set %s as service provider for mqtt", addon.slug) self.save() - return True - def del_service_data(self, provider): + def del_service_data(self, addon): """Remove the data from service object.""" if not self.enabled: _LOGGER.warning("Can't remove not exists services") - return False - - discovery_id = self._data.get(ATTR_DISCOVERY_ID) - if discovery_id: - self.sys_discovery.remove( - self.sys_discovery.get(discovery_id)) + raise ServicesError() self._data.clear() self.save() - return True diff --git a/hassio/services/validate.py b/hassio/services/validate.py index 14a4da1b8..6e0a2a5bb 100644 --- a/hassio/services/validate.py +++ b/hassio/services/validate.py @@ -1,20 +1,40 @@ """Validate services schema.""" +import re + import voluptuous as vol from ..const import ( SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL, - ATTR_PROVIDER, ATTR_PROTOCOL, ATTR_DISCOVERY, ATTR_COMPONENT, ATTR_UUID, - ATTR_PLATFORM, ATTR_CONFIG, ATTR_DISCOVERY_ID) + ATTR_ADDON, ATTR_PROTOCOL, ATTR_DISCOVERY, ATTR_COMPONENT, ATTR_UUID, + ATTR_PLATFORM, ATTR_CONFIG, ATTR_SERVICE) from ..validate import NETWORK_PORT +UUID_MATCH = re.compile(r"^[0-9a-f]{32}$") + +SERVICE_ALL = [ + SERVICE_MQTT +] + + +def schema_or(schema): + """Allow schema or empty.""" + def _wrapper(value): + """Wrapper for validator.""" + if not value: + return value + return schema(value) + + return _wrapper + SCHEMA_DISCOVERY = vol.Schema([ vol.Schema({ - vol.Required(ATTR_UUID): vol.Match(r"^[0-9a-f]{32}$"), - vol.Required(ATTR_PROVIDER): vol.Coerce(str), + vol.Required(ATTR_UUID): vol.Match(UUID_MATCH), + vol.Required(ATTR_ADDON): vol.Coerce(str), + vol.Required(ATTR_SERVICE): vol.In(SERVICE_ALL), vol.Required(ATTR_COMPONENT): vol.Coerce(str), - vol.Required(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)), - vol.Required(ATTR_CONFIG): vol.Any(None, dict), + vol.Required(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)), + vol.Required(ATTR_CONFIG): vol.Maybe(dict), }, extra=vol.REMOVE_EXTRA) ]) @@ -32,12 +52,16 @@ SCHEMA_SERVICE_MQTT = vol.Schema({ SCHEMA_CONFIG_MQTT = SCHEMA_SERVICE_MQTT.extend({ - vol.Required(ATTR_PROVIDER): vol.Coerce(str), - vol.Optional(ATTR_DISCOVERY_ID): vol.Match(r"^[0-9a-f]{32}$"), + vol.Required(ATTR_ADDON): vol.Coerce(str), }) SCHEMA_SERVICES_FILE = vol.Schema({ - vol.Optional(SERVICE_MQTT, default=dict): vol.Any({}, SCHEMA_CONFIG_MQTT), - vol.Optional(ATTR_DISCOVERY, default=list): vol.Any([], SCHEMA_DISCOVERY), + vol.Optional(SERVICE_MQTT, default=dict): schema_or(SCHEMA_CONFIG_MQTT), + vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY), }, extra=vol.REMOVE_EXTRA) + + +DISCOVERY_SERVICES = { + SERVICE_MQTT: SCHEMA_SERVICE_MQTT, +} From af19e95c81eaaccdc3e95e0ea1636dd960a63056 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 30 Sep 2018 15:33:16 +0200 Subject: [PATCH 11/13] Make discovery persistent (#727) * Make discovery persistent * fix file handling * fix detection * Smooth * Fix ring import * Fix handling * fix schema * fix validate * fix discovery cleanup --- hassio/addons/addon.py | 10 ++++++ hassio/addons/validate.py | 5 ++- hassio/api/discovery.py | 4 +-- hassio/api/supervisor.py | 3 +- hassio/bootstrap.py | 2 +- hassio/const.py | 1 + hassio/core.py | 3 ++ hassio/{services => }/discovery.py | 49 ++++++++++++++++-------------- hassio/services/__init__.py | 5 --- hassio/services/data.py | 11 ++----- hassio/services/validate.py | 38 ++--------------------- hassio/utils/validate.py | 28 +++++++++++++++++ hassio/validate.py | 42 ++++++++++++++----------- 13 files changed, 105 insertions(+), 96 deletions(-) rename hassio/{services => }/discovery.py (68%) create mode 100644 hassio/utils/validate.py diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 7bcef24b9..c9f81d547 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -584,6 +584,13 @@ class Addon(CoreSysAttributes): return False + def remove_discovery(self): + """Remove all discovery message from add-on.""" + for message in self.sys_discovery.list_messages: + if message.addon != self.slug: + continue + self.sys_discovery.remove(message) + def write_asound(self): """Write asound config to file and return True on success.""" asound_config = self.sys_host.alsa.asound( @@ -704,6 +711,9 @@ class Addon(CoreSysAttributes): with suppress(HostAppArmorError): await self.sys_host.apparmor.remove_profile(self.slug) + # Remove discovery messages + self.remove_discovery() + self._set_uninstall() return True diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 9d2a2b705..0e25e365f 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -25,7 +25,7 @@ from ..const import ( PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE, PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN) -from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE +from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE, UUID_MATCH from ..services.validate import DISCOVERY_SERVICES _LOGGER = logging.getLogger(__name__) @@ -185,8 +185,7 @@ SCHEMA_BUILD_CONFIG = vol.Schema({ # pylint: disable=no-value-for-parameter SCHEMA_ADDON_USER = vol.Schema({ vol.Required(ATTR_VERSION): vol.Coerce(str), - vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): - vol.Match(r"^[0-9a-f]{32}$"), + vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH, vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"), vol.Optional(ATTR_OPTIONS, default=dict): dict, vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(), diff --git a/hassio/api/discovery.py b/hassio/api/discovery.py index f32b623c6..0acf972a9 100644 --- a/hassio/api/discovery.py +++ b/hassio/api/discovery.py @@ -7,11 +7,11 @@ from ..const import ( ATTR_DISCOVERY, ATTR_SERVICE, REQUEST_FROM) from ..coresys import CoreSysAttributes from ..exceptions import APIError, APIForbidden -from ..services.validate import SERVICE_ALL +from ..validate import SERVICE_ALL SCHEMA_DISCOVERY = vol.Schema({ - vol.Required(ATTR_SERVICE): vol.In(SERVICE_ALL), + vol.Required(ATTR_SERVICE): SERVICE_ALL, vol.Required(ATTR_COMPONENT): vol.Coerce(str), vol.Optional(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_CONFIG): vol.Maybe(dict), diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 5f1fa2e4b..33fe66c79 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -13,8 +13,9 @@ from ..const import ( ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ, ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON) from ..coresys import CoreSysAttributes -from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES, CHANNELS +from ..validate import WAIT_BOOT, REPOSITORIES, CHANNELS from ..exceptions import APIError +from ..utils.validate import validate_timezone _LOGGER = logging.getLogger(__name__) diff --git a/hassio/bootstrap.py b/hassio/bootstrap.py index 012205410..2aaa67b17 100644 --- a/hassio/bootstrap.py +++ b/hassio/bootstrap.py @@ -18,7 +18,7 @@ from .snapshots import SnapshotManager from .tasks import Tasks from .updater import Updater from .services import ServiceManager -from .services import Discovery +from .discovery import Discovery from .host import HostManager from .dbus import DBusManager from .hassos import HassOS diff --git a/hassio/const.py b/hassio/const.py index a88d58f98..c4d4b03b3 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -21,6 +21,7 @@ FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json") FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json") FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json") FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json") +FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json") SOCKET_DOCKER = Path("/var/run/docker.sock") diff --git a/hassio/core.py b/hassio/core.py index be32ee89f..0884549fe 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -52,6 +52,9 @@ class HassIO(CoreSysAttributes): # load services await self.sys_services.load() + # Load discovery + await self.sys_discovery.load() + # start dns forwarding self.sys_create_task(self.sys_dns.start()) diff --git a/hassio/services/discovery.py b/hassio/discovery.py similarity index 68% rename from hassio/services/discovery.py rename to hassio/discovery.py index f9b0b2467..98fb0e7e6 100644 --- a/hassio/services/discovery.py +++ b/hassio/discovery.py @@ -7,9 +7,12 @@ import attr import voluptuous as vol from voluptuous.humanize import humanize_error -from .validate import DISCOVERY_SERVICES -from ..coresys import CoreSysAttributes -from ..exceptions import DiscoveryError, HomeAssistantAPIError +from .const import FILE_HASSIO_DISCOVERY, ATTR_CONFIG, ATTR_DISCOVERY +from .coresys import CoreSysAttributes +from .exceptions import DiscoveryError, HomeAssistantAPIError +from .validate import SCHEMA_DISCOVERY_CONFIG +from .utils.json import JsonConfig +from .services.validate import DISCOVERY_SERVICES _LOGGER = logging.getLogger(__name__) @@ -17,18 +20,19 @@ CMD_NEW = 'post' CMD_DEL = 'delete' -class Discovery(CoreSysAttributes): +class Discovery(CoreSysAttributes, JsonConfig): """Home Assistant Discovery handler.""" def __init__(self, coresys): """Initialize discovery handler.""" + super().__init__(FILE_HASSIO_DISCOVERY, SCHEMA_DISCOVERY_CONFIG) self.coresys = coresys self.message_obj = {} - def load(self): + async def load(self): """Load exists discovery message into storage.""" messages = {} - for message in self._data: + for message in self._data[ATTR_DISCOVERY]: discovery = Message(**message) messages[discovery.uuid] = discovery @@ -40,19 +44,14 @@ class Discovery(CoreSysAttributes): for message in self.message_obj.values(): messages.append(attr.asdict(message)) - self._data.clear() - self._data.extend(messages) - self.sys_services.data.save_data() + self._data[ATTR_DISCOVERY].clear() + self._data[ATTR_DISCOVERY].extend(messages) + self.save_data() def get(self, uuid): """Return discovery message.""" return self.message_obj.get(uuid) - @property - def _data(self): - """Return discovery data.""" - return self.sys_services.data.discovery - @property def list_messages(self): """Return list of available discovery messages.""" @@ -71,7 +70,7 @@ class Discovery(CoreSysAttributes): message = Message(addon.slug, service, component, platform, config) # Already exists? - for old_message in self.message_obj: + for old_message in self.list_messages: if old_message != message: continue _LOGGER.warning("Duplicate discovery message from %s", addon.slug) @@ -82,7 +81,7 @@ class Discovery(CoreSysAttributes): self.message_obj[message.uuid] = message self.save() - self.sys_create_task(self._push_discovery(message.uuid, CMD_NEW)) + self.sys_create_task(self._push_discovery(message, CMD_NEW)) return message def remove(self, message): @@ -92,21 +91,25 @@ class Discovery(CoreSysAttributes): _LOGGER.info("Delete discovery to Home Assistant %s/%s from %s", message.component, message.platform, message.addon) - self.sys_create_task(self._push_discovery(message.uuid, CMD_DEL)) + self.sys_create_task(self._push_discovery(message, CMD_DEL)) - async def _push_discovery(self, uuid, command): + async def _push_discovery(self, message, command): """Send a discovery request.""" if not await self.sys_homeassistant.check_api_state(): - _LOGGER.info("Discovery %s mesage ignore", uuid) + _LOGGER.info("Discovery %s mesage ignore", message.uuid) return + data = attr.asdict(message) + data.pop(ATTR_CONFIG) + with suppress(HomeAssistantAPIError): async with self.sys_homeassistant.make_request( - command, f"api/hassio_push/discovery/{uuid}"): - _LOGGER.info("Discovery %s message send", uuid) + command, f"api/hassio_push/discovery/{message.uuid}", + json=data, timeout=10): + _LOGGER.info("Discovery %s message send", message.uuid) return - _LOGGER.warning("Discovery %s message fail", uuid) + _LOGGER.warning("Discovery %s message fail", message.uuid) @attr.s @@ -116,5 +119,5 @@ class Message: service = attr.ib() component = attr.ib() platform = attr.ib() - config = attr.ib() + config = attr.ib(cmp=False) uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False) diff --git a/hassio/services/__init__.py b/hassio/services/__init__.py index 33c056725..1fec40187 100644 --- a/hassio/services/__init__.py +++ b/hassio/services/__init__.py @@ -1,5 +1,4 @@ """Handle internal services discovery.""" -from .discovery import Discovery # noqa from .mqtt import MQTTService from .data import ServicesData from ..const import SERVICE_MQTT @@ -34,10 +33,6 @@ class ServiceManager(CoreSysAttributes): for slug, service in AVAILABLE_SERVICES.items(): self.services_obj[slug] = service(self.coresys) - # Read exists discovery messages - self.sys_discovery.load() - def reset(self): """Reset available data.""" self.data.reset_data() - self.sys_discovery.load() diff --git a/hassio/services/data.py b/hassio/services/data.py index c2fe2d630..5df9df2d0 100644 --- a/hassio/services/data.py +++ b/hassio/services/data.py @@ -1,7 +1,7 @@ """Handle service data for persistent supervisor reboot.""" -from .validate import SCHEMA_SERVICES_FILE -from ..const import FILE_HASSIO_SERVICES, ATTR_DISCOVERY, SERVICE_MQTT +from .validate import SCHEMA_SERVICES_CONFIG +from ..const import FILE_HASSIO_SERVICES, SERVICE_MQTT from ..utils.json import JsonConfig @@ -10,12 +10,7 @@ class ServicesData(JsonConfig): def __init__(self): """Initialize services data.""" - super().__init__(FILE_HASSIO_SERVICES, SCHEMA_SERVICES_FILE) - - @property - def discovery(self): - """Return discovery data for Home Assistant.""" - return self._data[ATTR_DISCOVERY] + super().__init__(FILE_HASSIO_SERVICES, SCHEMA_SERVICES_CONFIG) @property def mqtt(self): diff --git a/hassio/services/validate.py b/hassio/services/validate.py index 6e0a2a5bb..a7217be90 100644 --- a/hassio/services/validate.py +++ b/hassio/services/validate.py @@ -1,42 +1,11 @@ """Validate services schema.""" -import re - import voluptuous as vol from ..const import ( SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL, - ATTR_ADDON, ATTR_PROTOCOL, ATTR_DISCOVERY, ATTR_COMPONENT, ATTR_UUID, - ATTR_PLATFORM, ATTR_CONFIG, ATTR_SERVICE) + ATTR_ADDON, ATTR_PROTOCOL) from ..validate import NETWORK_PORT - -UUID_MATCH = re.compile(r"^[0-9a-f]{32}$") - -SERVICE_ALL = [ - SERVICE_MQTT -] - - -def schema_or(schema): - """Allow schema or empty.""" - def _wrapper(value): - """Wrapper for validator.""" - if not value: - return value - return schema(value) - - return _wrapper - - -SCHEMA_DISCOVERY = vol.Schema([ - vol.Schema({ - vol.Required(ATTR_UUID): vol.Match(UUID_MATCH), - vol.Required(ATTR_ADDON): vol.Coerce(str), - vol.Required(ATTR_SERVICE): vol.In(SERVICE_ALL), - vol.Required(ATTR_COMPONENT): vol.Coerce(str), - vol.Required(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)), - vol.Required(ATTR_CONFIG): vol.Maybe(dict), - }, extra=vol.REMOVE_EXTRA) -]) +from ..utils.validate import schema_or # pylint: disable=no-value-for-parameter @@ -56,9 +25,8 @@ SCHEMA_CONFIG_MQTT = SCHEMA_SERVICE_MQTT.extend({ }) -SCHEMA_SERVICES_FILE = vol.Schema({ +SCHEMA_SERVICES_CONFIG = vol.Schema({ vol.Optional(SERVICE_MQTT, default=dict): schema_or(SCHEMA_CONFIG_MQTT), - vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY), }, extra=vol.REMOVE_EXTRA) diff --git a/hassio/utils/validate.py b/hassio/utils/validate.py new file mode 100644 index 000000000..147baa684 --- /dev/null +++ b/hassio/utils/validate.py @@ -0,0 +1,28 @@ +"""Validate utils.""" + +import pytz +import voluptuous as vol + + +def schema_or(schema): + """Allow schema or empty.""" + def _wrapper(value): + """Wrapper for validator.""" + if not value: + return value + return schema(value) + + return _wrapper + + +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 diff --git a/hassio/validate.py b/hassio/validate.py index 36dd2fbbe..6c3eaf39a 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -3,15 +3,17 @@ import uuid import re import voluptuous as vol -import pytz from .const import ( ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS, ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, - ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, + ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_CONFIG, ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI, - ATTR_ACCESS_TOKEN, + ATTR_ACCESS_TOKEN, ATTR_DISCOVERY, ATTR_ADDON, ATTR_COMPONENT, + ATTR_PLATFORM, ATTR_SERVICE, + SERVICE_MQTT, CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) +from .utils.validate import schema_or, validate_timezone RE_REPOSITORY = re.compile(r"^(?P[^#]+)(?:#(?P[\w\-]+))?$") @@ -21,6 +23,8 @@ WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60)) DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$") ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+")) CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV]) +UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$") +SERVICE_ALL = vol.In([SERVICE_MQTT]) def validate_repository(repository): @@ -40,19 +44,6 @@ def validate_repository(repository): REPOSITORIES = vol.All([validate_repository], vol.Unique()) -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 - - # pylint: disable=inconsistent-return-statements def convert_to_docker_ports(data): """Convert data into Docker port list.""" @@ -83,8 +74,7 @@ DOCKER_PORTS = vol.Schema({ # pylint: disable=no-value-for-parameter SCHEMA_HASS_CONFIG = vol.Schema({ - vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): - vol.Match(r"^[0-9a-f]{32}$"), + vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH, vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"), vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, @@ -117,3 +107,19 @@ SCHEMA_HASSIO_CONFIG = vol.Schema({ ]): REPOSITORIES, vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT, }, extra=vol.REMOVE_EXTRA) + + +SCHEMA_DISCOVERY = vol.Schema([ + vol.Schema({ + vol.Required(ATTR_UUID): UUID_MATCH, + vol.Required(ATTR_ADDON): vol.Coerce(str), + vol.Required(ATTR_SERVICE): SERVICE_ALL, + vol.Required(ATTR_COMPONENT): vol.Coerce(str), + vol.Required(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)), + vol.Required(ATTR_CONFIG): vol.Maybe(dict), + }, extra=vol.REMOVE_EXTRA) +]) + +SCHEMA_DISCOVERY_CONFIG = vol.Schema({ + vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY), +}, extra=vol.REMOVE_EXTRA) From 2872be63851f421d4a990cacf58dec2b00814fa6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 30 Sep 2018 17:58:26 +0200 Subject: [PATCH 12/13] Update Panel (#730) --- .../api/panel/chunk.0ef4ef1053fe3d5107b5.js | 1 - .../panel/chunk.0ef4ef1053fe3d5107b5.js.gz | Bin 19234 -> 0 bytes .../api/panel/chunk.457ac71b0904d7243237.js | 2 + .../panel/chunk.457ac71b0904d7243237.js.gz | Bin 0 -> 2756 bytes .../panel/chunk.457ac71b0904d7243237.js.map | 1 + .../api/panel/chunk.57f5b43a82b988080555.js | 2 + .../panel/chunk.57f5b43a82b988080555.js.gz | Bin 0 -> 19495 bytes .../panel/chunk.57f5b43a82b988080555.js.map | 1 + .../api/panel/chunk.72a6da063fe4cb6308e8.js | 3 + .../chunk.72a6da063fe4cb6308e8.js.LICENSE | 652 ++++++++++++++++++ .../panel/chunk.72a6da063fe4cb6308e8.js.gz | Bin 0 -> 76069 bytes .../panel/chunk.72a6da063fe4cb6308e8.js.map | 1 + .../api/panel/chunk.7ee37c2565bcf2d88182.js | 2 + .../panel/chunk.7ee37c2565bcf2d88182.js.gz | Bin 0 -> 225 bytes .../panel/chunk.7ee37c2565bcf2d88182.js.map | 1 + .../api/panel/chunk.a8e86d80be46b3b6e16d.js | 2 - .../chunk.a8e86d80be46b3b6e16d.js.LICENSE | 419 ----------- .../panel/chunk.a8e86d80be46b3b6e16d.js.gz | Bin 73638 -> 0 bytes .../api/panel/chunk.a8fa5591357cce978816.js | 3 + ... => chunk.a8fa5591357cce978816.js.LICENSE} | 172 ++--- .../panel/chunk.a8fa5591357cce978816.js.gz | Bin 0 -> 32883 bytes .../panel/chunk.a8fa5591357cce978816.js.map | 1 + .../api/panel/chunk.ad9001ac29bd3acbb520.js | 2 + .../panel/chunk.ad9001ac29bd3acbb520.js.gz | Bin 0 -> 12608 bytes .../panel/chunk.ad9001ac29bd3acbb520.js.map | 1 + .../api/panel/chunk.c77b56beea1d4547ff5f.js | 1 - .../panel/chunk.c77b56beea1d4547ff5f.js.gz | Bin 12316 -> 0 bytes .../api/panel/chunk.c93f37c558ff32991708.js | 1 - .../panel/chunk.c93f37c558ff32991708.js.gz | Bin 175 -> 0 bytes .../api/panel/chunk.f3880aa331d3ef2ddf32.js | 2 - .../panel/chunk.f3880aa331d3ef2ddf32.js.gz | Bin 32814 -> 0 bytes .../api/panel/chunk.ff92199b0d422767d108.js | 1 - .../panel/chunk.ff92199b0d422767d108.js.gz | Bin 2703 -> 0 bytes hassio/api/panel/entrypoint.js | 3 +- hassio/api/panel/entrypoint.js.gz | Bin 1193 -> 1229 bytes hassio/api/panel/entrypoint.js.map | 1 + home-assistant-polymer | 2 +- 37 files changed, 768 insertions(+), 509 deletions(-) delete mode 100644 hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js delete mode 100644 hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js.gz create mode 100644 hassio/api/panel/chunk.457ac71b0904d7243237.js create mode 100644 hassio/api/panel/chunk.457ac71b0904d7243237.js.gz create mode 100644 hassio/api/panel/chunk.457ac71b0904d7243237.js.map create mode 100644 hassio/api/panel/chunk.57f5b43a82b988080555.js create mode 100644 hassio/api/panel/chunk.57f5b43a82b988080555.js.gz create mode 100644 hassio/api/panel/chunk.57f5b43a82b988080555.js.map create mode 100644 hassio/api/panel/chunk.72a6da063fe4cb6308e8.js create mode 100644 hassio/api/panel/chunk.72a6da063fe4cb6308e8.js.LICENSE create mode 100644 hassio/api/panel/chunk.72a6da063fe4cb6308e8.js.gz create mode 100644 hassio/api/panel/chunk.72a6da063fe4cb6308e8.js.map create mode 100644 hassio/api/panel/chunk.7ee37c2565bcf2d88182.js create mode 100644 hassio/api/panel/chunk.7ee37c2565bcf2d88182.js.gz create mode 100644 hassio/api/panel/chunk.7ee37c2565bcf2d88182.js.map delete mode 100644 hassio/api/panel/chunk.a8e86d80be46b3b6e16d.js delete mode 100644 hassio/api/panel/chunk.a8e86d80be46b3b6e16d.js.LICENSE delete mode 100644 hassio/api/panel/chunk.a8e86d80be46b3b6e16d.js.gz create mode 100644 hassio/api/panel/chunk.a8fa5591357cce978816.js rename hassio/api/panel/{chunk.f3880aa331d3ef2ddf32.js.LICENSE => chunk.a8fa5591357cce978816.js.LICENSE} (95%) create mode 100644 hassio/api/panel/chunk.a8fa5591357cce978816.js.gz create mode 100644 hassio/api/panel/chunk.a8fa5591357cce978816.js.map create mode 100644 hassio/api/panel/chunk.ad9001ac29bd3acbb520.js create mode 100644 hassio/api/panel/chunk.ad9001ac29bd3acbb520.js.gz create mode 100644 hassio/api/panel/chunk.ad9001ac29bd3acbb520.js.map delete mode 100644 hassio/api/panel/chunk.c77b56beea1d4547ff5f.js delete mode 100644 hassio/api/panel/chunk.c77b56beea1d4547ff5f.js.gz delete mode 100644 hassio/api/panel/chunk.c93f37c558ff32991708.js delete mode 100644 hassio/api/panel/chunk.c93f37c558ff32991708.js.gz delete mode 100644 hassio/api/panel/chunk.f3880aa331d3ef2ddf32.js delete mode 100644 hassio/api/panel/chunk.f3880aa331d3ef2ddf32.js.gz delete mode 100644 hassio/api/panel/chunk.ff92199b0d422767d108.js delete mode 100644 hassio/api/panel/chunk.ff92199b0d422767d108.js.gz create mode 100644 hassio/api/panel/entrypoint.js.map diff --git a/hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js b/hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js deleted file mode 100644 index e5a0089c4..000000000 --- a/hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js +++ /dev/null @@ -1 +0,0 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[2],{2:function(e,n,t){"use strict";t.r(n),t(75);var o=t(4),a=t(6),r=(t(27),t(74),t(56),t(20),t(11)),s=function(){function e(e,n){for(var t=0;t\n .invisible {\n visibility: hidden;\n }\n \n \n'],{raw:{value:Object.freeze(['\n \n \n'])}})),c=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),l(n,[{key:"computeMenuButtonClass",value:function(e,n){return!e&&n?"invisible":""}},{key:"toggleMenu",value:function(e){e.stopPropagation(),this.fire("hass-open-menu")}},{key:"_getIcon",value:function(e){return(e?"hassio":"hass")+":menu"}}],[{key:"template",get:function(){return Object(o.a)(p)}},{key:"properties",get:function(){return{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassio:{type:Boolean,value:!1}}}}]),n}();customElements.define("ha-menu-button",c);var d=function(){function e(e,n){for(var t=0;t\n .placeholder {\n height: 100%;\n }\n\n .layout {\n height: calc(100% - 64px);\n }\n \n\n
\n \n \n
[[title]]
\n
\n
\n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n\n
\n \n \n
[[title]]
\n
\n
\n \n
\n
\n'])}})),h=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),d(n,null,[{key:"template",get:function(){return Object(o.a)(u)}},{key:"properties",get:function(){return{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},title:{type:String,value:""}}}}]),n}();customElements.define("hass-loading-screen",h),t(72),t(57),t(101),t(3);var f=document.createElement("template");f.setAttribute("style","display: none;"),f.innerHTML='\n \n\n \n\n \n\n \n',document.head.appendChild(f.content),t(70),t(55);var b=function(){function e(e,n){for(var t=0;t\n paper-dialog {\n min-width: 350px;\n font-size: 14px;\n border-radius: 2px;\n }\n app-toolbar {\n margin: 0;\n padding: 0 16px;\n color: var(--primary-text-color);\n background-color: var(--secondary-background-color);\n }\n app-toolbar [main-title] {\n margin-left: 16px;\n }\n paper-checkbox {\n display: block;\n margin: 4px;\n }\n @media all and (max-width: 450px), all and (max-height: 500px) {\n paper-dialog {\n max-height: 100%;\n }\n paper-dialog::before {\n content: "";\n position: fixed;\n z-index: -1;\n top: 0px;\n left: 0px;\n right: 0px;\n bottom: 0px;\n background-color: inherit;\n }\n app-toolbar {\n color: var(--text-primary-color);\n background-color: var(--primary-color);\n }\n }\n \n \n \n \n
[[title]]
\n
\n \n \n \n
\n'],{raw:{value:Object.freeze(['\n \n \n \n \n
[[title]]
\n
\n \n \n \n
\n'])}})),w=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),v(n,[{key:"openDialog",value:function(){this.$.dialog.open()}}],[{key:"template",get:function(){return Object(o.a)(g)}},{key:"properties",get:function(){return{title:String,content:String}}}]),n}();customElements.define("hassio-markdown-dialog",w),t(93),t(13),t(12),t(86),t(84),t(92);var k=function(){function e(e,n){for(var t=0;t\n :host,\n paper-card,\n paper-dropdown-menu {\n display: block;\n }\n .errors {\n color: var(--google-red-500);\n margin-bottom: 16px;\n }\n paper-item {\n width: 450px;\n }\n .card-actions {\n text-align: right;\n }\n \n \n
\n \n\n \n \n \n \n \n \n \n \n \n \n
\n
\n Save\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n \n\n \n \n \n \n \n \n \n \n \n \n
\n
\n Save\n
\n
\n'])}})),_=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),k(n,[{key:"addonChanged",value:function(e){var n=this;if(this.setProperties({selectedInput:e.audio_input||"null",selectedOutput:e.audio_output||"null"}),!this.outputDevices){var t=[{device:"null",name:"-"}];this.hass.callApi("get","hassio/hardware/audio").then(function(e){var o=e.data.audio,a=Object.keys(o.input).map(function(e){return{device:e,name:o.input[e]}}),r=Object.keys(o.output).map(function(e){return{device:e,name:o.output[e]}});n.setProperties({inputDevices:t.concat(a),outputDevices:t.concat(r)})},function(){n.setProperties({inputDevices:t,outputDevices:t})})}}},{key:"_saveSettings",value:function(){var e=this;this.error=null;var n="hassio/addons/"+this.addon.slug+"/options";this.hass.callApi("post",n,{audio_input:"null"===this.selectedInput?null:this.selectedInput,audio_output:"null"===this.selectedOutput?null:this.selectedOutput}).then(function(){e.fire("hass-api-called",{success:!0,path:n})},function(n){e.error=n.body.message})}}],[{key:"template",get:function(){return Object(o.a)(O)}},{key:"properties",get:function(){return{hass:Object,addon:{type:Object,observer:"addonChanged"},inputDevices:Array,outputDevices:Array,selectedInput:String,selectedOutput:String,error:String}}}]),n}();customElements.define("hassio-addon-audio",_),t(91);var j=function(){function e(e,n){for(var t=0;t\n .container {\n position: relative;\n display: inline-block;\n }\n\n paper-button {\n transition: all 1s;\n }\n\n .success paper-button {\n color: white;\n background-color: var(--google-green-500);\n transition: none;\n }\n\n .error paper-button {\n color: white;\n background-color: var(--google-red-500);\n transition: none;\n }\n\n paper-button[disabled] {\n color: #c8c8c8;\n }\n\n .progress {\n @apply --layout;\n @apply --layout-center-center;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n }\n \n
\n \n \n \n \n
\n'],{raw:{value:Object.freeze(['\n \n
\n \n \n \n \n
\n'])}})),S=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),j(n,[{key:"tempClass",value:function(e){var n=this.$.container.classList;n.add(e),setTimeout(function(){n.remove(e)},1e3)}},{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("click",function(n){return e.buttonTapped(n)})}},{key:"buttonTapped",value:function(e){this.progress&&e.stopPropagation()}},{key:"actionSuccess",value:function(){this.tempClass("success")}},{key:"actionError",value:function(){this.tempClass("error")}},{key:"computeDisabled",value:function(e,n){return e||n}}],[{key:"template",get:function(){return Object(o.a)(x)}},{key:"properties",get:function(){return{hass:{type:Object},progress:{type:Boolean,value:!1},disabled:{type:Boolean,value:!1}}}}]),n}();customElements.define("ha-progress-button",S);var P=function(){function e(e,n){for(var t=0;t\n'],{raw:{value:Object.freeze(['\n \n'])}})),E=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),P(n,[{key:"buttonTapped",value:function(){var e=this;this.progress=!0;var n={method:this.method,path:this.path,data:this.data};this.hass.callApi(this.method,this.path,this.data).then(function(t){e.progress=!1,e.$.progress.actionSuccess(),n.success=!0,n.response=t},function(t){e.progress=!1,e.$.progress.actionError(),n.success=!1,n.response=t}).then(function(){e.fire("hass-api-called",n)})}}],[{key:"template",get:function(){return Object(o.a)(C)}},{key:"properties",get:function(){return{hass:Object,progress:{type:Boolean,value:!1},path:String,method:{type:String,value:"POST"},data:{type:Object,value:{}},disabled:{type:Boolean,value:!1}}}}]),n}();customElements.define("ha-call-api-button",E);var T=function(){function e(e,n){for(var t=0;t\n :host {\n display: block;\n }\n paper-card {\n display: block;\n }\n .card-actions {\n @apply --layout;\n @apply --layout-justified;\n }\n .errors {\n color: var(--google-red-500);\n margin-bottom: 16px;\n }\n iron-autogrow-textarea {\n width: 100%;\n font-family: monospace;\n }\n .syntaxerror {\n color: var(--google-red-500);\n }\n \n \n
\n \n \n
\n
\n Reset to defaults\n Save\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n \n \n
\n
\n Reset to defaults\n Save\n
\n
\n'])}})),D=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),T(n,[{key:"addonChanged",value:function(e){this.config=e?JSON.stringify(e.options,null,2):""}},{key:"configChanged",value:function(e){try{this.$.config.classList.remove("syntaxerror"),this.configParsed=JSON.parse(e)}catch(e){this.$.config.classList.add("syntaxerror"),this.configParsed=null}}},{key:"saveTapped",value:function(){var e=this;this.error=null,this.hass.callApi("post","hassio/addons/"+this.addonSlug+"/options",{options:this.configParsed}).catch(function(n){e.error=n.body.message})}}],[{key:"template",get:function(){return Object(o.a)(A)}},{key:"properties",get:function(){return{hass:Object,addon:{type:Object,observer:"addonChanged"},addonSlug:String,config:{type:String,observer:"configChanged"},configParsed:Object,error:String,resetData:{type:Object,value:{options:null}}}}}]),n}();customElements.define("hassio-addon-config",D),t(19),t(90);var R=t(7),I=[60,"second",60,"minute",24,"hour",7,"day"],z=t(76),q=(t(82),t(85)),L={__localizationCache:{requests:{},messages:{},ajax:null},properties:{language:{type:String},resources:{type:Object},formats:{type:Object,value:function(){return{}}},useKeyIfMissing:{type:Boolean,value:!1},localize:{type:Function,computed:"__computeLocalize(language, resources, formats)"},bubbleEvent:{type:Boolean,value:!1}},loadResources:function(e,n,t){var o=this.constructor.prototype;this.__checkLocalizationCache(o);var a,r=o.__localizationCache.ajax;function s(e){this.__onRequestResponse(e,n,t)}r||(r=o.__localizationCache.ajax=document.createElement("iron-ajax")),(a=o.__localizationCache.requests[e])?a.completes.then(s.bind(this),this.__onRequestError.bind(this)):(r.url=e,(a=r.generateRequest()).completes.then(s.bind(this),this.__onRequestError.bind(this)),o.__localizationCache.requests[e]=a)},__computeLocalize:function(e,n,t){var o=this.constructor.prototype;return this.__checkLocalizationCache(o),o.__localizationCache||(o.__localizationCache={requests:{},messages:{},ajax:null}),o.__localizationCache.messages={},function(){var a=arguments[0];if(a&&n&&e&&n[e]){var r=n[e][a];if(!r)return this.useKeyIfMissing?a:"";var s=a+r,i=o.__localizationCache.messages[s];i||(i=new q.a(r,e,t),o.__localizationCache.messages[s]=i);for(var l={},p=1;p=0?"past":"future";n=Math.abs(n);for(var o=0;o\n iron-icon {\n margin-right: 16px;\n margin-top: 16px;\n float: left;\n color: var(--secondary-text-color);\n }\n iron-icon.update {\n color: var(--paper-orange-400);\n }\n iron-icon.running,\n iron-icon.installed {\n color: var(--paper-green-400);\n }\n iron-icon.hassupdate,\n iron-icon.snapshot {\n color: var(--paper-item-icon-color);\n }\n .title {\n color: var(--primary-text-color);\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n }\n .addition {\n color: var(--secondary-text-color);\n overflow: hidden;\n position: relative;\n height: 2.4em;\n line-height: 1.2em;\n }\n ha-relative-time {\n display: block;\n }\n \n \n
\n
[[title]]
\n
\n \n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n
[[title]]
\n
\n \n \n
\n
\n'])}})),W=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),N(n,null,[{key:"template",get:function(){return Object(o.a)($)}},{key:"properties",get:function(){return{hass:Object,title:String,description:String,datetime:String,icon:{type:String,value:"hass:help-circle"},iconTitle:String,iconClass:String}}}]),n}();customElements.define("hassio-card-content",W);var Y=function(){function e(e,n){for(var t=0;t\n :host {\n display: block;\n }\n paper-card {\n display: block;\n margin-bottom: 16px;\n }\n .addon-header {\n @apply --paper-font-headline;\n }\n .light-color {\n color: var(--secondary-text-color);\n }\n .addon-version {\n float: right;\n font-size: 15px;\n vertical-align: middle;\n }\n .description {\n margin-bottom: 16px;\n }\n .logo img {\n max-height: 60px;\n margin: 16px 0;\n display: block;\n }\n .state div{\n width: 150px;\n display: inline-block;\n }\n paper-toggle-button {\n display: inline;\n }\n iron-icon.running {\n color: var(--paper-green-400);\n }\n iron-icon.stopped {\n color: var(--google-red-300);\n }\n ha-call-api-button {\n font-weight: 500;\n color: var(--primary-color);\n }\n .right {\n float: right;\n }\n ha-markdown img {\n max-width: 100%;\n }\n \n \n\n \n
\n
[[addon.name]]\n
\n \n \n
\n
\n
\n [[addon.description]].
\n Visit [[addon.name]] page for details.\n
\n \n \n
\n
\n \n \n
\n
\n \n'],{raw:{value:Object.freeze(['\n \n \n\n \n
\n
[[addon.name]]\n
\n \n \n
\n
\n
\n [[addon.description]].
\n Visit [[addon.name]] page for details.\n
\n \n \n
\n
\n \n \n
\n
\n \n'])}})),G=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),Y(n,[{key:"computeIsRunning",value:function(e){return e&&"started"===e.state}},{key:"computeUpdateAvailable",value:function(e){return e&&!e.detached&&e.version&&e.version!==e.last_version}},{key:"pathWebui",value:function(e){return e&&e.replace("[HOST]",document.location.hostname)}},{key:"computeShowWebUI",value:function(e,n){return e&&n}},{key:"computeStartOnBoot",value:function(e){return"auto"===e}},{key:"startOnBootToggled",value:function(){var e={boot:"auto"===this.addon.boot?"manual":"auto"};this.hass.callApi("POST","hassio/addons/"+this.addonSlug+"/options",e)}},{key:"autoUpdateToggled",value:function(){var e={auto_update:!this.addon.auto_update};this.hass.callApi("POST","hassio/addons/"+this.addonSlug+"/options",e)}},{key:"openChangelog",value:function(){var e=this;this.hass.callApi("get","hassio/addons/"+this.addonSlug+"/changelog").then(function(e){return e},function(){return"Error getting changelog"}).then(function(n){e.fire("hassio-markdown-dialog",{title:"Changelog",content:n})})}},{key:"_unistallClicked",value:function(){var e=this;if(confirm("Are you sure you want to uninstall this add-on?")){var n="hassio/addons/"+this.addonSlug+"/uninstall",t={path:n};this.hass.callApi("post",n).then(function(e){t.success=!0,t.response=e},function(e){t.success=!1,t.response=e}).then(function(){e.fire("hass-api-called",t)})}}}],[{key:"template",get:function(){return Object(o.a)(J)}},{key:"properties",get:function(){return{hass:Object,addon:Object,addonSlug:String,isRunning:{type:Boolean,computed:"computeIsRunning(addon)"}}}}]),n}();customElements.define("hassio-addon-info",G);var V=function(){function e(e,n){for(var t=0;t\n :host,\n paper-card {\n display: block;\n }\n pre {\n overflow-x: auto;\n }\n \n \n
\n
[[log]]
\n
\n
\n Refresh\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n
[[log]]
\n
\n
\n Refresh\n
\n
\n'])}})),K=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),V(n,[{key:"addonSlugChanged",value:function(e){var n=this;this.hass?this.refresh():setTimeout(function(){n.addonChanged(e)},0)}},{key:"refresh",value:function(){var e=this;this.hass.callApi("get","hassio/addons/"+this.addonSlug+"/logs").then(function(n){e.log=n})}}],[{key:"template",get:function(){return Object(o.a)(X)}},{key:"properties",get:function(){return{hass:Object,addonSlug:{type:String,observer:"addonSlugChanged"},log:String}}}]),n}();customElements.define("hassio-addon-logs",K),t(24);var Q=function(){function e(e,n){for(var t=0;t\n :host {\n display: block;\n }\n paper-card {\n display: block;\n }\n .errors {\n color: var(--google-red-500);\n margin-bottom: 16px;\n }\n .card-actions {\n @apply --layout;\n @apply --layout-justified;\n }\n \n \n
\n \n\n \n \n \n \n \n \n
ContainerHost
\n
\n
\n Reset to defaults\n Save\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n \n\n \n \n \n \n \n \n
ContainerHost
\n
\n
\n Reset to defaults\n Save\n
\n
\n'])}})),ee=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),Q(n,[{key:"addonChanged",value:function(e){if(e){var n=e.network||{},t=Object.keys(n).map(function(e){return{container:e,host:n[e]}});this.config=t.sort(function(e,n){return e.host-n.host})}}},{key:"saveTapped",value:function(){var e=this;this.error=null;var n={};this.config.forEach(function(e){n[e.container]=parseInt(e.host)});var t="hassio/addons/"+this.addonSlug+"/options";this.hass.callApi("post",t,{network:n}).then(function(){e.fire("hass-api-called",{success:!0,path:t})},function(n){e.error=n.body.message})}}],[{key:"template",get:function(){return Object(o.a)(Z)}},{key:"properties",get:function(){return{hass:Object,addonSlug:String,config:Object,addon:{type:Object,observer:"addonChanged"},error:String,resetData:{type:Object,value:{network:null}}}}}]),n}();customElements.define("hassio-addon-network",ee);var ne=function(){function e(e,n){for(var t=0;t\n :host {\n color: var(--primary-text-color);\n --paper-card-header-color: var(--primary-text-color);\n }\n .content {\n padding: 24px 0 32px;\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n hassio-addon-info,\n hassio-addon-network,\n hassio-addon-audio,\n hassio-addon-config {\n margin-bottom: 24px;\n width: 600px;\n }\n hassio-addon-logs {\n max-width: calc(100% - 8px);\n min-width: 600px;\n }\n @media only screen and (max-width: 600px) {\n hassio-addon-info,\n hassio-addon-network,\n hassio-addon-audio,\n hassio-addon-config,\n hassio-addon-logs {\n max-width: 100%;\n min-width: 100%;\n }\n }\n \n \n \n \n \n \n \n
Hass.io: add-on details
\n
\n
\n
\n \n\n \n
\n
\n\n \n'],{raw:{value:Object.freeze(['\n \n \n \n \n \n \n \n
Hass.io: add-on details
\n
\n
\n
\n \n\n \n
\n
\n\n \n'])}})),oe=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),ne(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)}),this.addEventListener("hassio-markdown-dialog",function(n){return e.openMarkdown(n)})}},{key:"apiCalled",value:function(e){var n=e.detail.path;n&&("uninstall"===n.substr(n.lastIndexOf("/")+1)?this.backTapped():this.routeDataChanged(this.routeData))}},{key:"routeDataChanged",value:function(e){var n=this;this.routeMatches&&e&&e.slug&&this.hass.callApi("get","hassio/addons/"+e.slug+"/info").then(function(e){n.addon=e.data},function(){n.addon=null})}},{key:"backTapped",value:function(){history.back()}},{key:"openMarkdown",value:function(e){this.setProperties({markdownTitle:e.detail.title,markdownContent:e.detail.content}),this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog()}}],[{key:"template",get:function(){return Object(o.a)(te)}},{key:"properties",get:function(){return{hass:Object,showMenu:Boolean,narrow:Boolean,route:Object,routeData:{type:Object,observer:"routeDataChanged"},routeMatches:Boolean,addon:Object,markdownTitle:String,markdownContent:{type:String,value:""}}}}]),n}();customElements.define("hassio-addon-view",oe);var ae=function(){function e(e,n){for(var t=0;t1&&void 0!==arguments[1]&&arguments[1]?history.replaceState(null,null,e):history.pushState(null,null,e),this.fire("location-changed")}}]),t}()}),le=function(){function e(e,n){for(var t=0;t\n paper-card {\n cursor: pointer;\n }\n a.repo {\n display: block;\n color: var(--primary-text-color);\n }\n \n \n'],{raw:{value:Object.freeze(['\n \n \n'])}})),ce=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,ie(a.a)),le(n,[{key:"sortAddons",value:function(e,n){return e.name\n .add {\n padding: 12px 16px;\n }\n iron-icon {\n color: var(--secondary-text-color);\n margin-right: 16px;\n display: inline-block;\n }\n paper-input {\n width: calc(100% - 49px);\n display: inline-block;\n }\n \n
\n
\n Repositories\n
\n Configure which add-on repositories to fetch data from:\n
\n
\n \n \n
\n \n \n
\n
\n Add\n
\n
\n
\n'],{raw:{value:Object.freeze(['\n \n
\n
\n Repositories\n
\n Configure which add-on repositories to fetch data from:\n
\n
\n \n \n
\n \n \n
\n
\n Add\n
\n
\n
\n'])}})),he=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),de(n,[{key:"reposChanged",value:function(e){this.repoList=e.filter(function(e){return"core"!==e.slug&&"local"!==e.slug}),this.repoUrl=""}},{key:"sortRepos",value:function(e,n){return e.name\n hassio-addon-repository {\n margin-top: 24px;\n }\n \n \n\n \n'],{raw:{value:Object.freeze(['\n \n \n\n \n'])}})),me=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),fe(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)}),this.loadData()}},{key:"apiCalled",value:function(e){e.detail.success&&this.loadData()}},{key:"sortRepos",value:function(e,n){return"local"===e.slug?-1:"local"===n.slug?1:"core"===e.slug?-1:"core"===n.slug?1:e.name\n paper-card {\n cursor: pointer;\n }\n \n
\n
Add-ons
\n \n \n
\n'],{raw:{value:Object.freeze(['\n \n
\n
Add-ons
\n \n \n
\n'])}})),ge=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,ie(a.a)),ye(n,[{key:"sortAddons",value:function(e,n){return e.name\n paper-card {\n display: block;\n margin-bottom: 32px;\n }\n .errors {\n color: var(--google-red-500);\n margin-top: 16px;\n }\n \n \n'],{raw:{value:Object.freeze(['\n \n \n'])}})),_e=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),ke(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)})}},{key:"apiCalled",value:function(e){if(e.detail.success)this.errors=null;else{var n=e.detail.response;"object"===we(n.body)?this.errors=n.body.message||"Unknown error":this.errors=n.body}}},{key:"computeUpdateAvailable",value:function(e){return e.version!==e.last_version}}],[{key:"template",get:function(){return Object(o.a)(Oe)}},{key:"properties",get:function(){return{hass:Object,hassInfo:Object,error:String}}}]),n}();customElements.define("hassio-hass-update",_e);var je=function(){function e(e,n){for(var t=0;t\n .content {\n margin: 0 auto;\n }\n \n
\n \n \n
\n'],{raw:{value:Object.freeze(['\n \n
\n \n \n
\n'])}})),Se=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),je(n,null,[{key:"template",get:function(){return Object(o.a)(xe)}},{key:"properties",get:function(){return{hass:Object,supervisorInfo:Object,hassInfo:Object}}}]),n}();customElements.define("hassio-dashboard",Se),t(61);var Pe=function(){function e(e,n){for(var t=0;t\n paper-dialog {\n min-width: 350px;\n font-size: 14px;\n border-radius: 2px;\n }\n app-toolbar {\n margin: 0;\n padding: 0 16px;\n color: var(--primary-text-color);\n background-color: var(--secondary-background-color);\n }\n app-toolbar [main-title] {\n margin-left: 16px;\n }\n paper-dialog-scrollable {\n margin: 0;\n }\n paper-checkbox {\n display: block;\n margin: 4px;\n }\n @media all and (max-width: 450px), all and (max-height: 500px) {\n paper-dialog {\n max-height: 100%;\n height: 100%;\n }\n app-toolbar {\n color: var(--text-primary-color);\n background-color: var(--primary-color);\n }\n }\n .details {\n color: var(--secondary-text-color);\n }\n .download {\n color: var(--primary-color);\n }\n .warning,\n .error {\n color: var(--google-red-500);\n }\n \n \n \n \n
[[_computeName(snapshot)]]
\n
\n
\n [[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])
\n [[_formatDatetime(snapshot.date)]]\n
\n
Home Assistant:
\n \n Home Assistant [[snapshot.homeassistant]]\n \n \n \n \n \n
\n \n \n \n \n Restore selected\n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n \n \n
[[_computeName(snapshot)]]
\n
\n
\n [[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])
\n [[_formatDatetime(snapshot.date)]]\n
\n
Home Assistant:
\n \n Home Assistant [[snapshot.homeassistant]]\n \n \n \n \n \n
\n \n \n \n \n Restore selected\n \n
\n
\n'])}})),Ee=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),Pe(n,[{key:"_snapshotSlugChanged",value:function(e){var n=this;e&&"update"!==e&&this.hass.callApi("get","hassio/snapshots/"+e+"/info").then(function(e){e.data.folders=n._computeFolders(e.data.folders),e.data.addons=n._computeAddons(e.data.addons),n.snapshot=e.data,n.$.dialog.open()},function(){n.snapshot=null})}},{key:"_computeFolders",value:function(e){var n=[];return e.includes("homeassistant")&&n.push({slug:"homeassistant",name:"Home Assistant configuration",checked:!0}),e.includes("ssl")&&n.push({slug:"ssl",name:"SSL",checked:!0}),e.includes("share")&&n.push({slug:"share",name:"Share",checked:!0}),e.includes("addons/local")&&n.push({slug:"addons/local",name:"Local add-ons",checked:!0}),n}},{key:"_computeAddons",value:function(e){return e.map(function(e){return{slug:e.slug,name:e.name,version:e.version,checked:!0}})}},{key:"_isFullSnapshot",value:function(e){return"full"===e}},{key:"_partialRestoreClicked",value:function(){var e=this;if(confirm("Are you sure you want to restore this snapshot?")){var n=this.snapshot.addons.filter(function(e){return e.checked}).map(function(e){return e.slug}),t=this.snapshot.folders.filter(function(e){return e.checked}).map(function(e){return e.slug}),o={homeassistant:this.restoreHass,addons:n,folders:t};this.snapshot.protected&&(o.password=this.snapshotPassword),this.hass.callApi("post","hassio/snapshots/"+this.snapshotSlug+"/restore/partial",o).then(function(){alert("Snapshot restored!"),e.$.dialog.close()},function(n){e.error=n.body.message})}}},{key:"_fullRestoreClicked",value:function(){var e=this;if(confirm("Are you sure you want to restore this snapshot?")){var n=this.snapshot.protected?{password:this.snapshotPassword}:null;this.hass.callApi("post","hassio/snapshots/"+this.snapshotSlug+"/restore/full",n).then(function(){alert("Snapshot restored!"),e.$.dialog.close()},function(n){e.error=n.body.message})}}},{key:"_deleteClicked",value:function(){var e=this;confirm("Are you sure you want to delete this snapshot?")&&this.hass.callApi("post","hassio/snapshots/"+this.snapshotSlug+"/remove").then(function(){e.$.dialog.close(),e.snapshotDeleted=!0},function(n){e.error=n.body.message})}},{key:"_computeDownloadUrl",value:function(e){return"/api/hassio/snapshots/"+e+"/download?api_password="+encodeURIComponent(this.hass.connection.options.authToken)}},{key:"_computeDownloadName",value:function(e){return"Hass_io_"+this._computeName(e).replace(/[^a-z0-9]+/gi,"_")+".tar"}},{key:"_computeName",value:function(e){return e.name||e.slug}},{key:"_computeType",value:function(e){return"full"===e?"Full snapshot":"Partial snapshot"}},{key:"_computeSize",value:function(e){return Math.ceil(10*e)/10+" MB"}},{key:"_sortAddons",value:function(e,n){return e.name\n paper-radio-group {\n display: block;\n }\n paper-radio-button {\n padding: 0 0 2px 2px;\n }\n paper-radio-button,\n paper-checkbox,\n paper-input[type="password"] {\n display: block;\n margin: 4px 0 4px 48px;\n }\n .pointer {\n cursor: pointer;\n }\n \n
\n
\n
\n Create snapshot\n
\n Snapshots allow you to easily backup and\n restore all data of your Hass.io instance.\n
\n
\n \n
\n \n Type:\n \n \n Full snapshot\n \n \n Partial snapshot\n \n \n \n Security:\n Password protection\n \n \n
\n
\n Create\n
\n
\n
\n\n
\n
Available snapshots
\n \n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n
\n
\n
\n Create snapshot\n
\n Snapshots allow you to easily backup and\n restore all data of your Hass.io instance.\n
\n
\n \n
\n \n Type:\n \n \n Full snapshot\n \n \n Partial snapshot\n \n \n \n Security:\n Password protection\n \n \n
\n
\n Create\n
\n
\n
\n\n
\n
Available snapshots
\n \n \n
\n
\n'])}})),De=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),Te(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e._apiCalled(n)}),this._updateSnapshots()}},{key:"_apiCalled",value:function(e){e.detail.success&&this._updateSnapshots()}},{key:"_updateSnapshots",value:function(){var e=this;this.hass.callApi("get","hassio/snapshots").then(function(n){e.snapshots=n.data.snapshots},function(n){e.error=n.message})}},{key:"_createSnapshot",value:function(){var e=this;if(this.error="",!this.snapshotHasPassword||this.snapshotPassword.length){this.creatingSnapshot=!0;var n=this.snapshotName;n.length||(n=(new Date).toLocaleDateString(navigator.language,{weekday:"long",year:"numeric",month:"short",day:"numeric"}));var t=void 0,o=void 0;if("full"===this.snapshotType)t={name:n},o="hassio/snapshots/new/full";else{var a=this.addonList.filter(function(e){return e.checked}).map(function(e){return e.slug});t={name:n,folders:this.folderList.filter(function(e){return e.checked}).map(function(e){return e.slug}),addons:a},o="hassio/snapshots/new/partial"}this.snapshotHasPassword&&(t.password=this.snapshotPassword),this.hass.callApi("post",o,t).then(function(){e.creatingSnapshot=!1,e.fire("hass-api-called",{success:!0})},function(n){e.creatingSnapshot=!1,e.error=n.message})}else this.error="Please enter a password."}},{key:"_installedAddonsChanged",value:function(e){this.addonList=e.map(function(e){return{slug:e.slug,name:e.name,checked:!0}})}},{key:"_sortAddons",value:function(e,n){return e.name\n paper-card {\n display: inline-block;\n width: 400px;\n margin-left: 8px;\n }\n .card-content {\n height: 200px;\n }\n @media screen and (max-width: 830px) {\n paper-card {\n margin-top: 8px;\n margin-left: 0;\n width: 100%;\n }\n .card-content {\n height: 100%;\n }\n }\n .info {\n width: 100%;\n }\n .info td:nth-child(2) {\n text-align: right;\n }\n .errors {\n color: var(--google-red-500);\n margin-top: 16px;\n }\n paper-button.info {\n max-width: calc(50% - 12px);\n }\n \n \n
\n

Host system

\n \n \n \n \n \n \n \n \n \n \n
Hostname[[data.hostname]]
System[[data.operating_system]]
\n \n Hardware\n \n \n \n
\n
\n \n \n \n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n

Host system

\n \n \n \n \n \n \n \n \n \n \n
Hostname[[data.hostname]]
System[[data.operating_system]]
\n \n Hardware\n \n \n \n
\n
\n \n \n \n \n
\n
\n'])}})),qe=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),Ie(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)})}},{key:"apiCalled",value:function(e){if(e.detail.success)this.errors=null;else{var n=e.detail.response;"object"===Re(n.body)?this.errors=n.body.message||"Unknown error":this.errors=n.body}}},{key:"_dataChanged",value:function(e){var n=this;e.features&&e.features.includes("hassos")?this.hass.callApi("get","hassio/hassos/info").then(function(e){n._hassOs=e.data}):this._hassOs={}}},{key:"_computeUpdateAvailable",value:function(e){return e&&e.version!==e.version_latest}},{key:"_featureAvailable",value:function(e,n){return e&&e.features&&e.features.includes(n)}},{key:"_showHardware",value:function(){var e=this;this.hass.callApi("get","hassio/hardware/info").then(function(n){return e._objectToMarkdown(n.data)},function(){return"Error getting hardware info"}).then(function(n){e.fire("hassio-markdown-dialog",{title:"Hardware",content:n})})}},{key:"_objectToMarkdown",value:function(e){var n=this,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o="";return Object.keys(e).forEach(function(a){"object"!==Re(e[a])?o+=t+"- "+a+": "+e[a]+"\n":(o+=t+"- "+a+":\n",Array.isArray(e[a])?e[a].length&&(o+=t+" - "+e[a].join("\n"+t+" - ")+"\n"):o+=n._objectToMarkdown(e[a]," "+t))}),o}},{key:"_changeHostnameClicked",value:function(){var e=this.data.hostname,n=prompt("Please enter a new hostname:",e);n&&n!==e&&this.hass.callApi("post","hassio/host/options",{hostname:n})}}],[{key:"template",get:function(){return Object(o.a)(ze)}},{key:"properties",get:function(){return{hass:Object,data:{type:Object,observer:"_dataChanged"},errors:String,_hassOs:Object}}}]),n}();customElements.define("hassio-host-info",qe);var Le="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},He=function(){function e(e,n){for(var t=0;t\n paper-card {\n display: inline-block;\n width: 400px;\n }\n .card-content {\n height: 200px;\n }\n @media screen and (max-width: 830px) {\n paper-card {\n width: 100%;\n }\n .card-content {\n height: 100%;\n }\n }\n .info {\n width: 100%;\n }\n .info td:nth-child(2) {\n text-align: right;\n }\n .errors {\n color: var(--google-red-500);\n margin-top: 16px;\n }\n \n \n
\n

Hass.io supervisor

\n \n \n \n \n \n \n \n \n \n \n
Version\n [[data.version]]\n
Latest version[[data.last_version]]
\n \n
\n
\n Reload\n \n \n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n

Hass.io supervisor

\n \n \n \n \n \n \n \n \n \n \n
Version\n [[data.version]]\n
Latest version[[data.last_version]]
\n \n
\n
\n Reload\n \n \n \n
\n
\n'])}})),Ue=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,i(a.a)),He(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)})}},{key:"apiCalled",value:function(e){if(e.detail.success)this.errors=null;else{var n=e.detail.response;"object"===Le(n.body)?this.errors=n.body.message||"Unknown error":this.errors=n.body}}},{key:"computeUpdateAvailable",value:function(e){return e.version!==e.last_version}},{key:"_equals",value:function(e,n){return e===n}},{key:"_joinBeta",value:function(){var e=this;if(confirm("WARNING:\nBeta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.\n\nThis inludes beta releases for:\n- Home Assistant (Release Candidates)\n- Hass.io supervisor\n- Host system")){var n="hassio/supervisor/options",t={channel:"beta"},o={method:"post",path:n,data:t};this.hass.callApi("post",n,t).then(function(e){o.success=!0,o.response=e},function(e){o.success=!1,o.response=e}).then(function(){e.fire("hass-api-called",o)})}}}],[{key:"template",get:function(){return Object(o.a)(Me)}},{key:"properties",get:function(){return{hass:Object,data:Object,errors:String,leaveBeta:{type:Object,value:{channel:"stable"}}}}}]),n}();customElements.define("hassio-supervisor-info",Ue);var Be=function(){function e(e,n){for(var t=0;t\n paper-card {\n display: block;\n }\n pre {\n overflow-x: auto;\n }\n \n \n
\n
[[log]]
\n
\n
\n Refresh\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n
[[log]]
\n
\n
\n Refresh\n
\n
\n'])}})),Ne=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),Be(n,[{key:"ready",value:function(){(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.loadData()}},{key:"loadData",value:function(){var e=this;this.hass.callApi("get","hassio/supervisor/logs").then(function(n){e.log=n},function(){e.log="Error fetching logs"})}},{key:"refreshTapped",value:function(){this.loadData()}}],[{key:"template",get:function(){return Object(o.a)(Fe)}},{key:"properties",get:function(){return{hass:Object,log:String}}}]),n}();customElements.define("hassio-supervisor-log",Ne);var $e=function(){function e(e,n){for(var t=0;t\n .content {\n margin: 4px;\n }\n .title {\n margin-top: 24px;\n color: var(--primary-text-color);\n font-size: 2em;\n padding-left: 8px;\n margin-bottom: 8px;\n }\n \n
\n
Information
\n \n \n
System log
\n \n
\n'],{raw:{value:Object.freeze(['\n \n
\n
Information
\n \n \n
System log
\n \n
\n'])}})),Ye=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,a.a),$e(n,null,[{key:"template",get:function(){return Object(o.a)(We)}},{key:"properties",get:function(){return{hass:Object,supervisorInfo:Object,hostInfo:Object}}}]),n}();customElements.define("hassio-system",Ye);var Je=function(){function e(e,n){for(var t=0;t\n :host {\n color: var(--primary-text-color);\n --paper-card-header-color: var(--primary-text-color);\n }\n paper-tabs {\n margin-left: 12px;\n --paper-tabs-selection-bar-color: #FFF;\n text-transform: uppercase;\n }\n \n \n \n \n \n
Hass.io
\n \n
\n \n Dashboard\n Snapshots\n Add-on store\n System\n \n
\n \n \n \n \n
\n\n \n\n \n'],{raw:{value:Object.freeze(['\n \n \n \n \n \n
Hass.io
\n \n
\n \n Dashboard\n Snapshots\n Add-on store\n System\n \n
\n \n \n \n \n
\n\n \n\n \n'])}})),Ve=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,ie(a.a)),Je(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hassio-markdown-dialog",function(n){return e.openMarkdown(n)})}},{key:"handlePageSelected",value:function(e){var n,t,o,a,r,s,i,l=e.detail.item.getAttribute("page-name");l!==this.page&&this.navigate("/hassio/"+l),n=this,t=this.$.layout.header.scrollTarget,o=t,a=Math.random(),r=Date.now(),s=o.scrollTop,i=0-s,n._currentAnimationId=a,function e(){var t,l=Date.now()-r;l>200?o.scrollTop=0:n._currentAnimationId===a&&(o.scrollTop=(t=l,-i*(t/=200)*(t-2)+s),requestAnimationFrame(e.bind(n)))}.call(n)}},{key:"equals",value:function(e,n){return e===n}},{key:"showRefreshButton",value:function(e){return"store"===e||"snapshots"===e}},{key:"refreshClicked",value:function(){"snapshots"===this.page?this.shadowRoot.querySelector("hassio-snapshots").refreshData():this.shadowRoot.querySelector("hassio-addon-store").refreshData()}},{key:"openMarkdown",value:function(e){this.setProperties({markdownTitle:e.detail.title,markdownContent:e.detail.content}),this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog()}}],[{key:"template",get:function(){return Object(o.a)(Ge)}},{key:"properties",get:function(){return{hass:Object,showMenu:Boolean,narrow:Boolean,page:String,supervisorInfo:Object,hostInfo:Object,hassInfo:Object,snapshotSlug:String,snapshotDeleted:Boolean,markdownTitle:String,markdownContent:{type:String,value:""}}}}]),n}();customElements.define("hassio-pages-with-tabs",Ve);var Xe=function(){function e(e,n){for(var t=0;t\n \n\n \n\n \n'],{raw:{value:Object.freeze(['\n \n \n\n \n\n \n'])}})),Ze=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,ie(a.a)),Xe(n,[{key:"ready",value:function(){var e=this;Ke(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),function(e,n,t){var o=arguments.length>3&&void 0!==arguments[3]&&arguments[3];e._themes||(e._themes={});var a=n.default_theme;("default"===t||t&&n.themes[t])&&(a=t);var r=Object.assign({},e._themes);if("default"!==a){var s=n.themes[a];Object.keys(s).forEach(function(n){var t="--"+n;e._themes[t]="",r[t]=s[n]})}if(e.updateStyles?e.updateStyles(r):window.ShadyCSS&&window.ShadyCSS.styleSubtree(e,r),o){var i=document.querySelector("meta[name=theme-color]");if(i){i.hasAttribute("default-content")||i.setAttribute("default-content",i.getAttribute("content"));var l=r["--primary-color"]||i.getAttribute("default-content");i.setAttribute("content",l)}}}(this,this.hass.themes,this.hass.selectedTheme,!0),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)})}},{key:"connectedCallback",value:function(){Ke(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"connectedCallback",this).call(this),this.routeChanged(this.route)}},{key:"apiCalled",value:function(e){var n=this;if(e.detail.success){var t=1;!function e(){n.$.data.refresh().catch(function(){t+=1,setTimeout(e,1e3*Math.min(t,5))})}()}}},{key:"computeIsLoaded",value:function(e,n,t){return null!==e&&null!==n&&null!==t}},{key:"routeChanged",value:function(e){""===e.path&&"/hassio"===e.prefix&&this.navigate("/hassio/dashboard",!0)}},{key:"equalsAddon",value:function(e){return e&&"addon"===e}}],[{key:"template",get:function(){return Object(o.a)(Qe)}},{key:"properties",get:function(){return{hass:Object,narrow:Boolean,showMenu:Boolean,route:{type:Object,value:{prefix:"/hassio",path:"/dashboard",__queryParams:{}},observer:"routeChanged"},routeData:Object,supervisorInfo:Object,hostInfo:Object,hassInfo:Object,loaded:{type:Boolean,computed:"computeIsLoaded(supervisorInfo, hostInfo, hassInfo)"}}}}]),n}();customElements.define("hassio-main",Ze)},25:function(e,n){var t=document.createElement("template");t.setAttribute("style","display: none;"),t.innerHTML='\n \n',document.head.appendChild(t.content)}}]); \ No newline at end of file diff --git a/hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js.gz b/hassio/api/panel/chunk.0ef4ef1053fe3d5107b5.js.gz deleted file mode 100644 index 68abb0644477d814fcd63649dd9b0629ed51c999..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19234 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 diff --git a/hassio/api/panel/chunk.457ac71b0904d7243237.js b/hassio/api/panel/chunk.457ac71b0904d7243237.js new file mode 100644 index 000000000..f0f03e081 --- /dev/null +++ b/hassio/api/panel/chunk.457ac71b0904d7243237.js @@ -0,0 +1,2 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[2],{2:function(e,t,n){"use strict";n.r(t),n(36);var C=function(){function e(e,t){for(var n=0;ng%s3Pxf&4zWQ*PDO2t4O;g-# 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 this.fire('iron-iconset-added', this, { node: window }));\n }\n\n /**\n *\n * When name is changed, register iconset metadata\n *\n */\n _nameChanged() {\n this._meta.value = null;\n this._meta.key = this.name;\n this._meta.value = this;\n if (this.ownerDocument && this.ownerDocument.readyState === 'loading') {\n // Document still loading. It could be that not all icons in the iconset are parsed yet.\n this.ownerDocument.addEventListener('DOMContentLoaded', () => {\n this._fireIronIconsetAdded();\n });\n } else {\n this._fireIronIconsetAdded();\n }\n }\n}\n\ncustomElements.define('ha-iconset-svg', HaIconset);\n","export default \"\";","import '../../../src/components/ha-iconset-svg.js';\nimport iconSetContent from '../../hassio-icons.html';\n\nconst documentContainer = document.createElement('template');\ndocumentContainer.setAttribute('style', 'display: none;');\ndocumentContainer.innerHTML = iconSetContent;\ndocument.head.appendChild(documentContainer.content);\n"],"sourceRoot":""} \ No newline at end of file diff --git a/hassio/api/panel/chunk.57f5b43a82b988080555.js b/hassio/api/panel/chunk.57f5b43a82b988080555.js new file mode 100644 index 000000000..d321b04d4 --- /dev/null +++ b/hassio/api/panel/chunk.57f5b43a82b988080555.js @@ -0,0 +1,2 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[3],{1:function(e,n,t){"use strict";t.r(n),t(58);var o,a,r=t(4),s=t(6),i=(t(28),t(59),t(80),t(20),t(10)),l=function(){function e(e,n){for(var t=0;t\n'],a=['\n \n'],Object.freeze(Object.defineProperties(o,{raw:{value:Object.freeze(a)}}))),u=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),c(n,[{key:"toggleMenu",value:function(e){e.stopPropagation(),this.fire(this.showMenu?"hass-close-menu":"hass-open-menu")}},{key:"_getIcon",value:function(e){return(e?"hassio":"hass")+":menu"}}],[{key:"template",get:function(){return Object(r.a)(d)}},{key:"properties",get:function(){return{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassio:{type:Boolean,value:!1}}}}]),n}();customElements.define("ha-menu-button",u);var h=function(){function e(e,n){for(var t=0;t\n .placeholder {\n height: 100%;\n }\n\n .layout {\n height: calc(100% - 64px);\n }\n \n\n
\n \n \n
[[title]]
\n
\n
\n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n\n
\n \n \n
[[title]]
\n
\n
\n \n
\n
\n'])}})),b=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),h(n,null,[{key:"template",get:function(){return Object(r.a)(f)}},{key:"properties",get:function(){return{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},title:{type:String,value:""}}}}]),n}();customElements.define("hass-loading-screen",b),t(61),t(76),t(83),t(3);var m=document.createElement("template");m.setAttribute("style","display: none;"),m.innerHTML='\n \n\n \n\n \n\n \n',document.head.appendChild(m.content),t(63),t(78);var y=function(){function e(e,n){for(var t=0;t=0?n:null}:null}),e._resize();for(var n=document.createTreeWalker(e,1,null,!1);n.nextNode();){var t=n.currentNode;"A"===t.tagName&&t.host!==document.location.host?t.target="_blank":"IMG"===t.tagName&&t.addEventListener("load",e._resize)}}else 2===e._scriptLoaded&&(e.innerText=e.content)}))}}],[{key:"properties",get:function(){return{content:{type:String,observer:"_render"},allowSvg:{type:Boolean,value:!1}}}}]),n}();customElements.define("ha-markdown",w);var k=function(){function e(e,n){for(var t=0;t\n paper-dialog {\n min-width: 350px;\n font-size: 14px;\n border-radius: 2px;\n }\n app-toolbar {\n margin: 0;\n padding: 0 16px;\n color: var(--primary-text-color);\n background-color: var(--secondary-background-color);\n }\n app-toolbar [main-title] {\n margin-left: 16px;\n }\n paper-checkbox {\n display: block;\n margin: 4px;\n }\n @media all and (max-width: 450px), all and (max-height: 500px) {\n paper-dialog {\n max-height: 100%;\n }\n paper-dialog::before {\n content: "";\n position: fixed;\n z-index: -1;\n top: 0px;\n left: 0px;\n right: 0px;\n bottom: 0px;\n background-color: inherit;\n }\n app-toolbar {\n color: var(--text-primary-color);\n background-color: var(--primary-color);\n }\n }\n \n \n \n \n
[[title]]
\n
\n \n \n \n
\n'],{raw:{value:Object.freeze(['\n \n \n \n \n
[[title]]
\n
\n \n \n \n
\n'])}})),O=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),k(n,[{key:"openDialog",value:function(){this.$.dialog.open()}}],[{key:"template",get:function(){return Object(r.a)(_)}},{key:"properties",get:function(){return{title:String,content:String}}}]),n}();customElements.define("hassio-markdown-dialog",O),t(84),t(13),t(12),t(97),t(99),t(85);var j=function(){function e(e,n){for(var t=0;t\n :host,\n paper-card,\n paper-dropdown-menu {\n display: block;\n }\n .errors {\n color: var(--google-red-500);\n margin-bottom: 16px;\n }\n paper-item {\n width: 450px;\n }\n .card-actions {\n text-align: right;\n }\n \n \n
\n \n\n \n \n \n \n \n \n \n \n \n \n
\n
\n Save\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n \n\n \n \n \n \n \n \n \n \n \n \n
\n
\n Save\n
\n
\n'])}})),S=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),j(n,[{key:"addonChanged",value:function(e){var n=this;if(this.setProperties({selectedInput:e.audio_input||"null",selectedOutput:e.audio_output||"null"}),!this.outputDevices){var t=[{device:"null",name:"-"}];this.hass.callApi("get","hassio/hardware/audio").then(function(e){var o=e.data.audio,a=Object.keys(o.input).map(function(e){return{device:e,name:o.input[e]}}),r=Object.keys(o.output).map(function(e){return{device:e,name:o.output[e]}});n.setProperties({inputDevices:t.concat(a),outputDevices:t.concat(r)})},function(){n.setProperties({inputDevices:t,outputDevices:t})})}}},{key:"_saveSettings",value:function(){var e=this;this.error=null;var n="hassio/addons/"+this.addon.slug+"/options";this.hass.callApi("post",n,{audio_input:"null"===this.selectedInput?null:this.selectedInput,audio_output:"null"===this.selectedOutput?null:this.selectedOutput}).then(function(){e.fire("hass-api-called",{success:!0,path:n})},function(n){e.error=n.body.message})}}],[{key:"template",get:function(){return Object(r.a)(x)}},{key:"properties",get:function(){return{hass:Object,addon:{type:Object,observer:"addonChanged"},inputDevices:Array,outputDevices:Array,selectedInput:String,selectedOutput:String,error:String}}}]),n}();customElements.define("hassio-addon-audio",S),t(86);var P=function(){function e(e,n){for(var t=0;t\n .container {\n position: relative;\n display: inline-block;\n }\n\n paper-button {\n transition: all 1s;\n }\n\n .success paper-button {\n color: white;\n background-color: var(--google-green-500);\n transition: none;\n }\n\n .error paper-button {\n color: white;\n background-color: var(--google-red-500);\n transition: none;\n }\n\n paper-button[disabled] {\n color: #c8c8c8;\n }\n\n .progress {\n @apply --layout;\n @apply --layout-center-center;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n }\n \n
\n \n \n \n \n
\n'],{raw:{value:Object.freeze(['\n \n
\n \n \n \n \n
\n'])}})),E=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),P(n,[{key:"tempClass",value:function(e){var n=this.$.container.classList;n.add(e),setTimeout(function(){n.remove(e)},1e3)}},{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("click",function(n){return e.buttonTapped(n)})}},{key:"buttonTapped",value:function(e){this.progress&&e.stopPropagation()}},{key:"actionSuccess",value:function(){this.tempClass("success")}},{key:"actionError",value:function(){this.tempClass("error")}},{key:"computeDisabled",value:function(e,n){return e||n}}],[{key:"template",get:function(){return Object(r.a)(C)}},{key:"properties",get:function(){return{hass:{type:Object},progress:{type:Boolean,value:!1},disabled:{type:Boolean,value:!1}}}}]),n}();customElements.define("ha-progress-button",E);var T=function(){function e(e,n){for(var t=0;t\n'],{raw:{value:Object.freeze(['\n \n'])}})),D=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),T(n,[{key:"buttonTapped",value:function(){var e=this;this.progress=!0;var n={method:this.method,path:this.path,data:this.data};this.hass.callApi(this.method,this.path,this.data).then(function(t){e.progress=!1,e.$.progress.actionSuccess(),n.success=!0,n.response=t},function(t){e.progress=!1,e.$.progress.actionError(),n.success=!1,n.response=t}).then(function(){e.fire("hass-api-called",n)})}}],[{key:"template",get:function(){return Object(r.a)(A)}},{key:"properties",get:function(){return{hass:Object,progress:{type:Boolean,value:!1},path:String,method:{type:String,value:"POST"},data:{type:Object,value:{}},disabled:{type:Boolean,value:!1}}}}]),n}();customElements.define("ha-call-api-button",D);var R=function(){function e(e,n){for(var t=0;t\n :host {\n display: block;\n }\n paper-card {\n display: block;\n }\n .card-actions {\n @apply --layout;\n @apply --layout-justified;\n }\n .errors {\n color: var(--google-red-500);\n margin-bottom: 16px;\n }\n iron-autogrow-textarea {\n width: 100%;\n font-family: monospace;\n }\n .syntaxerror {\n color: var(--google-red-500);\n }\n \n \n
\n \n \n
\n
\n Reset to defaults\n Save\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n \n \n
\n
\n Reset to defaults\n Save\n
\n
\n'])}})),z=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),R(n,[{key:"addonChanged",value:function(e){this.config=e?JSON.stringify(e.options,null,2):""}},{key:"configChanged",value:function(e){try{this.$.config.classList.remove("syntaxerror"),this.configParsed=JSON.parse(e)}catch(e){this.$.config.classList.add("syntaxerror"),this.configParsed=null}}},{key:"saveTapped",value:function(){var e=this;this.error=null,this.hass.callApi("post","hassio/addons/"+this.addonSlug+"/options",{options:this.configParsed}).catch(function(n){e.error=n.body.message})}}],[{key:"template",get:function(){return Object(r.a)(I)}},{key:"properties",get:function(){return{hass:Object,addon:{type:Object,observer:"addonChanged"},addonSlug:String,config:{type:String,observer:"configChanged"},configParsed:Object,error:String,resetData:{type:Object,value:{options:null}}}}}]),n}();customElements.define("hassio-addon-config",z),t(21),t(87);var q=t(7),L=[60,"second",60,"minute",24,"hour",7,"day"],H=t(57),U=(t(101),t(98)),B={__localizationCache:{requests:{},messages:{},ajax:null},properties:{language:{type:String},resources:{type:Object},formats:{type:Object,value:function(){return{}}},useKeyIfMissing:{type:Boolean,value:!1},localize:{type:Function,computed:"__computeLocalize(language, resources, formats)"},bubbleEvent:{type:Boolean,value:!1}},loadResources:function(e,n,t){var o=this.constructor.prototype;this.__checkLocalizationCache(o);var a,r=o.__localizationCache.ajax;function s(e){this.__onRequestResponse(e,n,t)}r||(r=o.__localizationCache.ajax=document.createElement("iron-ajax")),(a=o.__localizationCache.requests[e])?a.completes.then(s.bind(this),this.__onRequestError.bind(this)):(r.url=e,(a=r.generateRequest()).completes.then(s.bind(this),this.__onRequestError.bind(this)),o.__localizationCache.requests[e]=a)},__computeLocalize:function(e,n,t){var o=this.constructor.prototype;return this.__checkLocalizationCache(o),o.__localizationCache||(o.__localizationCache={requests:{},messages:{},ajax:null}),o.__localizationCache.messages={},function(){var a=arguments[0];if(a&&n&&e&&n[e]){var r=n[e][a];if(!r)return this.useKeyIfMissing?a:"";var s=a+r,i=o.__localizationCache.messages[s];i||(i=new U.a(r,e,t),o.__localizationCache.messages[s]=i);for(var l={},p=1;p=0?"past":"future";t=Math.abs(t);for(var a=0;a\n iron-icon {\n margin-right: 16px;\n margin-top: 16px;\n float: left;\n color: var(--secondary-text-color);\n }\n iron-icon.update {\n color: var(--paper-orange-400);\n }\n iron-icon.running,\n iron-icon.installed {\n color: var(--paper-green-400);\n }\n iron-icon.hassupdate,\n iron-icon.snapshot {\n color: var(--paper-item-icon-color);\n }\n .title {\n color: var(--primary-text-color);\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n }\n .addition {\n color: var(--secondary-text-color);\n overflow: hidden;\n position: relative;\n height: 2.4em;\n line-height: 1.2em;\n }\n ha-relative-time {\n display: block;\n }\n \n \n
\n
[[title]]
\n
\n \n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n
[[title]]
\n
\n \n \n
\n
\n'])}})),G=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),Y(n,null,[{key:"template",get:function(){return Object(r.a)(J)}},{key:"properties",get:function(){return{hass:Object,title:String,description:String,datetime:String,icon:{type:String,value:"hass:help-circle"},iconTitle:String,iconClass:String}}}]),n}();customElements.define("hassio-card-content",G);var V=function(){function e(e,n){for(var t=0;t\n :host {\n display: block;\n }\n paper-card {\n display: block;\n margin-bottom: 16px;\n }\n .addon-header {\n @apply --paper-font-headline;\n }\n .light-color {\n color: var(--secondary-text-color);\n }\n .addon-version {\n float: right;\n font-size: 15px;\n vertical-align: middle;\n }\n .description {\n margin-bottom: 16px;\n }\n .logo img {\n max-height: 60px;\n margin: 16px 0;\n display: block;\n }\n .state div{\n width: 150px;\n display: inline-block;\n }\n paper-toggle-button {\n display: inline;\n }\n iron-icon.running {\n color: var(--paper-green-400);\n }\n iron-icon.stopped {\n color: var(--google-red-300);\n }\n ha-call-api-button {\n font-weight: 500;\n color: var(--primary-color);\n }\n .right {\n float: right;\n }\n ha-markdown img {\n max-width: 100%;\n }\n \n \n\n \n
\n
[[addon.name]]\n
\n \n \n
\n
\n
\n [[addon.description]].
\n Visit [[addon.name]] page for details.\n
\n \n \n
\n
\n \n \n
\n
\n \n'],{raw:{value:Object.freeze(['\n \n \n\n \n
\n
[[addon.name]]\n
\n \n \n
\n
\n
\n [[addon.description]].
\n Visit [[addon.name]] page for details.\n
\n \n \n
\n
\n \n \n
\n
\n \n'])}})),K=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),V(n,[{key:"computeIsRunning",value:function(e){return e&&"started"===e.state}},{key:"computeUpdateAvailable",value:function(e){return e&&!e.detached&&e.version&&e.version!==e.last_version}},{key:"pathWebui",value:function(e){return e&&e.replace("[HOST]",document.location.hostname)}},{key:"computeShowWebUI",value:function(e,n){return e&&n}},{key:"computeStartOnBoot",value:function(e){return"auto"===e}},{key:"startOnBootToggled",value:function(){var e={boot:"auto"===this.addon.boot?"manual":"auto"};this.hass.callApi("POST","hassio/addons/"+this.addonSlug+"/options",e)}},{key:"autoUpdateToggled",value:function(){var e={auto_update:!this.addon.auto_update};this.hass.callApi("POST","hassio/addons/"+this.addonSlug+"/options",e)}},{key:"openChangelog",value:function(){var e=this;this.hass.callApi("get","hassio/addons/"+this.addonSlug+"/changelog").then(function(e){return e},function(){return"Error getting changelog"}).then(function(n){e.fire("hassio-markdown-dialog",{title:"Changelog",content:n})})}},{key:"_unistallClicked",value:function(){var e=this;if(confirm("Are you sure you want to uninstall this add-on?")){var n="hassio/addons/"+this.addonSlug+"/uninstall",t={path:n};this.hass.callApi("post",n).then(function(e){t.success=!0,t.response=e},function(e){t.success=!1,t.response=e}).then(function(){e.fire("hass-api-called",t)})}}}],[{key:"template",get:function(){return Object(r.a)(X)}},{key:"properties",get:function(){return{hass:Object,addon:Object,addonSlug:String,isRunning:{type:Boolean,computed:"computeIsRunning(addon)"}}}}]),n}();customElements.define("hassio-addon-info",K);var Q=function(){function e(e,n){for(var t=0;t\n :host,\n paper-card {\n display: block;\n }\n pre {\n overflow-x: auto;\n }\n \n \n
\n
[[log]]
\n
\n
\n Refresh\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n
[[log]]
\n
\n
\n Refresh\n
\n
\n'])}})),ee=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),Q(n,[{key:"addonSlugChanged",value:function(e){var n=this;this.hass?this.refresh():setTimeout(function(){n.addonChanged(e)},0)}},{key:"refresh",value:function(){var e=this;this.hass.callApi("get","hassio/addons/"+this.addonSlug+"/logs").then(function(n){e.log=n})}}],[{key:"template",get:function(){return Object(r.a)(Z)}},{key:"properties",get:function(){return{hass:Object,addonSlug:{type:String,observer:"addonSlugChanged"},log:String}}}]),n}();customElements.define("hassio-addon-logs",ee),t(31);var ne=function(){function e(e,n){for(var t=0;t\n :host {\n display: block;\n }\n paper-card {\n display: block;\n }\n .errors {\n color: var(--google-red-500);\n margin-bottom: 16px;\n }\n .card-actions {\n @apply --layout;\n @apply --layout-justified;\n }\n \n \n
\n \n\n \n \n \n \n \n \n
ContainerHost
\n
\n
\n Reset to defaults\n Save\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n \n\n \n \n \n \n \n \n
ContainerHost
\n
\n
\n Reset to defaults\n Save\n
\n
\n'])}})),oe=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),ne(n,[{key:"addonChanged",value:function(e){if(e){var n=e.network||{},t=Object.keys(n).map(function(e){return{container:e,host:n[e]}});this.config=t.sort(function(e,n){return e.host-n.host})}}},{key:"saveTapped",value:function(){var e=this;this.error=null;var n={};this.config.forEach(function(e){n[e.container]=parseInt(e.host)});var t="hassio/addons/"+this.addonSlug+"/options";this.hass.callApi("post",t,{network:n}).then(function(){e.fire("hass-api-called",{success:!0,path:t})},function(n){e.error=n.body.message})}}],[{key:"template",get:function(){return Object(r.a)(te)}},{key:"properties",get:function(){return{hass:Object,addonSlug:String,config:Object,addon:{type:Object,observer:"addonChanged"},error:String,resetData:{type:Object,value:{network:null}}}}}]),n}();customElements.define("hassio-addon-network",oe);var ae=function(){function e(e,n){for(var t=0;t\n :host {\n color: var(--primary-text-color);\n --paper-card-header-color: var(--primary-text-color);\n }\n .content {\n padding: 24px 0 32px;\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n hassio-addon-info,\n hassio-addon-network,\n hassio-addon-audio,\n hassio-addon-config {\n margin-bottom: 24px;\n width: 600px;\n }\n hassio-addon-logs {\n max-width: calc(100% - 8px);\n min-width: 600px;\n }\n @media only screen and (max-width: 600px) {\n hassio-addon-info,\n hassio-addon-network,\n hassio-addon-audio,\n hassio-addon-config,\n hassio-addon-logs {\n max-width: 100%;\n min-width: 100%;\n }\n }\n \n \n \n \n \n \n \n
Hass.io: add-on details
\n
\n
\n
\n \n\n \n
\n
\n\n \n'],{raw:{value:Object.freeze(['\n \n \n \n \n \n \n \n
Hass.io: add-on details
\n
\n
\n
\n \n\n \n
\n
\n\n \n'])}})),se=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),ae(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)}),this.addEventListener("hassio-markdown-dialog",function(n){return e.openMarkdown(n)})}},{key:"apiCalled",value:function(e){var n=e.detail.path;n&&("uninstall"===n.substr(n.lastIndexOf("/")+1)?this.backTapped():this.routeDataChanged(this.routeData))}},{key:"routeDataChanged",value:function(e){var n=this;this.routeMatches&&e&&e.slug&&this.hass.callApi("get","hassio/addons/"+e.slug+"/info").then(function(e){n.addon=e.data},function(){n.addon=null})}},{key:"backTapped",value:function(){history.back()}},{key:"openMarkdown",value:function(e){this.setProperties({markdownTitle:e.detail.title,markdownContent:e.detail.content}),this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog()}}],[{key:"template",get:function(){return Object(r.a)(re)}},{key:"properties",get:function(){return{hass:Object,showMenu:Boolean,narrow:Boolean,route:Object,routeData:{type:Object,observer:"routeDataChanged"},routeMatches:Boolean,addon:Object,markdownTitle:String,markdownContent:{type:String,value:""}}}}]),n}();customElements.define("hassio-addon-view",se);var ie=function(){function e(e,n){for(var t=0;t1&&void 0!==arguments[1]&&arguments[1]?history.replaceState(null,null,e):history.pushState(null,null,e),this.fire("location-changed")}}]),t}()}),de=function(){function e(e,n){for(var t=0;t\n paper-card {\n cursor: pointer;\n }\n a.repo {\n display: block;\n color: var(--primary-text-color);\n }\n \n \n'],{raw:{value:Object.freeze(['\n \n \n'])}})),he=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,ce(s.a)),de(n,[{key:"sortAddons",value:function(e,n){return e.name\n .add {\n padding: 12px 16px;\n }\n iron-icon {\n color: var(--secondary-text-color);\n margin-right: 16px;\n display: inline-block;\n }\n paper-input {\n width: calc(100% - 49px);\n display: inline-block;\n }\n \n
\n
\n Repositories\n
\n Configure which add-on repositories to fetch data from:\n
\n
\n \n \n
\n \n \n
\n
\n Add\n
\n
\n
\n'],{raw:{value:Object.freeze(['\n \n
\n
\n Repositories\n
\n Configure which add-on repositories to fetch data from:\n
\n
\n \n \n
\n \n \n
\n
\n Add\n
\n
\n
\n'])}})),me=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),fe(n,[{key:"reposChanged",value:function(e){this.repoList=e.filter(function(e){return"core"!==e.slug&&"local"!==e.slug}),this.repoUrl=""}},{key:"sortRepos",value:function(e,n){return e.name\n hassio-addon-repository {\n margin-top: 24px;\n }\n \n \n\n \n'],{raw:{value:Object.freeze(['\n \n \n\n \n'])}})),ge=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),ye(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)}),this.loadData()}},{key:"apiCalled",value:function(e){e.detail.success&&this.loadData()}},{key:"sortRepos",value:function(e,n){return"local"===e.slug?-1:"local"===n.slug?1:"core"===e.slug?-1:"core"===n.slug?1:e.name\n paper-card {\n cursor: pointer;\n }\n \n
\n
Add-ons
\n \n \n
\n'],{raw:{value:Object.freeze(['\n \n
\n
Add-ons
\n \n \n
\n'])}})),_e=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,ce(s.a)),we(n,[{key:"sortAddons",value:function(e,n){return e.name\n paper-card {\n display: block;\n margin-bottom: 32px;\n }\n .errors {\n color: var(--google-red-500);\n margin-top: 16px;\n }\n \n \n'],{raw:{value:Object.freeze(['\n \n \n'])}})),Se=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),je(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)})}},{key:"apiCalled",value:function(e){if(e.detail.success)this.errors=null;else{var n=e.detail.response;"object"===Oe(n.body)?this.errors=n.body.message||"Unknown error":this.errors=n.body}}},{key:"computeUpdateAvailable",value:function(e){return e.version!==e.last_version}}],[{key:"template",get:function(){return Object(r.a)(xe)}},{key:"properties",get:function(){return{hass:Object,hassInfo:Object,error:String}}}]),n}();customElements.define("hassio-hass-update",Se);var Pe=function(){function e(e,n){for(var t=0;t\n .content {\n margin: 0 auto;\n }\n \n
\n \n \n
\n'],{raw:{value:Object.freeze(['\n \n
\n \n \n
\n'])}})),Ee=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),Pe(n,null,[{key:"template",get:function(){return Object(r.a)(Ce)}},{key:"properties",get:function(){return{hass:Object,supervisorInfo:Object,hassInfo:Object}}}]),n}();customElements.define("hassio-dashboard",Ee),t(68);var Te=function(){function e(e,n){for(var t=0;t\n paper-dialog {\n min-width: 350px;\n font-size: 14px;\n border-radius: 2px;\n }\n app-toolbar {\n margin: 0;\n padding: 0 16px;\n color: var(--primary-text-color);\n background-color: var(--secondary-background-color);\n }\n app-toolbar [main-title] {\n margin-left: 16px;\n }\n paper-dialog-scrollable {\n margin: 0;\n }\n paper-checkbox {\n display: block;\n margin: 4px;\n }\n @media all and (max-width: 450px), all and (max-height: 500px) {\n paper-dialog {\n max-height: 100%;\n height: 100%;\n }\n app-toolbar {\n color: var(--text-primary-color);\n background-color: var(--primary-color);\n }\n }\n .details {\n color: var(--secondary-text-color);\n }\n .download {\n color: var(--primary-color);\n }\n .warning,\n .error {\n color: var(--google-red-500);\n }\n \n \n \n \n
[[_computeName(snapshot)]]
\n
\n
\n [[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])
\n [[_formatDatetime(snapshot.date)]]\n
\n
Home Assistant:
\n \n Home Assistant [[snapshot.homeassistant]]\n \n \n \n \n \n
\n \n \n \n \n Restore selected\n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n \n \n
[[_computeName(snapshot)]]
\n
\n
\n [[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])
\n [[_formatDatetime(snapshot.date)]]\n
\n
Home Assistant:
\n \n Home Assistant [[snapshot.homeassistant]]\n \n \n \n \n \n
\n \n \n \n \n Restore selected\n \n
\n
\n'])}})),De=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),Te(n,[{key:"_snapshotSlugChanged",value:function(e){var n=this;e&&"update"!==e&&this.hass.callApi("get","hassio/snapshots/"+e+"/info").then(function(e){e.data.folders=n._computeFolders(e.data.folders),e.data.addons=n._computeAddons(e.data.addons),n.snapshot=e.data,n.$.dialog.open()},function(){n.snapshot=null})}},{key:"_computeFolders",value:function(e){var n=[];return e.includes("homeassistant")&&n.push({slug:"homeassistant",name:"Home Assistant configuration",checked:!0}),e.includes("ssl")&&n.push({slug:"ssl",name:"SSL",checked:!0}),e.includes("share")&&n.push({slug:"share",name:"Share",checked:!0}),e.includes("addons/local")&&n.push({slug:"addons/local",name:"Local add-ons",checked:!0}),n}},{key:"_computeAddons",value:function(e){return e.map(function(e){return{slug:e.slug,name:e.name,version:e.version,checked:!0}})}},{key:"_isFullSnapshot",value:function(e){return"full"===e}},{key:"_partialRestoreClicked",value:function(){var e=this;if(confirm("Are you sure you want to restore this snapshot?")){var n=this.snapshot.addons.filter(function(e){return e.checked}).map(function(e){return e.slug}),t=this.snapshot.folders.filter(function(e){return e.checked}).map(function(e){return e.slug}),o={homeassistant:this.restoreHass,addons:n,folders:t};this.snapshot.protected&&(o.password=this.snapshotPassword),this.hass.callApi("post","hassio/snapshots/"+this.snapshotSlug+"/restore/partial",o).then(function(){alert("Snapshot restored!"),e.$.dialog.close()},function(n){e.error=n.body.message})}}},{key:"_fullRestoreClicked",value:function(){var e=this;if(confirm("Are you sure you want to restore this snapshot?")){var n=this.snapshot.protected?{password:this.snapshotPassword}:null;this.hass.callApi("post","hassio/snapshots/"+this.snapshotSlug+"/restore/full",n).then(function(){alert("Snapshot restored!"),e.$.dialog.close()},function(n){e.error=n.body.message})}}},{key:"_deleteClicked",value:function(){var e=this;confirm("Are you sure you want to delete this snapshot?")&&this.hass.callApi("post","hassio/snapshots/"+this.snapshotSlug+"/remove").then(function(){e.$.dialog.close(),e.snapshotDeleted=!0},function(n){e.error=n.body.message})}},{key:"_computeDownloadUrl",value:function(e){return"/api/hassio/snapshots/"+e+"/download?api_password="+encodeURIComponent(this.hass.connection.options.authToken)}},{key:"_computeDownloadName",value:function(e){return"Hass_io_"+this._computeName(e).replace(/[^a-z0-9]+/gi,"_")+".tar"}},{key:"_computeName",value:function(e){return e.name||e.slug}},{key:"_computeType",value:function(e){return"full"===e?"Full snapshot":"Partial snapshot"}},{key:"_computeSize",value:function(e){return Math.ceil(10*e)/10+" MB"}},{key:"_sortAddons",value:function(e,n){return e.name\n paper-radio-group {\n display: block;\n }\n paper-radio-button {\n padding: 0 0 2px 2px;\n }\n paper-radio-button,\n paper-checkbox,\n paper-input[type="password"] {\n display: block;\n margin: 4px 0 4px 48px;\n }\n .pointer {\n cursor: pointer;\n }\n \n
\n
\n
\n Create snapshot\n
\n Snapshots allow you to easily backup and\n restore all data of your Hass.io instance.\n
\n
\n \n
\n \n Type:\n \n \n Full snapshot\n \n \n Partial snapshot\n \n \n \n Security:\n Password protection\n \n \n
\n
\n Create\n
\n
\n
\n\n
\n
Available snapshots
\n \n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n
\n
\n
\n Create snapshot\n
\n Snapshots allow you to easily backup and\n restore all data of your Hass.io instance.\n
\n
\n \n
\n \n Type:\n \n \n Full snapshot\n \n \n Partial snapshot\n \n \n \n Security:\n Password protection\n \n \n
\n
\n Create\n
\n
\n
\n\n
\n
Available snapshots
\n \n \n
\n
\n'])}})),ze=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),Re(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e._apiCalled(n)}),this._updateSnapshots()}},{key:"_apiCalled",value:function(e){e.detail.success&&this._updateSnapshots()}},{key:"_updateSnapshots",value:function(){var e=this;this.hass.callApi("get","hassio/snapshots").then(function(n){e.snapshots=n.data.snapshots},function(n){e.error=n.message})}},{key:"_createSnapshot",value:function(){var e=this;if(this.error="",!this.snapshotHasPassword||this.snapshotPassword.length){this.creatingSnapshot=!0;var n=this.snapshotName;n.length||(n=(new Date).toLocaleDateString(navigator.language,{weekday:"long",year:"numeric",month:"short",day:"numeric"}));var t=void 0,o=void 0;if("full"===this.snapshotType)t={name:n},o="hassio/snapshots/new/full";else{var a=this.addonList.filter(function(e){return e.checked}).map(function(e){return e.slug});t={name:n,folders:this.folderList.filter(function(e){return e.checked}).map(function(e){return e.slug}),addons:a},o="hassio/snapshots/new/partial"}this.snapshotHasPassword&&(t.password=this.snapshotPassword),this.hass.callApi("post",o,t).then(function(){e.creatingSnapshot=!1,e.fire("hass-api-called",{success:!0})},function(n){e.creatingSnapshot=!1,e.error=n.message})}else this.error="Please enter a password."}},{key:"_installedAddonsChanged",value:function(e){this.addonList=e.map(function(e){return{slug:e.slug,name:e.name,checked:!0}})}},{key:"_sortAddons",value:function(e,n){return e.name\n paper-card {\n display: inline-block;\n width: 400px;\n margin-left: 8px;\n }\n .card-content {\n height: 200px;\n color: var(--primary-text-color);\n }\n @media screen and (max-width: 830px) {\n paper-card {\n margin-top: 8px;\n margin-left: 0;\n width: 100%;\n }\n .card-content {\n height: 100%;\n }\n }\n .info {\n width: 100%;\n }\n .info td:nth-child(2) {\n text-align: right;\n }\n .errors {\n color: var(--google-red-500);\n margin-top: 16px;\n }\n paper-button.info {\n max-width: calc(50% - 12px);\n }\n \n \n
\n

Host system

\n \n \n \n \n \n \n \n \n \n \n
Hostname[[data.hostname]]
System[[data.operating_system]]
\n \n Hardware\n \n \n \n
\n
\n \n \n \n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n

Host system

\n \n \n \n \n \n \n \n \n \n \n
Hostname[[data.hostname]]
System[[data.operating_system]]
\n \n Hardware\n \n \n \n
\n
\n \n \n \n \n
\n
\n'])}})),Ue=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),Le(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)})}},{key:"apiCalled",value:function(e){if(e.detail.success)this.errors=null;else{var n=e.detail.response;"object"===qe(n.body)?this.errors=n.body.message||"Unknown error":this.errors=n.body}}},{key:"_dataChanged",value:function(e){var n=this;e.features&&e.features.includes("hassos")?this.hass.callApi("get","hassio/hassos/info").then(function(e){n._hassOs=e.data}):this._hassOs={}}},{key:"_computeUpdateAvailable",value:function(e){return e&&e.version!==e.version_latest}},{key:"_featureAvailable",value:function(e,n){return e&&e.features&&e.features.includes(n)}},{key:"_showHardware",value:function(){var e=this;this.hass.callApi("get","hassio/hardware/info").then(function(n){return e._objectToMarkdown(n.data)},function(){return"Error getting hardware info"}).then(function(n){e.fire("hassio-markdown-dialog",{title:"Hardware",content:n})})}},{key:"_objectToMarkdown",value:function(e){var n=this,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o="";return Object.keys(e).forEach(function(a){"object"!==qe(e[a])?o+=t+"- "+a+": "+e[a]+"\n":(o+=t+"- "+a+":\n",Array.isArray(e[a])?e[a].length&&(o+=t+" - "+e[a].join("\n"+t+" - ")+"\n"):o+=n._objectToMarkdown(e[a]," "+t))}),o}},{key:"_changeHostnameClicked",value:function(){var e=this.data.hostname,n=prompt("Please enter a new hostname:",e);n&&n!==e&&this.hass.callApi("post","hassio/host/options",{hostname:n})}}],[{key:"template",get:function(){return Object(r.a)(He)}},{key:"properties",get:function(){return{hass:Object,data:{type:Object,observer:"_dataChanged"},errors:String,_hassOs:Object}}}]),n}();customElements.define("hassio-host-info",Ue);var Be="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Me=function(){function e(e,n){for(var t=0;t\n paper-card {\n display: inline-block;\n width: 400px;\n }\n .card-content {\n height: 200px;\n color: var(--primary-text-color);\n }\n @media screen and (max-width: 830px) {\n paper-card {\n width: 100%;\n }\n .card-content {\n height: 100%;\n }\n }\n .info {\n width: 100%;\n }\n .info td:nth-child(2) {\n text-align: right;\n }\n .errors {\n color: var(--google-red-500);\n margin-top: 16px;\n }\n \n \n
\n

Hass.io supervisor

\n \n \n \n \n \n \n \n \n \n \n
Version\n [[data.version]]\n
Latest version[[data.last_version]]
\n \n
\n
\n Reload\n \n \n \n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n

Hass.io supervisor

\n \n \n \n \n \n \n \n \n \n \n
Version\n [[data.version]]\n
Latest version[[data.last_version]]
\n \n
\n
\n Reload\n \n \n \n
\n
\n'])}})),Ne=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(s.a)),Me(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)})}},{key:"apiCalled",value:function(e){if(e.detail.success)this.errors=null;else{var n=e.detail.response;"object"===Be(n.body)?this.errors=n.body.message||"Unknown error":this.errors=n.body}}},{key:"computeUpdateAvailable",value:function(e){return e.version!==e.last_version}},{key:"_equals",value:function(e,n){return e===n}},{key:"_joinBeta",value:function(){var e=this;if(confirm("WARNING:\nBeta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.\n\nThis inludes beta releases for:\n- Home Assistant (Release Candidates)\n- Hass.io supervisor\n- Host system")){var n="hassio/supervisor/options",t={channel:"beta"},o={method:"post",path:n,data:t};this.hass.callApi("post",n,t).then(function(e){o.success=!0,o.response=e},function(e){o.success=!1,o.response=e}).then(function(){e.fire("hass-api-called",o)})}}}],[{key:"template",get:function(){return Object(r.a)(Fe)}},{key:"properties",get:function(){return{hass:Object,data:Object,errors:String,leaveBeta:{type:Object,value:{channel:"stable"}}}}}]),n}();customElements.define("hassio-supervisor-info",Ne);var $e=function(){function e(e,n){for(var t=0;t\n paper-card {\n display: block;\n }\n pre {\n overflow-x: auto;\n }\n \n \n
\n
[[log]]
\n
\n
\n Refresh\n
\n
\n'],{raw:{value:Object.freeze(['\n \n \n
\n
[[log]]
\n
\n
\n Refresh\n
\n
\n'])}})),Ye=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),$e(n,[{key:"ready",value:function(){(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.loadData()}},{key:"loadData",value:function(){var e=this;this.hass.callApi("get","hassio/supervisor/logs").then(function(n){e.log=n},function(){e.log="Error fetching logs"})}},{key:"refreshTapped",value:function(){this.loadData()}}],[{key:"template",get:function(){return Object(r.a)(We)}},{key:"properties",get:function(){return{hass:Object,log:String}}}]),n}();customElements.define("hassio-supervisor-log",Ye);var Je=function(){function e(e,n){for(var t=0;t\n .content {\n margin: 4px;\n }\n .title {\n margin-top: 24px;\n color: var(--primary-text-color);\n font-size: 2em;\n padding-left: 8px;\n margin-bottom: 8px;\n }\n \n
\n
Information
\n \n \n
System log
\n \n
\n'],{raw:{value:Object.freeze(['\n \n
\n
Information
\n \n \n
System log
\n \n
\n'])}})),Ve=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,s.a),Je(n,null,[{key:"template",get:function(){return Object(r.a)(Ge)}},{key:"properties",get:function(){return{hass:Object,supervisorInfo:Object,hostInfo:Object}}}]),n}();customElements.define("hassio-system",Ve);var Xe=function(){function e(e,n){for(var t=0;t\n :host {\n color: var(--primary-text-color);\n --paper-card-header-color: var(--primary-text-color);\n }\n paper-tabs {\n margin-left: 12px;\n --paper-tabs-selection-bar-color: #FFF;\n text-transform: uppercase;\n }\n \n \n \n \n \n
Hass.io
\n \n
\n \n Dashboard\n Snapshots\n Add-on store\n System\n \n
\n \n \n \n \n
\n\n \n\n \n'],{raw:{value:Object.freeze(['\n \n \n \n \n \n
Hass.io
\n \n
\n \n Dashboard\n Snapshots\n Add-on store\n System\n \n
\n \n \n \n \n
\n\n \n\n \n'])}})),Qe=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,ce(s.a)),Xe(n,[{key:"ready",value:function(){var e=this;(function e(n,t,o){null===n&&(n=Function.prototype);var a=Object.getOwnPropertyDescriptor(n,t);if(void 0===a){var r=Object.getPrototypeOf(n);return null===r?void 0:e(r,t,o)}if("value"in a)return a.value;var s=a.get;return void 0!==s?s.call(o):void 0})(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),this.addEventListener("hassio-markdown-dialog",function(n){return e.openMarkdown(n)})}},{key:"handlePageSelected",value:function(e){var n,t,o,a,r,s,i,l=e.detail.item.getAttribute("page-name");l!==this.page&&this.navigate("/hassio/"+l),n=this,t=this.$.layout.header.scrollTarget,o=t,a=Math.random(),r=Date.now(),s=o.scrollTop,i=0-s,n._currentAnimationId=a,function e(){var t,l=Date.now()-r;l>200?o.scrollTop=0:n._currentAnimationId===a&&(o.scrollTop=(t=l,-i*(t/=200)*(t-2)+s),requestAnimationFrame(e.bind(n)))}.call(n)}},{key:"equals",value:function(e,n){return e===n}},{key:"showRefreshButton",value:function(e){return"store"===e||"snapshots"===e}},{key:"refreshClicked",value:function(){"snapshots"===this.page?this.shadowRoot.querySelector("hassio-snapshots").refreshData():this.shadowRoot.querySelector("hassio-addon-store").refreshData()}},{key:"openMarkdown",value:function(e){this.setProperties({markdownTitle:e.detail.title,markdownContent:e.detail.content}),this.shadowRoot.querySelector("hassio-markdown-dialog").openDialog()}}],[{key:"template",get:function(){return Object(r.a)(Ke)}},{key:"properties",get:function(){return{hass:Object,showMenu:Boolean,narrow:Boolean,page:String,supervisorInfo:Object,hostInfo:Object,hassInfo:Object,snapshotSlug:String,snapshotDeleted:Boolean,markdownTitle:String,markdownContent:{type:String,value:""}}}}]),n}();customElements.define("hassio-pages-with-tabs",Qe);var Ze=function(){function e(e,n){for(var t=0;t\n \n\n \n\n \n'],{raw:{value:Object.freeze(['\n \n \n\n \n\n \n'])}})),tn=function(e){function n(){return function(e,t){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this),function(e,n){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!n||"object"!=typeof n&&"function"!=typeof n?e:n}(this,(n.__proto__||Object.getPrototypeOf(n)).apply(this,arguments))}return function(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),n&&(Object.setPrototypeOf?Object.setPrototypeOf(e,n):e.__proto__=n)}(n,p(ce(s.a))),Ze(n,[{key:"ready",value:function(){var e=this;en(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"ready",this).call(this),function(e,n,t){var o=arguments.length>3&&void 0!==arguments[3]&&arguments[3];e._themes||(e._themes={});var a=n.default_theme;("default"===t||t&&n.themes[t])&&(a=t);var r=Object.assign({},e._themes);if("default"!==a){var s=n.themes[a];Object.keys(s).forEach(function(n){var t="--"+n;e._themes[t]="",r[t]=s[n]})}if(e.updateStyles?e.updateStyles(r):window.ShadyCSS&&window.ShadyCSS.styleSubtree(e,r),o){var i=document.querySelector("meta[name=theme-color]");if(i){i.hasAttribute("default-content")||i.setAttribute("default-content",i.getAttribute("content"));var l=r["--primary-color"]||i.getAttribute("default-content");i.setAttribute("content",l)}}}(this,this.hass.themes,this.hass.selectedTheme,!0),this.addEventListener("hass-api-called",function(n){return e.apiCalled(n)})}},{key:"connectedCallback",value:function(){en(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"connectedCallback",this).call(this),this.routeChanged(this.route)}},{key:"apiCalled",value:function(e){var n=this;if(e.detail.success){var t=1;!function e(){n.$.data.refresh().catch(function(){t+=1,setTimeout(e,1e3*Math.min(t,5))})}()}}},{key:"computeIsLoaded",value:function(e,n,t){return null!==e&&null!==n&&null!==t}},{key:"routeChanged",value:function(e){""===e.path&&"/hassio"===e.prefix&&this.navigate("/hassio/dashboard",!0),this.fire("iron-resize")}},{key:"equalsAddon",value:function(e){return e&&"addon"===e}}],[{key:"template",get:function(){return Object(r.a)(nn)}},{key:"properties",get:function(){return{hass:Object,narrow:Boolean,showMenu:Boolean,route:{type:Object,value:{prefix:"/hassio",path:"/dashboard",__queryParams:{}},observer:"routeChanged"},routeData:Object,supervisorInfo:Object,hostInfo:Object,hassInfo:Object,loaded:{type:Boolean,computed:"computeIsLoaded(supervisorInfo, hostInfo, hassInfo)"}}}}]),n}();customElements.define("hassio-main",tn)},30:function(e,n){var t=document.createElement("template");t.setAttribute("style","display: none;"),t.innerHTML='\n \n',document.head.appendChild(t.content)}}]); +//# sourceMappingURL=chunk.57f5b43a82b988080555.js.map \ No newline at end of file diff --git a/hassio/api/panel/chunk.57f5b43a82b988080555.js.gz b/hassio/api/panel/chunk.57f5b43a82b988080555.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..1c46c036b85c3f6bb65de59a62f389cee5aadc60 GIT binary patch literal 19495 zcmaf(LwGI<%x>#;w_mwy+qP}n?XGRxwr$(CZQHhud;alN25XR6MtSmP;YC6L{ZD~_ zUVC49Y>FnF{XQbola4}+;5*%uuu@1~u8j{dxnwn8jZcwte(*O?5JCV$0Fh83L_o&%M^vc&c%n>T`?Fd^;{=f_=;RPA9l6uyl2HadWzS zk27O&h>69LvPXZqrwzf~8_>C$zAzK1rj+XQwQ{xRa@JoS&m@}KoKlNd55aUb zWpc&p&;|PDbm*=*JD9#=&^%%P@@|gwc0S$Cv|M^9sJ-mZ3>Avh3B6?(!Leq!wsO7e zUWpO&PK#xK?F+eza$Svxz1x%BOK{^PGM|YQRsOVfb+sOn`0#~??w==l_qJZIn+h%fHQwLz_K~xs>UsLL++uU8O+PU{!3Yrk1=>?p-v7 z4xQl%RoBlt7T6*ipPSHO!z&(dM@e+N1dTe_WDag_`Uhd3b)?D{Mo3lnp%7=R3iL~c z)wJE77&|caT%m(dJpF1Ut_YHSA5tH~0^RT?VDk(Wu1~Lz3!#t}@4wv8KtWx2XhC9! zV5HZH!&>Tje_WhBXIy{lY=5kWd;k5}f~|S~;@4!zDmy#=o%pw7jf^Zc)$37K;-^sk z*Hs8obr7C3@hm{)&w>(A9Q&-ct8QyfiSF%gY^j1%?Pm0Jq-R)S)7YtmWCMWBihJ52 zGUy#j_cbiIxtdr-b+gfq?JBeYvQ-GWZ=e3)RSD?`h@6v$`x6etqtt1DJos}A%~tAo{Ak(#Xv)G9!14`Gl`jENo3+7rEG z=w;1qVr{T)?dptp`{==e_4R?aMxk|cnvQNZ!A{Tm@Uh=&XNDr+PewK|Yhn3I^qLu3=j)!0PfJOXG}E0mK*Lw+AWa6QNmSHySN4%r1B zk2^=J{@)i;;HPuHJ***H+5K~Ay$Slg&w>4CeJXt>I&)g1| zoU9ztBO2)(->u!vil(k#$FIFL7@yIe_Xjv7K_8+R^Ln8Kj&L@qKu7zsXHH-%e;6aj z4VCf`uZ3KvHqJ##v2S$BoKs|w`vSdk0O8KZaQEwVK(s$Be zrND!+xHeI-N5MAnxzP!ZRX%!$`$2@*{N8ehTR?|{8NjFN;w+25G%R~kf#xp9uIEloW? ze_e(y-)9pT-xciQr5%+}_*1+gFM%1oKinSHRW29rt>*=lr%xL;7bFLdC?x$%{th#>`;fQxF>b9E#1CO z!g#TO`$fBh5bXgKj|i0(^jZPu2@qAed%gS%Zao9os7<=7;alR@)Bvmo;)N5F(h1{`rZO zT=0f>UW~t&;6NmVVOs&~V2k9fX8TigP=l9i9}MBNKtWYHzME7i!d~3b5UM%&%!7V3 z%Iq;=EEA)MUolW1g>;yXa1uF#oEl+Pk~AdtPX5iF<9h=a{S96ND}nvPlY>H*AVTON z3P+s<@LJi9yk)!*)s-%^k? z6nhAFo>VZtfj%yNR}vMaaSM(A4Ow2zr^WG}HMv4vxtvk7&n`}<`If&dN64PGlI!An z*DI?)No0o%ne6cI_P|$?<3oj+LK61;H%So)->m@B!>}9prxYH|oKVj=PfI9o0hJ~L zW0X#F-3OT)k5`ooW^ZnNs1u^coSTx3~M%*qp?edH~UG!-b5H?ymd9utQ!&A7z)$HSbNWD>{ThZe8atx();B&*Q-s5_9e&ED zGHc&&6oMJKOwOJ!4ks@ow7$06fIYUG#X936L51b4SPXW0&(&2RM3$9vh3W9M@}_&kB_rmq1e z#h_2IW0_pDOG-n{hNX;-@p8Uh4rx3ua0%nS)xG4QU&_R?-&u_K?_Y}Q4i5wU=w|20 zrN$1cohm!v5KR`)4|?ShXRXIJSpVk$N67+I5rmDmAB=J{n*ow=K`A z{8dFIw2%1#wraA?%?~8hgPBHbYi663(Ty9#ZgkHNFpu@e1gCj5sJ{=*v>mO;F2C_O2I> zuj!bEy3qJa2u*5vL@p8R6m)J33-1Y+&*#axR1w5K9?iY$^D2A<@s1D~)jj=s|FJ*O zbeR!=>`w3}=45|?$wn9Y?3BU!;CxZ{wp?;W9g%Zm)T^mKaX(QqoEY{%Nx_62(6i%R z0Z*I`_D$cL2P&8nYVYw!kXl5Ra$j{+e4VJj+rUD++bKH(TYKa1>~%u6eH2y-NAU;a?66(r30YcgXR$Qlz% zJ(O{>v#em|R`sGXciE)OAs_s}ji*p`FeO9l4`H0_fuD=&ssYcVCMuLXZ*~dl;HD~$ z3k`!bc6sPMS+^GGKNTH$M=es_E6V9)j$GIcMkPAOKoXNVru#oPm^ie!KEG1}D-q4?8mWq>D_K+O@uHpyX3)s3&_78GRi0D9WT zd*c|bYItgC2O{RmLB zlbK>8IJdQdf(V<+KVY*gq^sxc{AcZ@q2!W7XhKVDpAbZXx&gic56~g{ummN`IG}Wa zYq|w=(lluBw3wFnUsejHO{T&PxrZRJSQba{Tg(5xar z0%BFmCvwp~&w8Ine7bR1>7a5QLdBR*n2xyl2+x^Mv1^(x&$c`HvcdFjufo9w=j za0va6j*46*z_h8+D$#tfq{0e_m;a3Hk<(Gc%XobNop_pFjrS=5xur(Y2MGXy{L?D6 zXz7DOkLcAKiM{NJXovWuCN>= z{W$aT;9nVa!ZPdckS9fYk7o#?BM-xxafZ#Uhb@oG=l|`>^SxQd|9z)`6UnLT9Tg>H zU!mS#jj?rccIi*%4Ih^*OoomGuNyO9wih*@&KN4UK#a>d}FiZbfrVkd#dR&+=XnUhQC zvkx7L?%I2p8+qROwWCtMdpl!jFZmCVzkMGJ6++9At1kNiTBVOuw$nL|@J8_-xCpY! zB9om0XC2#Lh%lxvS*`Yri?gZPhzCflJ3xO$3n&sG$KQ$2|Fb z-Z;BXN&1#JtM1t?(a((U%;quG&da}<{VEoU@J0X)NYsm8MYHunKx~Z%@&tcM61eAX zhL$e7kv7T^P`W+Z6YG$>$D{RiTJL%FVFzYF_Uft6ZAU({xxO>}DQJ2X=~0_R@UaG& zbNm;>&2KYbZE4~6@D3pc(o@E^ZG3eb%;!9DE8TPAZStS%sZF_|ag3M#9jUgxKw!5P zVB|?v&$xS?rgOq!Fk{(ju!SrIDTO_JCQJ#38i;`5W)ChZpIIx*XTQIcI+IBCFD|?O zi%IDqLjg<#8>5#&jYAltwr$E#t=u6o30}Y=>-NRb*WJ|)>qQB3=MPyDuI(Qe4Cy_5^Q1k=!%jX%HAyXNhD7&Kqvi6;sMJ2xxJj6}JeMt& zfG@U4K;7ef9YWQ)zJTQ)$LEN(zRe=ok#mjL;%YzW#wx_`4U@wQt>62?OdaMS?lbZ+=w1G&A1q@YtR(7`zA&s4c2m9^qO=%hdex7&mQhaPqnV z)_q$lubDJd^86>Lt#oOQW|d@r_PlwG>+dN3D)9ND6=`tavb-_Bb2@$$ zftkU{B8Q?`VPRps!P3_i5@190yi0hlzB9d_8E(Ptb5_0{9qiC59_K4FS~Yp5`~ zO%FFZ&Z(xbuA)eL3-i(MQ{$4zEPVuw78Uy%ql@n?ZMdsYI5k82m?J=TfKSOjRcQe6u3ZDB;izgM$>xS6;B+%+h3Sfea%at!P*a?HrSX;)w&ii zZUhJ|Y%!Ra#1c(pIH?jGuVn;7i28lid>e2={oO>D_jj3;i$LWO$WovRQZ(xWgtnl- z#S7H~$>u8UD^1@_cF$;zzy;K-G@mrG5X?@knek+)P)lNy7& z$LN|2SM!n!&vlNpM6flcyOrQfpL(d3fJ=;n$+J>JdyU)a2v)sEux2NxiW$cs(62%D z6fEzx&iWYA{1C#8B(+mqBtbT*I5^C&Blb{k6kw&e`Zh27vQ78SJv8R9YW@bPc2{G` z9~e%WCKW;m1^t~dMa#dIZP<&ttNfAQNAL-+k;-bueJJ9`QB%QWF6$CFb_FE=emMqC z`I;L2gl+`zVRM>Fh7O9J&ynNUWZy2)1@91`0}|ejq3ELErSJLEbHSY1{hLh~#LWK* z0+!1`JIwJVc=FNl`oPo{UYx*4VHk>tCHuK==i!*ONw#QWOaPG0?ulW38!nSMc&=t7 z%53s#MQDk0^}tnDv6c8OqIM>;fk{^Ly4MZ9Fo+)@2k-&hdWMfht!UIrC&awjahI`y z;RMRv=vexL2gR{t3^ux&7U43s$0h3aE2?C)wLOv=n;aHQAHdPQ)ieh8BqDr7%Nphq zk;g74OL7vNl;&+t`JG7H#1l{QY)f#U)b%3BfJPejf9g>`4~=FXA9FjBq=4qk7I%;> zdcOcRCBGQZu)COrzDFRw=E_?h6kJP8y*f)Xf2HG!%_KX$LWbo#IGEAG9qP;%eU|dU zznuFZ5z1^NBNbldUR|}PvH5m(k|&uK-@LVj&x898z{7=H{oU?wl>@h8gE)?Ipjl|< znA%Bxq1}u3;A*rf2w=Wat9m-YL^vvuIL^0m6@POXoeyGN6|&zPh@1@&{Z)3-SE`a1 zOwtmiJ?u=M3tx^UZQ+`~cmQ82-jH%rs~<^869DF9>r`2h&pNp{puG zU#r86jNNSm^X6bz%QNuS4WwxY=N*h()q{mX;l{i<0Waj-*NZc&N&iDk~o=;VlM6A z9#v{(a_tz!F5wI54i_j=4(=GXWq9uGEk433;Zr>IY0U!f;K4l$!)1z47@MCd+2!!A z3bpqJI^Ckqz!bhD7=OpPlF>6bStSq0h+89lswtI3vbCkYYp9XG`$MMKE7V}cmhF^V zXO5op^(s&kMk}TTbLu@ex6Bx@61|cDX()a?Of0-q@N&4A32Sm*8CX_m7RLoSRf;X7 zM`nM?LmCbd^*)Ad{}C9YF6`fE_AhNx&7)fG0k7PhvAEC=3`W6htU zWp2==AenU7Zf*6oE+=uGj=F~8U2$xxPGb)A%$}?YA4u)bSlyx!-QV64hWmOm<;-2a z4N~wLUb@XMw^voyjD3Lf6Jv)=cvsOK7m5v9>%CPfMX5*2x&ZIF1@`KmGh%ipvjax- z!C;tV4~R3tybaKOT!0HMOiNm~@DFw(a=hs%n1A(kK943dj!!-WvI0bA2|oPm(awIG zTa9a6QGClOKM+;ix`eSfh$k!$ddEx&U#K_lVq?w*^_h8icvDt9VHC(z!L6hp)b2o6 zA9#NH(4V;pL`2FO7R!Tor=-9Yjc5~{O4VgxH0U`xcX@bpI`RMY5_Oc|8~Y^Uun=lAbJN%$g_R*K&DV=}#-`}jY#jsd zNe=C>EM_~DyPm*5H7n^G-^p`5xTM?9CO@s-G7ZsK8D?Jex40>{B>Yg^{>vkw5%mFF z-{Up8`e=NVwDt*`jp1sJxnuf|sk^Usd+UAUdB-U>C!lXeuwo6DH+Il1VzT!W9f%;k zq~*2Q6g~jvbL|tClE+^YuT%Es%4!iJmk$*BQO4E9Awd74;Ev7Wj|aw+82v}m7bl5E zPP3_FtSzfIoX;QLMhf@gk!Djw>J8)xGfS>flmPoYqB0w94AOJK}f8Av45AC3HDCIusb1vNm+A*iODb)aZIS? zOECqS&?!g?B!OFD*$p#9bpp1F`4mmwluvrwk2=QmEIo(`OX8l3KH z+s=iB1{sNmhQ-CB+aczq?3O+@HC zMPQ}%pe$#S6St|UOj{E~T0QsIq4)OX84G&~UQy0nKyV7)d>&--Z915!O?hk^uz*6S zCti?R59cwKEmDvp(Zciy8YzZGWkRWe7BaE(`bl}>My5G{$QN>pQ7m&PPlICLU#6_- zbS!ZH*=L%U z9Xpq0;uq*v@~1l_E9>=oNr&=D%s6t}>c07kI+G-5WZf=iBFIa^QEODJ3Y24IVHr~6 zV@E*s&G%e)BV6Q;nrl4zmvSvlx9g+F0-Vy?SEjiXOsLAAfuUSk2<0CGHiwR5v?u}6 z8ZpcYJLl>P*RAYxDz}o2-e`G-8#&wPJ z$3lpYYL_H-@B5KQwEA_Qn6~i!P#tv?2kQzwFl#S438TR`4hSY}yoio?$?Z=>NJS;_ zk**VEQl2`ObT!vTsn~n|L%H$=I^+e<;0_hvtmluc`XW4n#8?eR|AK(8anZor#Sxj&TO7=))S zSUjx!xgR?HuBRh1wp8?=O0FoRGKeA?mn(DeWlafbXVim_!#}j-W1Hcohl$-)M|u;C z*@7r%xBD{;IZ@_WpkTnFv#lNM(EcU0y{nFbK38DsYBi7D7obKKTvEsyV(zXpB<1K(YZ{ z2_$W#1zi+K-(8HcJgn>iW!A-u=-tR&7ibgegqbZxL@tnS8hqlT z>us<)#`g5%q7pqtWPsIS!NAq(csZe8S{Zn&m^V3FtkyD6I9*g;xj+eyddGOWWMKvN z@d0TH1uyL2K;UrL!NDQ0C$JgTfM9TM>UI-a!u#8g*m3o4#wgbr<1`;%wRdtc9Z}j! zF6U(Kn25mPf_^-l{oXSqA>)MFAuNw_=zov&w^!GYsAoDI_j_E&4Gr_wp{(+FB)Sw7 zE+#6oCBSG!LY%2!+)vh0rv_jwgo66>R7+*DG`DmEz}aOT!DEO1%4=9OVoon;AvQE=Rr>w=TMS>Fd95T@|c;M`=3QS`U^H zPC7OQxKKT`>D@hppCxMp^eBwb0Mo)_eDo&9LNg`Y#CnbmNV!8C<~+3BIUTp*BObZ? zB|tT(h}WZi3?e1_RFHFh4|Ey>kwt#_N~ADFaL1JMM21Qso^*{!U0G-vpsB@r zRp83dVN$@#ILT8xCO~M5IMhsN-F{j}E}yZ=GamM+LCn*cLa|Ae4RIq_s#O^%VIHql zzu&pPKeZiqhR&`vn;kk1nJ_GhZT@Y|GG0^Di2W>9bzNAW2 z;^D(gp`Z&$b>#A4W=gE4ml(eR$aLB=WOj3f{`iHcf0@>m)3+wrk0+)qK+CILq$a)O zEWbT945F<5rfPR;0z|CZKbDHX)~Yi$@@`g8OB6gQD%L+A>Q4exyZM!&#Y?xhD9l9mueL{XS8ZT?&wNfHE2GD#+>bpg#uQie;$OV)&gSBI{9x)4j#SQ>Ef zCDB0yqC%givr8#vB=tULrC&{<4?Pr*Q>8?I@1Q`;{t3tK`ttS;to^Mslcln4je$^` zf>7)!(u_@1?kPoe7*=PF4(VN$YF6}xZ=`R10kKR%0xv2x7mnwl=OMn`AnPU%<4cZ3 z)oWGiQ}5^fAP04s1zt4$380|nkr+!tP9f;~EPm-yM!dAOOD9HfrXz`FpNJUfW&*u~hJIf|?*l^0|jqLTmz%#?();cS! z9+u~!mSmV29R1NQv0!|vL-10|-i$QQQCE`vT*<}Q=`=}NWM*K6p>`bTShJ#)ax$md zx!6G$WiHU^fvO>XOdlao`RX0oMc;ZcoRR^KJbPQve=gByroTgCbCBX8Y2xaNlf_GU z6k=I(VruYEN-Thk+i4gLEl%tx_s1t_2F@l<%v4|Xw4}WvJuIDcYg-_+p$GMc5*vS) zD|)?W5eqzpip(J?J;gjxL!ihSky96{zGjSAF621sbg?W=@ll{au1j(;*aZ%>5fEA# zT`g{`QYzK#@scTMPFrlxnLcV8-|?l1TRI5)I|;y4(KiHlA>!pJXMjvUN?-vqHynBW z(4GFXx0+mLBVv~$I=o)Sj)h`ccwnWR6&=y2V9!c9^Z#j9DcLiIKKHV~T}BUnR7-U~ zy0XlHiVW3)kLX}KhuyaVK{HEY9*D_&+b8VJy@VU$dH>$r4c1?ekEOdQWa|cs34uauCR4j6@Y2XSLVoI$Yt+3xgBD4% z=u8rx*s1uH!Q|2l<>%9Ek{kR{GC%0Ug}i4Dv(g<%x(_3n*C3;=ZxB}T&vVI((4Oll zjZ$*=y&ha-v7S6+yW|1Y)GRhE2X30wNJYk9GH`u}E_E9V&~;cEM4eV{U>o-MD4~yU zs$>BwidM?3-^6cW)RO%J{9K_yPUSE;paQ%k$)LdLa)m}wPn$r20QqBwJH#yes|>}< zTvmo6KfU089id$|nJVAncTZ-*{H|mL27xNb#Y*i|bFwlKsjYr7aXIYrWMEk|HE|wb zgR1h0rxsaJ{UbkUB4yK#{L_XMD?{jRb5pkp>xnZmIP~%}Li_M_s?#<0Sew~CcBn(s z3#enTu(uy?eQviVh}g#0n#eTTR;Hj2`7R`~005tGbSHq6j<|ss;9+kdO1?hrzsg`O zU9G2XFp*jyE{thLS2=gmLjIakY(ghogV(P_eBFbCI`s|uE-oInU3W~~TR6aG@cC#> zOl zR+zRWazakl%F1&#T{71>{^d(W55X7r#Vx`2t?#&7o=w=7QU#SZZ{|xUNA&|*_?=3B z1Efb4{3051y9@fH%-bL76rN8=@;8r`Ugs@=!4o`(C6J|nk=l!mzD*w+N8ixRY&{GX zj#4Wm;$-BFHIyODS%i~+0~j~$eG38!I&YCkA@XC(`Fa3b%JRB)&eJ*HLK@d9wB>2K zvk?RGR#b8k!jKicu|ExVB+;1A6pO}H7E2fhIJmN^ ziTQ@mcg{s^rXV<# zAx54scJ?DzbF_E32+wwQ2tcp0JXBApY!mNN)>?s9oO1Zx6a|oUR|Euq-2Nv>tZNF` zsS`a_xAlm=BfD@FH9I&5pP#bjCkaT?1DShmw7B3MTJvAd*Surm$x6axO7pft*eQQf z7tuIz;8f1roCk|xBC-e8f+gCe(H12~<>uVOgvvgS>TyC@UNF3A+T|jhZk1~rdv$bM zBc)SHj%R#2`qh8H=@x*p9B#Zn>koRep+TX3W%OO^v6R+$^^k`L7q;G&vkS|_&M$5{h*{QW zL)av%iv3m-^%aelP$RynJeG2dn21qfZjn}{*P=6)|mBH?$9@xrT-!ZZ3%Nf+wRm$GqrEYYHQ$T z)5DeA)Q{ZE=yfrRHaJ#c+y`Dj=fIU4q_UD?tYUU?8F&0eW3XNyF>q9u5NkDN;VeLY z2{`ngk%?{AFAg`lB(IGBG#n4i`n+~?fYp2dF!8h6ZB>Y3Epro9Ys>GEe8btE!fZ0? zj4-l>yUy=CBs47_AZ4Fb?ck*8!~=-%rX_KLAUoTbf)Nf(7jgZ11_SoHMm%rp6kk%V zdAID-KkfPpIBYUGkYN%Cv_qa-YYw=zDcd6s8zzKzd*y2N35`2^eT~TsRrm9|MQ*V% zXua)YO|@~4_PZ;Xn41|XEvxU!mj|H^0}dSo)F$R_5hJoIr(>q?0_wV_n4#BFHpM84 z)Cm2DO=U^Jhl~q>BD&=gwtk%@7b1m_5QnUr<4zUHJw<%kGci#XqoX4PN8(y%)2Io3 z6~hpMNyL}aF{tR-UX|k#sx8nCpPWxc#~(a06ZERR;cuvkPg(Cs0DnSV7f>-+v^55k zw;5}wi7oa5*+VXj{GQP2d;H0nt=2`Y()G=!sWZBkn+9Y^c+Jk70i(;KDKTb5LLkik zg3lUhBn{GZsq`%ARtrEd|2BYa(3TFU}H930y z_IzCOi`R3X%BnYoUfQ>b%tAvX!)v(ySAdk3FR#Y&RM=7W`w8RiD({RfIhz}i(7KFPgDV;hQDv>1^ zT{jr5_{5j=4`+x9Tam&jptk}mG0E)S$x1GFUq)c54E_){q3dk9x{&2sg2W+9B(hGf zkn2n;rHIO18c#n3Qk#jel;&Tk|Gc%|X!IyTlF)kn5$cZ1_MywecriDpB*}jD9N&*u zKP{}|L-Oe>Umy(0%v*uH=(dJkG}@|x->&_pY}*r9gqle~?bZuB zCx%s^lj_>IkiPYxt|hEdX4h4TnDc6p^S4fW(G-EJHI_a<_!4}U_+TAV%|{=hWDsN5 z@Q@oB$LJ3hb43uliZc!Wqm)F=;P82-&UvaphQLmpV^8{CVs7K1woqlf zl^avBv_u6=88^)XEZc<*8RksXR{7T^9+-FCFD9Mw=0KM6aK?slL_Hd02_c?9j*;K%hgoB$t@}RhqzkSq&l+q#0E^}u z`C>bFnYX^nclE;v@z|tKj|K6V!jOhG4SV5RinXMG1SnJPKG-&Or%M@KK2{5bovs>s za}+3`0{dh19qBri;8nM10urVx*_9BSGKt^}2Lnz-3i{U+#XVnre2s37O9n|xTC-}< zxyXs}U;F-1*mm=NFZX~b@lJ)2N~b?fB_7Tsr`YJ@1W#>rNgoJHs!dcWSn3d~h?T0I ziIS;*V!7xj|D%aG2>u zx7zY*SqAJ^x&)Q^=Mj_{kcCJUM%NZBd`}DVhvT*S)<0oaL|^YpxJjF01! zZU4L}babL<{yaIeO}^>p`|aiWPbWuX?MdHEI8bWuy~Bh5K~IjX;e|hx$St;R6tEjGqRCVKS3hJ&E<_ywl2ipiC2=B(t`Kio%$1QWsfP}P^!}6MMq4-U zkFKTVa`x>&R{S>G8;ShS)3F&DSQp0C*Lz*IynXWMvHe3tJ!XSf@#;x1PsViU@V>3t z)esi3E>$<1C4!>r62y47@#od|=d%}(?FCZhxK{XZ6JaM-9}{cRg5GyX|9peh=< zFprHBG+I$E4hi|)&wR2FM!=T1STTb}(D^dQ2_3DB1Qhh#iwSi<$+$Q(AL}TwdYsbo zR_UhFdif+pC$3WY>Z}Lh*2jl_;!$E{XM>ZE@AVnuli#M!CuL9}Ogt_g)y|f9^W)vW zZR6_^)TQAT`)swxWgbY(p>6yd2z$1$PJyjn zvI7&$ra{rb6|*^;bg4PIXsNm0P^mc!TK=@ht|RaxRE{C06yYpk3<@$yzd{D!J`x=J zCH>*ud|i;gwvSOz1+X+2Z1|zE_oeI{p#(}GTQw=wZ6~R39l@AlGrscPg%bx>Wl^79 z^^CH1!UwGxW1hyx*xMemhW>eVi;sud$`;*S61DD18KR^)P1V*^X(!_wDd!cv4Grli zsF6N{XHy3x0Wl`GN*0+s`Id_6^K9v;4x6bX>j{mHE;v3w)10<-Gf~E0E&3Q(DfcgR zQGab5zY!5xqHVi?gT33`1+lN|mglxv42Db#bwQAFVMpU2B78^2u0y#bZ2oDd!b%v) zQBn}ND-NbJDBTv4ZE7k<6XboUqW*|(ti_*83IMP zdEAFzS6*ALi&7&Jc3e~Hi-m}4bSy#g>wtq$PdrwjO9JCC9on8Cya}Kt z0m{Ro%gX}+dd8R6GS*$Le5CHi8Yx%U0M>`Wc}hJIsI~SF-Mw>4{@uXzWfCjBCLVFv ze;eS0e2|cv0E&Y(aU6-m7}F}?BJR_z#3R8tap%=%(Ot+bj+$F0o}FZ*UsSb@#KZ7= zv=7_bX&9OeX_^!hBh&W#CP4p778xkPD(TQ#WmGfXSrxpNpO z#UbJ^#GfSsEL8d*1Cr8sdJP((9r3&*KHM~IF=NjV)t%#DmI(=X0 zVe$S})4^}je{3UYcG^%_5MtNvZFTl1*!x~yBrGEoeGiY9T&_0XF`3K^lR4`**Ve7T zsAk&pCgRs%tAwt`LolEr;2~G3<5{#;j$9F2EYuMs_N#@uI4hM$P{Z zbP{Mpr+@!rG5H<2@}1-s;Umu}Afx=zL?}+$>D3is*YA>(VPQ=-w47?bR#fj-A7tv<*`oD+iNml+r3(~?%GbNK zPl474S1UfFoGh?_d`n>SbbB*VxK{8>Eu?xrx10z0RM>=w@ zUUJZ04=kQPx-j5vM68o{sWL^C?mNsf(&8+t5R%X5C)0iws*7ix_k0l1VG_|ftA|aC zvGM$$jEvuzE`egt;>RvVKAtRKW**%m0fxO!=NatN_})N4-lo5r4r+qNAT|~Abo41h zQ?7&QTCd5y%#mUvg6gS$l*{JyF@~3L+)b$$yPx-R5H*X*^;{QOTW;7=6YIZJe)hiK z`o6oRy9m>dE=)WFl;eP9_y_i8J`D_sN5MKgB-adGaKbEgt7-Y|jI`!VYA{XvDKK6A zzQum{)cT^)$9hs$4kxakuKT0|xxubIwU))Qs$ogAp!0@NpWN3_dh7wIuOe(h+6~hQ z7^S9-mc*-iOcsXzM3w$etHl~5!8100`d|03eXT7yh@->LRl59AVu+G?1yqV~!vWf+ zyDL)Ou(d+?aWbS;c%eEs9R#~B0 z-umrO))B0FDjd3-xX{k_t|Rq&ZLIfAm9aVh=%7jjq|KLDfCx%0rFEf;WB=F=#QWH7 zBE5r4%dhlSb@p{=%kv9XadxHH6Ggc!S`eki9oGE$OMCN)TznpKV7SvtjZ|A(@Z^>Q-3gII68S@3Pm||lCX9)LG-R{+XP}l z0c~bwWa1 z)+aT3j~SWdc0x0gCK}tq{0*zBnwI_a>iQgl+g$~TUyrVuaa7R|Upzi0QR6*emRhD2 z6Bz)wVO&r!2RO|oiHg0rqyn0DK5KKH@t+YYAu1;a*>@a0XTMhXLKurha5ty}e_biN z>@v7Q49G-+6=|9wWS=zgA*Jj4LY7kitNxmPt2Ip*8Smg|ZOUvX)lSTtQk!C#@4P)-w${!PN?F0|kHvB`3~9 zv-480vkWk0(6xhmQ)EPRba*BAXfegZ@X1S4F>5nPNIl6tQ_!=0poEgX&e5~Bh2j?x zozQit5QsY06x{|@rWkk!(=sTS0pmx_H8M`1p%{>Vz&L)oG@~IM*XWF? z97bBq@+dWwRqBqTSW?3Ubo#336$_^~7|NNO_4e zNMv*twNK|39Kj^(%%vzPxgv1-?*%iP6GvvnOiGhq-x2sV_>G>g%y`JbLOor~oY(d> zA)hr#>s>e9*D?KM2o#oL>{8mg1K|!qqchW=mp~Yo^r^5AkGcbRoPbtFJPPYB<&)`7 zrVA46*?iIEaX8XGZn((2PJz}jqi8)}o}4-yj<|t&lunAEGs}mzzujDG;|qlyTQ?U?*8fpt9-rfwJ49lc8fINY_ z2REdPr3~&q{male;vJTsm8)*0{{nlnX9nnOf=>|g)FZL4nzXo9uQtZquRbbKgIx<} z86#+%?Wt=_kZC9Gg*Fp6&BtF|Iz8_7>#PHB4-*`qIa6|yDD|0EN;h?z2)G%{ zJp^@6&fIR26gIO9{VJCl=4yXs*U5LcB-r+DZ5i!;6QWG?u?6s| z8(=kt7&caf)XW(&rbLZpK1-j-15mD0GPF66RXA>t#6Qb~DMNKVUdFI>%DX^3T$)Wc z(j@nDd(A=ES1scO&-bk_r})*FA_SM(Z%W!j z{bQ8MUIRAKE1QyD=juE0J{^DgZPr|wp}jhg>`!qr9D?4s=$^m%dF|^^Ul)M@jP9U* zsWv|>0s1%dQp=?FweK#Le{^$H0TQ4a)W%P(5B~d)^N1>JQEn7XsL2!kkxfh4<+(z< z48|dfK_aTklQ&` z-H72rteFMBiV_v6F>;PyT)z`+_c#wI1d$4t`e#B1fWJ;!i6pD2ht|%2SC3z8E*|xt zDTQpoTn8c$z$Alag=0PSNUwT>Tp)vCGCeL8x?<&i!NfR3qk~gsRfdgSIE66_(}l@r zLV|9>QHc&i5fpXkutowS{os-J8uy52zi+A$w6-VAMQL($3CGS$SABRm+|{?x&(kq! z=%IK6RywA&d!&7$*1E`6>dG`5IyThy@D7dx28B=~e4;m#Wg4mB<>5Zt%g^OKv4rFM z!A5E^>GkEGm~Wkyg^a2O2+QL(Z3YMgvdh&Rz4HA~ET6Xj1Kw2c2d6~t3=av{+MsrL z(fZkk8ZhG0lbp7RsFN>UoZi|Q~ z!sTH@w2T`XEdhtE$HErR99$3J52-?phWve$Ow$B$g-9^GAuB5pVB!Ig! zO+YqZ8!Tu8AfbzMiYM^V28&W%NzxBS?!L1>Ur99M@nT(K;Fs{MH$`oQ&NGMY{|grf z)oliezgf>>BPc*@sLN<`gdC9nGsMORZGPr#fUTi%H>}?v|M4~X#$2})`Ua}uIy2r6 zM*U1`_4RrknR%2t$DNzsk9_@0pm(Vo4jo2Ib=hypce7ekt2&);kRKN@JrU-h7j?Ho z1bk#f3`BPNnT;lHQFxLOW}_8vv_J+yh1*-7nQXg8M1XS}5c)-0k?pK;U3^A&7uxk~ z!H4uJWc^e2;xOfokZms;TmkPRAtjtoy%Clf0ABRb|ia>YDbKtd(VIcHY^| z=-6+Q7IFfK}vNRUPsRxki6E?)I9@i>Sf0YK_B&bVNa z42Z?dcA0Oz4L*?~eH#tS$Kv>AGE6Ux+1n$-gcm}-?<^46-MD4i$9RPw@CC@uXrN|P zQH~gD!z@}=+R6%8K#c|=CBZH&R*{leW$9F1r0rY4$ar~o#_4j~#_`w~6lOLi`6wB* zxycZ@p?1uybRzYl$+Q?(!@2;#Bm&^kNR&u`?h?fet{2U+VTN7LzW<({;c8UuUk*q?9Lb4 z9VS%$vk|Hqq3ZSsRVY$&ls|3EQ^I)Hj?`4}gwzG}pB#AqdM8=J zzucrZm~XKADLHTqb~ma`trlnYwB5&>Sv@Tf8sof`g})JKgp50tR6&M&;r0&Kz9 zc!_+&q&qya{_B|KbEdW}jm0rbFBSE>(5@Hduu0Tu%Mj_MCMsP>7th+pu|%a=S#8n$ z*wtS|DNCmZWy%9E&H>6@M{24DW*eCOU164q4}WjOhemw3W#YqMk$b=iZo=pNW%IzW z8oCf~#W`R1VguLw!U1Hg-#_jo>-V?oWBusUB*{V?3cHzArmJn-tXhYv#iS6vOZ7`@ z1X310ukG{3+JH6A48)od)16(7nu>n0PK#yPgZZ_3f$aWI-+lL8r{r3er$L+{nQU8A z5S!9|kdY-+7?75jR}@?%)7%ONU7J19%l$2oppz&Z;7RX2|?vEl&G-8qss_C)ivjgoU(meO$e@fMy1E5)7DA_N}=l<90mWyd&R_>&0tW-O|_C0YrTF(>T%=r zO=czBGvCZEveAhTAMgc^hl{D|x0mddHmFjAMTggS7Ro1~y1q9B+j3!*mA;}Tq+#J2 z7Or988Wyf$;TjgMVc{AUu3_PR4;GFY2L7`#3^ay;+hZ8$6MkFcAI)uzKjv)>Q|L3} zr43lgxQN51-rF!RsdXXHaKXI5OTEksy{NdP5dx~isl5RHI0TlQI0|3vcJN@)Ip9kk znu}+?g_0b9ZrKkb09+AejTZXKXB5e2^vP!&`Ug1W1V+-$y`cLx$VWa1bwDf}*Gs#Y zT-T42bNG>U6Hz0Xcwu+Dm3e?t`qLE9TmB*rnYsJ*pc{A!z_PjKFu$U$R@#a7cel3> zloq?&ZF6e?l+^B1lyvfL@rDs*x%4Qm~)0VJLnIW&l?G&FmM(;hq_06uono7R8r9nBg!*cc3CVU zP9po5v3LBmYLX51w~~x@4;_ZdpdyF0@IC5Bd03jYwU?D9OhVT3@fn7;CTGjFO)lkg*ZzOs611)V`@8dpL>Q;BGn;#|2P&kcwz4?d3o@ zVf|AVMK>Hm*c~K&x~gR*#F*s45jEh_p)pU2W1B8O2!|1`D=JDpz9jP2w|jXRBBU-T z;)Nwep{&dHA?l__cHuNa2Vfs#yQd4=g&~%DmOVxo5`&?e;>smkdgX;M>3_fv9*WmD z5hMo&71M6;BZg5i9LlSmL_-f|g)*g*Jh6aNyjd}zkCLXv6x)Y``cCJbrdf^suUK*i zu9kCfSh;#i+*$sxyW_!Deh9||1_Y41LmvH^>Uqau?Bw1P%rG%WWmqYC&+F_B<@8Lt zQA9q9@_;3Y5Hv(S71>zS)iKTAraDm{L&E@FPLJ6VDROdzNU}T|)zj&RB40 zVK2N#+a(1*J5~~XGk%0O

}DcJdN7WFCK>EklIFNlZF6z@Hxi!brdW@b*pj!5*^m z<76;JSHeM8s^klI!M!rqtoq7{sKBSN4U?5FPR>mgs~0bS|J#+N5e~Q(6A|0Y*!i~q zMC+b@I_Hs*pKNc}gce@9IOfPNtyWP4*JL`5GiaMdI(DZ|{@>SubCsx>b6#Fh=#_@jW*W z@98xTZ}$qrdukru_S!=;hXdC(UdvIg1qwIC2Eoe zI4HQLU)b9!Fzx;WPa class extends superClass {\n /**\n * Dispatches a custom event with an optional detail value.\n *\n * @param {string} type Name of event type.\n * @param {*=} detail Detail value containing event-specific\n * payload.\n * @param {{ bubbles: (boolean|undefined),\n cancelable: (boolean|undefined),\n composed: (boolean|undefined) }=}\n * options Object specifying options. These may include:\n * `bubbles` (boolean, defaults to `true`),\n * `cancelable` (boolean, defaults to false), and\n * `node` on which to fire the event (HTMLElement, defaults to `this`).\n * @return {Event} The new event that was fired.\n */\n fire(type, detail, options) {\n options = options || {};\n return fireEvent(options.node || this, type, detail, options);\n }\n});\n","// Polymer legacy event helpers used courtesy of the Polymer project.\n//\n// Copyright (c) 2017 The Polymer Authors. All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n// * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n// * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n// * Neither the name of Google Inc. nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n/**\n * Dispatches a custom event with an optional detail value.\n *\n * @param {string} type Name of event type.\n * @param {*=} detail Detail value containing event-specific\n * payload.\n * @param {{ bubbles: (boolean|undefined),\n cancelable: (boolean|undefined),\n composed: (boolean|undefined) }=}\n * options Object specifying options. These may include:\n * `bubbles` (boolean, defaults to `true`),\n * `cancelable` (boolean, defaults to false), and\n * `node` on which to fire the event (HTMLElement, defaults to `this`).\n * @return {Event} The new event that was fired.\n */\nexport default function fire(node, type, detail, options) {\n options = options || {};\n detail = (detail === null || detail === undefined) ? {} : detail;\n const event = new Event(type, {\n bubbles: options.bubbles === undefined ? true : options.bubbles,\n cancelable: Boolean(options.cancelable),\n composed: options.composed === undefined ? true : options.composed\n });\n event.detail = detail;\n node.dispatchEvent(event);\n return event;\n}\n","import '@polymer/paper-icon-button/paper-icon-button.js';\nimport { html } from '@polymer/polymer/lib/utils/html-tag.js';\nimport { PolymerElement } from '@polymer/polymer/polymer-element.js';\n\nimport EventsMixin from '../mixins/events-mixin.js';\n\n/*\n * @appliesMixin EventsMixin\n */\nclass HaMenuButton extends EventsMixin(PolymerElement) {\n static get template() {\n return html`\n \n`;\n }\n\n static get properties() {\n return {\n narrow: {\n type: Boolean,\n value: false,\n },\n\n showMenu: {\n type: Boolean,\n value: false,\n },\n\n hassio: {\n type: Boolean,\n value: false,\n }\n };\n }\n\n toggleMenu(ev) {\n ev.stopPropagation();\n this.fire(this.showMenu ? 'hass-close-menu' : 'hass-open-menu');\n }\n\n _getIcon(hassio) {\n // hass:menu\n return `${hassio ? 'hassio' : 'hass'}:menu`;\n }\n}\n\ncustomElements.define('ha-menu-button', HaMenuButton);\n","import '@polymer/app-layout/app-toolbar/app-toolbar.js';\nimport '@polymer/iron-flex-layout/iron-flex-layout-classes.js';\nimport '@polymer/paper-spinner/paper-spinner.js';\nimport { html } from '@polymer/polymer/lib/utils/html-tag.js';\nimport { PolymerElement } from '@polymer/polymer/polymer-element.js';\n\nimport '../components/ha-menu-button.js';\n\nclass HassLoadingScreen extends PolymerElement {\n static get template() {\n return html`\n \n\n

\n \n \n
[[title]]
\n
\n
\n \n
\n
\n`;\n }\n\n static get properties() {\n return {\n narrow: {\n type: Boolean,\n value: false,\n },\n\n showMenu: {\n type: Boolean,\n value: false,\n },\n\n title: {\n type: String,\n value: '',\n },\n };\n }\n}\n\ncustomElements.define('hass-loading-screen', HassLoadingScreen);\n","import '@polymer/paper-styles/paper-styles.js';\nimport '@polymer/polymer/polymer-legacy.js';\n\nconst documentContainer = document.createElement('template');\ndocumentContainer.setAttribute('style', 'display: none;');\n\ndocumentContainer.innerHTML = `\n \n\n \n\n \n\n \n`;\n\ndocument.head.appendChild(documentContainer.content);\n","import { PolymerElement } from '@polymer/polymer/polymer-element.js';\nimport EventsMixin from '../mixins/events-mixin.js';\n\nlet loaded = null;\n\n/**\n * White list allowed svg tag.\n * Only put in the tag used in QR code, can be extend in future.\n */\nconst svgWhiteList = ['svg', 'path'];\n\n/*\n * @appliesMixin EventsMixin\n */\nclass HaMarkdown extends EventsMixin(PolymerElement) {\n static get properties() {\n return {\n content: {\n type: String,\n observer: '_render',\n },\n allowSvg: {\n type: Boolean,\n value: false,\n },\n };\n }\n\n connectedCallback() {\n super.connectedCallback();\n // 0 = not loaded, 1 = success, 2 = error\n this._scriptLoaded = 0;\n this._renderScheduled = false;\n this._resize = () => this.fire('iron-resize');\n\n if (!loaded) {\n loaded = import(/* webpackChunkName: \"load_markdown\" */ '../resources/load_markdown.js');\n }\n loaded.then(\n ({ marked, filterXSS }) => {\n this.marked = marked;\n this.filterXSS = filterXSS;\n this._scriptLoaded = 1;\n },\n () => { this._scriptLoaded = 2; },\n ).then(() => this._render());\n }\n\n _render() {\n if (this._scriptLoaded === 0 || this._renderScheduled) return;\n\n this._renderScheduled = true;\n\n // debounce it to next microtask.\n Promise.resolve().then(() => {\n this._renderScheduled = false;\n\n if (this._scriptLoaded === 1) {\n this.innerHTML = this.filterXSS(this.marked(this.content, {\n gfm: true,\n tables: true,\n breaks: true\n }), {\n onIgnoreTag: this.allowSvg\n ? (tag, html) => (svgWhiteList.indexOf(tag) >= 0 ? html : null)\n : null\n });\n this._resize();\n\n const walker = document.createTreeWalker(this, 1 /* SHOW_ELEMENT */, null, false);\n\n while (walker.nextNode()) {\n const node = walker.currentNode;\n\n // Open external links in a new window\n if (node.tagName === 'A'\n && node.host !== document.location.host) {\n node.target = '_blank';\n\n // Fire a resize event when images loaded to notify content resized\n } else if (node.tagName === 'IMG') {\n node.addEventListener('load', this._resize);\n }\n }\n } else if (this._scriptLoaded === 2) {\n this.innerText = this.content;\n }\n });\n }\n}\n\ncustomElements.define('ha-markdown', HaMarkdown);\n","import '@polymer/app-layout/app-toolbar/app-toolbar.js';\nimport '@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js';\nimport '@polymer/paper-dialog/paper-dialog.js';\nimport '@polymer/paper-icon-button/paper-icon-button.js';\nimport { html } from '@polymer/polymer/lib/utils/html-tag.js';\nimport { PolymerElement } from '@polymer/polymer/polymer-element.js';\n\nimport '../../src/components/ha-markdown.js';\nimport '../../src/resources/ha-style.js';\n\nclass HassioMarkdownDialog extends PolymerElement {\n static get template() {\n return html`\n \n \n \n \n
[[title]]
\n
\n \n \n \n
\n`;\n }\n\n static get properties() {\n return {\n title: String,\n content: String,\n };\n }\n\n openDialog() {\n this.$.dialog.open();\n }\n}\ncustomElements.define('hassio-markdown-dialog', HassioMarkdownDialog);\n","import 'web-animations-js/web-animations-next-lite.min.js';\n\nimport '@polymer/paper-button/paper-button.js';\nimport '@polymer/paper-card/paper-card.js';\nimport '@polymer/paper-dropdown-menu/paper-dropdown-menu.js';\nimport '@polymer/paper-item/paper-item.js';\nimport '@polymer/paper-listbox/paper-listbox.js';\nimport { html } from '@polymer/polymer/lib/utils/html-tag.js';\nimport { PolymerElement } from '@polymer/polymer/polymer-element.js';\n\nimport '../../../src/resources/ha-style.js';\nimport EventsMixin from '../../../src/mixins/events-mixin.js';\n\nclass HassioAddonAudio extends EventsMixin(PolymerElement) {\n static get template() {\n return html`\n \n \n
\n \n\n \n \n \n \n \n \n \n \n \n \n
\n
\n Save\n
\n
\n`;\n }\n\n static get properties() {\n return {\n hass: Object,\n addon: {\n type: Object,\n observer: 'addonChanged'\n },\n inputDevices: Array,\n outputDevices: Array,\n selectedInput: String,\n selectedOutput: String,\n error: String,\n };\n }\n\n addonChanged(addon) {\n this.setProperties({\n selectedInput: addon.audio_input || 'null',\n selectedOutput: addon.audio_output || 'null'\n });\n if (this.outputDevices) return;\n\n const noDevice = [{ device: 'null', name: '-' }];\n this.hass.callApi('get', 'hassio/hardware/audio').then((resp) => {\n const dev = resp.data.audio;\n const input = Object.keys(dev.input).map(key => ({ device: key, name: dev.input[key] }));\n const output = Object.keys(dev.output).map(key => ({ device: key, name: dev.output[key] }));\n this.setProperties({\n inputDevices: noDevice.concat(input),\n outputDevices: noDevice.concat(output)\n });\n }, () => {\n this.setProperties({\n inputDevices: noDevice,\n outputDevices: noDevice\n });\n });\n }\n\n _saveSettings() {\n this.error = null;\n const path = `hassio/addons/${this.addon.slug}/options`;\n this.hass.callApi('post', path, {\n audio_input: this.selectedInput === 'null' ? null : this.selectedInput,\n audio_output: this.selectedOutput === 'null' ? null : this.selectedOutput\n }).then(() => {\n this.fire('hass-api-called', { success: true, path: path });\n }, (resp) => {\n this.error = resp.body.message;\n });\n }\n}\n\ncustomElements.define('hassio-addon-audio', HassioAddonAudio);\n","import '@polymer/paper-button/paper-button.js';\nimport '@polymer/paper-spinner/paper-spinner.js';\nimport { html } from '@polymer/polymer/lib/utils/html-tag.js';\nimport { PolymerElement } from '@polymer/polymer/polymer-element.js';\n\nclass HaProgressButton extends PolymerElement {\n static get template() {\n return html`\n \n
\n \n \n \n \n
\n`;\n }\n\n static get properties() {\n return {\n hass: {\n type: Object,\n },\n\n progress: {\n type: Boolean,\n value: false,\n },\n\n disabled: {\n type: Boolean,\n value: false,\n },\n };\n }\n\n tempClass(className) {\n var classList = this.$.container.classList;\n classList.add(className);\n setTimeout(() => {\n classList.remove(className);\n }, 1000);\n }\n\n ready() {\n super.ready();\n this.addEventListener('click', ev => this.buttonTapped(ev));\n }\n\n buttonTapped(ev) {\n if (this.progress) ev.stopPropagation();\n }\n\n actionSuccess() {\n this.tempClass('success');\n }\n\n actionError() {\n this.tempClass('error');\n }\n\n computeDisabled(disabled, progress) {\n return disabled || progress;\n }\n}\n\ncustomElements.define('ha-progress-button', HaProgressButton);\n","import { html } from '@polymer/polymer/lib/utils/html-tag.js';\nimport { PolymerElement } from '@polymer/polymer/polymer-element.js';\n\n\nimport './ha-progress-button.js';\nimport EventsMixin from '../../mixins/events-mixin.js';\n\n/*\n * @appliesMixin EventsMixin\n */\nclass HaCallApiButton extends EventsMixin(PolymerElement) {\n static get template() {\n return html`\n \n`;\n }\n\n static get properties() {\n return {\n hass: Object,\n\n progress: {\n type: Boolean,\n value: false,\n },\n\n path: String,\n\n method: {\n type: String,\n value: 'POST',\n },\n\n data: {\n type: Object,\n value: {},\n },\n\n disabled: {\n type: Boolean,\n value: false,\n },\n };\n }\n\n buttonTapped() {\n this.progress = true;\n const eventData = {\n method: this.method,\n path: this.path,\n data: this.data,\n };\n\n this.hass.callApi(this.method, this.path, this.data)\n .then((resp) => {\n this.progress = false;\n this.$.progress.actionSuccess();\n eventData.success = true;\n eventData.response = resp;\n }, (resp) => {\n this.progress = false;\n this.$.progress.actionError();\n eventData.success = false;\n eventData.response = resp;\n }).then(() => {\n this.fire('hass-api-called', eventData);\n });\n }\n}\n\ncustomElements.define('ha-call-api-button', HaCallApiButton);\n","import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';\nimport '@polymer/paper-button/paper-button.js';\nimport '@polymer/paper-card/paper-card.js';\nimport { html } from '@polymer/polymer/lib/utils/html-tag.js';\nimport { PolymerElement } from '@polymer/polymer/polymer-element.js';\n\nimport '../../../src/components/buttons/ha-call-api-button.js';\n\nclass HassioAddonConfig extends PolymerElement {\n static get template() {\n return html`\n \n \n
\n \n \n
\n
\n Reset to defaults\n Save\n
\n
\n`;\n }\n\n static get properties() {\n return {\n hass: Object,\n addon: {\n type: Object,\n observer: 'addonChanged',\n },\n addonSlug: String,\n config: {\n type: String,\n observer: 'configChanged',\n },\n configParsed: Object,\n error: String,\n resetData: {\n type: Object,\n value: {\n options: null,\n },\n },\n };\n }\n\n addonChanged(addon) {\n this.config = addon ? JSON.stringify(addon.options, null, 2) : '';\n }\n\n configChanged(config) {\n try {\n this.$.config.classList.remove('syntaxerror');\n this.configParsed = JSON.parse(config);\n } catch (err) {\n this.$.config.classList.add('syntaxerror');\n this.configParsed = null;\n }\n }\n\n saveTapped() {\n this.error = null;\n\n this.hass.callApi('post', `hassio/addons/${this.addonSlug}/options`, {\n options: this.configParsed\n }).catch((resp) => {\n this.error = resp.body.message;\n });\n }\n}\n\ncustomElements.define('hassio-addon-config', HassioAddonConfig);\n","/** Calculate a string representing a date object as relative time from now.\n *\n * Example output: 5 minutes ago, in 3 days.\n*/\nconst tests = [\n 60, 'second',\n 60, 'minute',\n 24, 'hour',\n 7, 'day',\n];\n\nexport default function relativeTime(dateObj, localize) {\n let delta = (new Date() - dateObj) / 1000;\n const tense = delta >= 0 ? 'past' : 'future';\n delta = Math.abs(delta);\n\n for (let i = 0; i < tests.length; i += 2) {\n if (delta < tests[i]) {\n delta = Math.floor(delta);\n const time = localize(`ui.components.relative_time.duration.${tests[i + 1]}`, 'count', delta);\n return localize(`ui.components.relative_time.${tense}`, 'time', time);\n }\n\n delta /= tests[i];\n }\n\n delta = Math.floor(delta);\n const time = localize('ui.components.relative_time.duration.week', 'count', delta);\n return localize(`ui.components.relative_time.${tense}`, 'time', time);\n}\n","/* Forked to fix the import of IntlMessageFormat */\n/* eslint-disable */\n/**\n@license\nCopyright (c) 2016 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt\nThe complete set of authors may be found at http://polymer.github.io/AUTHORS.txt\nThe complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt\nCode distributed by Google as part of the polymer project is also\nsubject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport '@polymer/iron-ajax/iron-ajax.js';\nimport IntlMessageFormat from 'intl-messageformat/src/main.js';\n\n/**\n* `Polymer.AppLocalizeBehavior` wraps the [format.js](http://formatjs.io/) library to\n* help you internationalize your application. Note that if you're on a browser that\n* does not natively support the [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl)\n* object, you must load the polyfill yourself. An example polyfill can\n* be found [here](https://github.com/andyearnshaw/Intl.js/).\n*\n* `Polymer.AppLocalizeBehavior` supports the same [message-syntax](http://formatjs.io/guides/message-syntax/)\n* of format.js, in its entirety; use the library docs as reference for the\n* available message formats and options.\n*\n* Sample application loading resources from an external file:\n*\n* \n* \n* \n \n \n \n ```\n\n ```js\n import {html} from '@polymer/polymer/lib/utils/html-tag.js';\n import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';\n\n const template = html`\n \n \n
\n
horizontal layout center alignment
\n
\n `;\n document.body.appendChild(template.content);\n ```\n\n2. [Custom CSS\nmixins](https://github.com/PolymerElements/iron-flex-layout/blob/master/iron-flex-layout.html).\nThe mixin stylesheet includes custom CSS mixins that can be applied inside a CSS\nrule using the `@apply` function.\n\nPlease note that the old [/deep/ layout\nclasses](https://github.com/PolymerElements/iron-flex-layout/tree/master/classes)\nare deprecated, and should not be used. To continue using layout properties\ndirectly in markup, please switch to using the new `dom-module`-based\n[layout\nclasses](https://github.com/PolymerElements/iron-flex-layout/tree/master/iron-flex-layout-classes.html).\nPlease note that the new version does not use `/deep/`, and therefore requires\nyou to import the `dom-modules` in every element that needs to use them.\n\n@group Iron Elements\n@pseudoElement iron-flex-layout\n@demo demo/index.html\n*/\nconst template = html`\n\n \n\n\n \n`;\n\ntemplate.setAttribute('style', 'display: none;');\ndocument.head.appendChild(template.content);\n\nvar style = document.createElement('style');\nstyle.textContent = '[hidden] { display: none !important; }';\ndocument.head.appendChild(style);\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\n/* Taken from\n * https://www.google.com/design/spec/style/color.html#color-ui-color-application\n */\nimport '@polymer/polymer/polymer-legacy.js';\nimport './color.js';\n\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\nconst template = html`\n\n \n`;\ntemplate.setAttribute('style', 'display: none;');\ndocument.head.appendChild(template.content);\n","/**\n@license\nCopyright (c) 2016 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt\nThe complete set of authors may be found at http://polymer.github.io/AUTHORS.txt\nThe complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt\nCode distributed by Google as part of the polymer project is also\nsubject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\nimport {resolveUrl} from '@polymer/polymer/lib/utils/resolve-url.js';\n\n/**\n`iron-image` is an element for displaying an image that provides useful sizing and\npreloading options not found on the standard `` tag.\n\nThe `sizing` option allows the image to be either cropped (`cover`) or\nletterboxed (`contain`) to fill a fixed user-size placed on the element.\n\nThe `preload` option prevents the browser from rendering the image until the\nimage is fully loaded. In the interim, either the element's CSS `background-color`\ncan be be used as the placeholder, or the `placeholder` property can be\nset to a URL (preferably a data-URI, for instant rendering) for an\nplaceholder image.\n\nThe `fade` option (only valid when `preload` is set) will cause the placeholder\nimage/color to be faded out once the image is rendered.\n\nExamples:\n\n Basically identical to `` tag:\n\n \n\n Will letterbox the image to fit:\n\n \n\n Will crop the image to fit:\n\n \n\n Will show light-gray background until the image loads:\n\n \n\n Will show a base-64 encoded placeholder image until the image loads:\n\n \n\n Will fade the light-gray background out once the image is loaded:\n\n \n\nCustom property | Description | Default\n----------------|-------------|----------\n`--iron-image-placeholder` | Mixin applied to #placeholder | `{}`\n`--iron-image-width` | Sets the width of the wrapped image | `auto`\n`--iron-image-height` | Sets the height of the wrapped image | `auto`\n\n@group Iron Elements\n@element iron-image\n@demo demo/index.html\n*/\nPolymer({\n _template: html`\n \n\n \n
\n \n
\n`,\n\n is: 'iron-image',\n\n properties: {\n /**\n * The URL of an image.\n */\n src: {type: String, value: ''},\n\n /**\n * A short text alternative for the image.\n */\n alt: {type: String, value: null},\n\n /**\n * CORS enabled images support:\n * https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image\n */\n crossorigin: {type: String, value: null},\n\n /**\n * When true, the image is prevented from loading and any placeholder is\n * shown. This may be useful when a binding to the src property is known to\n * be invalid, to prevent 404 requests.\n */\n preventLoad: {type: Boolean, value: false},\n\n /**\n * Sets a sizing option for the image. Valid values are `contain` (full\n * aspect ratio of the image is contained within the element and\n * letterboxed) or `cover` (image is cropped in order to fully cover the\n * bounds of the element), or `null` (default: image takes natural size).\n */\n sizing: {type: String, value: null, reflectToAttribute: true},\n\n /**\n * When a sizing option is used (`cover` or `contain`), this determines\n * how the image is aligned within the element bounds.\n */\n position: {type: String, value: 'center'},\n\n /**\n * When `true`, any change to the `src` property will cause the\n * `placeholder` image to be shown until the new image has loaded.\n */\n preload: {type: Boolean, value: false},\n\n /**\n * This image will be used as a background/placeholder until the src image\n * has loaded. Use of a data-URI for placeholder is encouraged for instant\n * rendering.\n */\n placeholder: {type: String, value: null, observer: '_placeholderChanged'},\n\n /**\n * When `preload` is true, setting `fade` to true will cause the image to\n * fade into place.\n */\n fade: {type: Boolean, value: false},\n\n /**\n * Read-only value that is true when the image is loaded.\n */\n loaded: {notify: true, readOnly: true, type: Boolean, value: false},\n\n /**\n * Read-only value that tracks the loading state of the image when the\n * `preload` option is used.\n */\n loading: {notify: true, readOnly: true, type: Boolean, value: false},\n\n /**\n * Read-only value that indicates that the last set `src` failed to load.\n */\n error: {notify: true, readOnly: true, type: Boolean, value: false},\n\n /**\n * Can be used to set the width of image (e.g. via binding); size may also\n * be set via CSS.\n */\n width: {observer: '_widthChanged', type: Number, value: null},\n\n /**\n * Can be used to set the height of image (e.g. via binding); size may also\n * be set via CSS.\n *\n * @attribute height\n * @type number\n * @default null\n */\n height: {observer: '_heightChanged', type: Number, value: null},\n },\n\n observers: [\n '_transformChanged(sizing, position)',\n '_loadStateObserver(src, preventLoad)'\n ],\n\n created: function() {\n this._resolvedSrc = '';\n },\n\n _imgOnLoad: function() {\n if (this.$.img.src !== this._resolveSrc(this.src)) {\n return;\n }\n\n this._setLoading(false);\n this._setLoaded(true);\n this._setError(false);\n },\n\n _imgOnError: function() {\n if (this.$.img.src !== this._resolveSrc(this.src)) {\n return;\n }\n\n this.$.img.removeAttribute('src');\n this.$.sizedImgDiv.style.backgroundImage = '';\n\n this._setLoading(false);\n this._setLoaded(false);\n this._setError(true);\n },\n\n _computePlaceholderHidden: function() {\n return !this.preload || (!this.fade && !this.loading && this.loaded);\n },\n\n _computePlaceholderClassName: function() {\n return (this.preload && this.fade && !this.loading && this.loaded) ?\n 'faded-out' :\n '';\n },\n\n _computeImgDivHidden: function() {\n return !this.sizing;\n },\n\n _computeImgDivARIAHidden: function() {\n return this.alt === '' ? 'true' : undefined;\n },\n\n _computeImgDivARIALabel: function() {\n if (this.alt !== null) {\n return this.alt;\n }\n\n // Polymer.ResolveUrl.resolveUrl will resolve '' relative to a URL x to\n // that URL x, but '' is the default for src.\n if (this.src === '') {\n return '';\n }\n\n // NOTE: Use of `URL` was removed here because IE11 doesn't support\n // constructing it. If this ends up being problematic, we should\n // consider reverting and adding the URL polyfill as a dev dependency.\n var resolved = this._resolveSrc(this.src);\n // Remove query parts, get file name.\n return resolved.replace(/[?|#].*/g, '').split('/').pop();\n },\n\n _computeImgHidden: function() {\n return !!this.sizing;\n },\n\n _widthChanged: function() {\n this.style.width = isNaN(this.width) ? this.width : this.width + 'px';\n },\n\n _heightChanged: function() {\n this.style.height = isNaN(this.height) ? this.height : this.height + 'px';\n },\n\n _loadStateObserver: function(src, preventLoad) {\n var newResolvedSrc = this._resolveSrc(src);\n if (newResolvedSrc === this._resolvedSrc) {\n return;\n }\n\n this._resolvedSrc = '';\n this.$.img.removeAttribute('src');\n this.$.sizedImgDiv.style.backgroundImage = '';\n\n if (src === '' || preventLoad) {\n this._setLoading(false);\n this._setLoaded(false);\n this._setError(false);\n } else {\n this._resolvedSrc = newResolvedSrc;\n this.$.img.src = this._resolvedSrc;\n this.$.sizedImgDiv.style.backgroundImage =\n 'url(\"' + this._resolvedSrc + '\")';\n\n this._setLoading(true);\n this._setLoaded(false);\n this._setError(false);\n }\n },\n\n _placeholderChanged: function() {\n this.$.placeholder.style.backgroundImage =\n this.placeholder ? 'url(\"' + this.placeholder + '\")' : '';\n },\n\n _transformChanged: function() {\n var sizedImgDivStyle = this.$.sizedImgDiv.style;\n var placeholderStyle = this.$.placeholder.style;\n\n sizedImgDivStyle.backgroundSize = placeholderStyle.backgroundSize =\n this.sizing;\n\n sizedImgDivStyle.backgroundPosition = placeholderStyle.backgroundPosition =\n this.sizing ? this.position : '';\n\n sizedImgDivStyle.backgroundRepeat = placeholderStyle.backgroundRepeat =\n this.sizing ? 'no-repeat' : '';\n },\n\n _resolveSrc: function(testSrc) {\n var resolved = resolveUrl(testSrc, this.$.baseURIAnchor.href);\n // NOTE: Use of `URL` was removed here because IE11 doesn't support\n // constructing it. If this ends up being problematic, we should\n // consider reverting and adding the URL polyfill as a dev dependency.\n if (resolved[0] === '/') {\n // In IE location.origin might not work\n // https://connect.microsoft.com/IE/feedback/details/1763802/location-origin-is-undefined-in-ie-11-on-windows-10-but-works-on-windows-7\n resolved = (location.origin || location.protocol + '//' + location.host) +\n resolved;\n }\n return resolved;\n }\n});\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport '@polymer/iron-flex-layout/iron-flex-layout.js';\nimport '@polymer/iron-image/iron-image.js';\nimport '@polymer/paper-styles/element-styles/paper-material-styles.js';\nimport '@polymer/paper-styles/default-theme.js';\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\n\n/**\nMaterial design:\n[Cards](https://www.google.com/design/spec/components/cards.html)\n\n`paper-card` is a container with a drop shadow.\n\nExample:\n\n \n
Some content
\n
\n Some action\n
\n
\n\nExample - top card image:\n\n \n ...\n \n\n### Accessibility\n\nBy default, the `aria-label` will be set to the value of the `heading`\nattribute.\n\n### Styling\n\nThe following custom properties and mixins are available for styling:\n\nCustom property | Description | Default\n----------------|-------------|----------\n`--paper-card-background-color` | The background color of the card | `--primary-background-color`\n`--paper-card-header-color` | The color of the header text | `#000`\n`--paper-card-header` | Mixin applied to the card header section | `{}`\n`--paper-card-header-text` | Mixin applied to the title in the card header section | `{}`\n`--paper-card-header-image` | Mixin applied to the image in the card header section | `{}`\n`--paper-card-header-image-text` | Mixin applied to the text overlapping the image in the card header section | `{}`\n`--paper-card-content` | Mixin applied to the card content section| `{}`\n`--paper-card-actions` | Mixin applied to the card action section | `{}`\n`--paper-card` | Mixin applied to the card | `{}`\n\n@group Paper Elements\n@element paper-card\n@demo demo/index.html\n*/\nPolymer({\n _template: html`\n \n\n
\n \n
[[heading]]
\n
\n\n \n`,\n\n is: 'paper-card',\n\n properties: {\n /**\n * The title of the card.\n */\n heading: {type: String, value: '', observer: '_headingChanged'},\n\n /**\n * The url of the title image of the card.\n */\n image: {type: String, value: ''},\n\n /**\n * The text alternative of the card's title image.\n */\n alt: {type: String},\n\n /**\n * When `true`, any change to the image url property will cause the\n * `placeholder` image to be shown until the image is fully rendered.\n */\n preloadImage: {type: Boolean, value: false},\n\n /**\n * When `preloadImage` is true, setting `fadeImage` to true will cause the\n * image to fade into place.\n */\n fadeImage: {type: Boolean, value: false},\n\n /**\n * This image will be used as a background/placeholder until the src image\n * has loaded. Use of a data-URI for placeholder is encouraged for instant\n * rendering.\n */\n placeholderImage: {type: String, value: null},\n\n /**\n * The z-depth of the card, from 0-5.\n */\n elevation: {type: Number, value: 1, reflectToAttribute: true},\n\n /**\n * Set this to true to animate the card shadow when setting a new\n * `z` value.\n */\n animatedShadow: {type: Boolean, value: false},\n\n /**\n * Read-only property used to pass down the `animatedShadow` value to\n * the underlying paper-material style (since they have different names).\n */\n animated: {\n type: Boolean,\n reflectToAttribute: true,\n readOnly: true,\n computed: '_computeAnimated(animatedShadow)'\n }\n },\n\n /**\n * Format function for aria-hidden. Use the ! operator results in the\n * empty string when given a falsy value.\n */\n _isHidden: function(image) {\n return image ? 'false' : 'true';\n },\n\n _headingChanged: function(heading) {\n var currentHeading = this.getAttribute('heading'),\n currentLabel = this.getAttribute('aria-label');\n\n if (typeof currentLabel !== 'string' || currentLabel === currentHeading) {\n this.setAttribute('aria-label', heading);\n }\n },\n\n _computeHeadingClass: function(image) {\n return image ? ' over-image' : '';\n },\n\n _computeAnimated: function(animatedShadow) {\n return animatedShadow;\n }\n});\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport {IronButtonState, IronButtonStateImpl} from '@polymer/iron-behaviors/iron-button-state.js';\nimport {IronControlState} from '@polymer/iron-behaviors/iron-control-state.js';\n\nimport {PaperRippleBehavior} from './paper-ripple-behavior.js';\n\n/** @polymerBehavior PaperButtonBehavior */\nexport const PaperButtonBehaviorImpl = {\n properties: {\n /**\n * The z-depth of this element, from 0-5. Setting to 0 will remove the\n * shadow, and each increasing number greater than 0 will be \"deeper\"\n * than the last.\n *\n * @attribute elevation\n * @type number\n * @default 1\n */\n elevation: {type: Number, reflectToAttribute: true, readOnly: true}\n },\n\n observers: [\n '_calculateElevation(focused, disabled, active, pressed, receivedFocusFromKeyboard)',\n '_computeKeyboardClass(receivedFocusFromKeyboard)'\n ],\n\n hostAttributes: {role: 'button', tabindex: '0', animated: true},\n\n _calculateElevation: function() {\n var e = 1;\n if (this.disabled) {\n e = 0;\n } else if (this.active || this.pressed) {\n e = 4;\n } else if (this.receivedFocusFromKeyboard) {\n e = 3;\n }\n this._setElevation(e);\n },\n\n _computeKeyboardClass: function(receivedFocusFromKeyboard) {\n this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);\n },\n\n /**\n * In addition to `IronButtonState` behavior, when space key goes down,\n * create a ripple down effect.\n *\n * @param {!KeyboardEvent} event .\n */\n _spaceKeyDownHandler: function(event) {\n IronButtonStateImpl._spaceKeyDownHandler.call(this, event);\n // Ensure that there is at most one ripple when the space key is held down.\n if (this.hasRipple() && this.getRipple().ripples.length < 1) {\n this._ripple.uiDownAction();\n }\n },\n\n /**\n * In addition to `IronButtonState` behavior, when space key goes up,\n * create a ripple up effect.\n *\n * @param {!KeyboardEvent} event .\n */\n _spaceKeyUpHandler: function(event) {\n IronButtonStateImpl._spaceKeyUpHandler.call(this, event);\n if (this.hasRipple()) {\n this._ripple.uiUpAction();\n }\n }\n};\n\n/** @polymerBehavior */\nexport const PaperButtonBehavior = [\n IronButtonState,\n IronControlState,\n PaperRippleBehavior,\n PaperButtonBehaviorImpl\n];\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/iron-flex-layout/iron-flex-layout.js';\nimport '@polymer/paper-styles/element-styles/paper-material-styles.js';\n\nimport {PaperButtonBehavior, PaperButtonBehaviorImpl} from '@polymer/paper-behaviors/paper-button-behavior.js';\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {html} from '@polymer/polymer/polymer-legacy.js';\n\nconst template = html`\n \n\n `;\n\ntemplate.setAttribute('strip-whitespace', '');\n\n/**\nMaterial design:\n[Buttons](https://www.google.com/design/spec/components/buttons.html)\n\n`paper-button` is a button. When the user touches the button, a ripple effect\nemanates from the point of contact. It may be flat or raised. A raised button is\nstyled with a shadow.\n\nExample:\n\n Flat button\n Raised button\n No ripple effect\n Toggle-able button\n\nA button that has `toggles` true will remain `active` after being clicked (and\nwill have an `active` attribute set). For more information, see the\n`IronButtonState` behavior.\n\nYou may use custom DOM in the button body to create a variety of buttons. For\nexample, to create a button with an icon and some text:\n\n \n \n custom button content\n \n\nTo use `paper-button` as a link, wrap it in an anchor tag. Since `paper-button`\nwill already receive focus, you may want to prevent the anchor tag from\nreceiving focus as well by setting its tabindex to -1.\n\n \n Polymer Project\n \n\n### Styling\n\nStyle the button with CSS as you would a normal DOM element.\n\n paper-button.fancy {\n background: green;\n color: yellow;\n }\n\n paper-button.fancy:hover {\n background: lime;\n }\n\n paper-button[disabled],\n paper-button[toggles][active] {\n background: red;\n }\n\nBy default, the ripple is the same color as the foreground at 25% opacity. You\nmay customize the color using the `--paper-button-ink-color` custom property.\n\nThe following custom properties and mixins are also available for styling:\n\nCustom property | Description | Default\n----------------|-------------|----------\n`--paper-button-ink-color` | Background color of the ripple | `Based on the button's color`\n`--paper-button` | Mixin applied to the button | `{}`\n`--paper-button-disabled` | Mixin applied to the disabled button. Note that you can also use the `paper-button[disabled]` selector | `{}`\n`--paper-button-flat-keyboard-focus` | Mixin applied to a flat button after it's been focused using the keyboard | `{}`\n`--paper-button-raised-keyboard-focus` | Mixin applied to a raised button after it's been focused using the keyboard | `{}`\n\n@demo demo/index.html\n*/\nPolymer({\n _template: template,\n\n is: 'paper-button',\n\n behaviors: [PaperButtonBehavior],\n\n properties: {\n /**\n * If true, the button should be styled with a shadow.\n */\n raised: {\n type: Boolean,\n reflectToAttribute: true,\n value: false,\n observer: '_calculateElevation',\n }\n },\n\n _calculateElevation: function() {\n if (!this.raised) {\n this._setElevation(0);\n } else {\n PaperButtonBehaviorImpl._calculateElevation.apply(this);\n }\n }\n\n /**\n Fired when the animation finishes.\n This is useful if you want to wait until\n the ripple animation finishes to perform some action.\n\n @event transitionend\n Event param: {{node: Object}} detail Contains the animated node.\n */\n});\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';\n\n/**\n * @demo demo/index.html\n * @polymerBehavior\n */\nexport const IronControlState = {\n\n properties: {\n\n /**\n * If true, the element currently has focus.\n */\n focused: {\n type: Boolean,\n value: false,\n notify: true,\n readOnly: true,\n reflectToAttribute: true\n },\n\n /**\n * If true, the user cannot interact with this element.\n */\n disabled: {\n type: Boolean,\n value: false,\n notify: true,\n observer: '_disabledChanged',\n reflectToAttribute: true\n },\n\n /**\n * Value of the `tabindex` attribute before `disabled` was activated.\n * `null` means the attribute was not present.\n * @type {?string|undefined}\n */\n _oldTabIndex: {type: String},\n\n _boundFocusBlurHandler: {\n type: Function,\n value: function() {\n return this._focusBlurHandler.bind(this);\n }\n }\n },\n\n observers: ['_changedControlState(focused, disabled)'],\n\n /**\n * @return {void}\n */\n ready: function() {\n this.addEventListener('focus', this._boundFocusBlurHandler, true);\n this.addEventListener('blur', this._boundFocusBlurHandler, true);\n },\n\n _focusBlurHandler: function(event) {\n // Polymer takes care of retargeting events.\n this._setFocused(event.type === 'focus');\n return;\n },\n\n _disabledChanged: function(disabled, old) {\n this.setAttribute('aria-disabled', disabled ? 'true' : 'false');\n this.style.pointerEvents = disabled ? 'none' : '';\n if (disabled) {\n // Read the `tabindex` attribute instead of the `tabIndex` property.\n // The property returns `-1` if there is no `tabindex` attribute.\n // This distinction is important when restoring the value because\n // leaving `-1` hides shadow root children from the tab order.\n this._oldTabIndex = this.getAttribute('tabindex');\n this._setFocused(false);\n this.tabIndex = -1;\n this.blur();\n } else if (this._oldTabIndex !== undefined) {\n if (this._oldTabIndex === null) {\n this.removeAttribute('tabindex');\n } else {\n this.setAttribute('tabindex', this._oldTabIndex);\n }\n }\n },\n\n _changedControlState: function() {\n // _controlStateChanged is abstract, follow-on behaviors may implement it\n if (this._controlStateChanged) {\n this._controlStateChanged();\n }\n }\n\n};\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\n/**\n * Chrome uses an older version of DOM Level 3 Keyboard Events\n *\n * Most keys are labeled as text, but some are Unicode codepoints.\n * Values taken from:\n * http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set\n */\nvar KEY_IDENTIFIER = {\n 'U+0008': 'backspace',\n 'U+0009': 'tab',\n 'U+001B': 'esc',\n 'U+0020': 'space',\n 'U+007F': 'del'\n};\n\n/**\n * Special table for KeyboardEvent.keyCode.\n * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better\n * than that.\n *\n * Values from:\n * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode\n */\nvar KEY_CODE = {\n 8: 'backspace',\n 9: 'tab',\n 13: 'enter',\n 27: 'esc',\n 33: 'pageup',\n 34: 'pagedown',\n 35: 'end',\n 36: 'home',\n 32: 'space',\n 37: 'left',\n 38: 'up',\n 39: 'right',\n 40: 'down',\n 46: 'del',\n 106: '*'\n};\n\n/**\n * MODIFIER_KEYS maps the short name for modifier keys used in a key\n * combo string to the property name that references those same keys\n * in a KeyboardEvent instance.\n */\nvar MODIFIER_KEYS = {\n 'shift': 'shiftKey',\n 'ctrl': 'ctrlKey',\n 'alt': 'altKey',\n 'meta': 'metaKey'\n};\n\n/**\n * KeyboardEvent.key is mostly represented by printable character made by\n * the keyboard, with unprintable keys labeled nicely.\n *\n * However, on OS X, Alt+char can make a Unicode character that follows an\n * Apple-specific mapping. In this case, we fall back to .keyCode.\n */\nvar KEY_CHAR = /[a-z0-9*]/;\n\n/**\n * Matches a keyIdentifier string.\n */\nvar IDENT_CHAR = /U\\+/;\n\n/**\n * Matches arrow keys in Gecko 27.0+\n */\nvar ARROW_KEY = /^arrow/;\n\n/**\n * Matches space keys everywhere (notably including IE10's exceptional name\n * `spacebar`).\n */\nvar SPACE_KEY = /^space(bar)?/;\n\n/**\n * Matches ESC key.\n *\n * Value from: http://w3c.github.io/uievents-key/#key-Escape\n */\nvar ESC_KEY = /^escape$/;\n\n/**\n * Transforms the key.\n * @param {string} key The KeyBoardEvent.key\n * @param {Boolean} [noSpecialChars] Limits the transformation to\n * alpha-numeric characters.\n */\nfunction transformKey(key, noSpecialChars) {\n var validKey = '';\n if (key) {\n var lKey = key.toLowerCase();\n if (lKey === ' ' || SPACE_KEY.test(lKey)) {\n validKey = 'space';\n } else if (ESC_KEY.test(lKey)) {\n validKey = 'esc';\n } else if (lKey.length == 1) {\n if (!noSpecialChars || KEY_CHAR.test(lKey)) {\n validKey = lKey;\n }\n } else if (ARROW_KEY.test(lKey)) {\n validKey = lKey.replace('arrow', '');\n } else if (lKey == 'multiply') {\n // numpad '*' can map to Multiply on IE/Windows\n validKey = '*';\n } else {\n validKey = lKey;\n }\n }\n return validKey;\n}\n\nfunction transformKeyIdentifier(keyIdent) {\n var validKey = '';\n if (keyIdent) {\n if (keyIdent in KEY_IDENTIFIER) {\n validKey = KEY_IDENTIFIER[keyIdent];\n } else if (IDENT_CHAR.test(keyIdent)) {\n keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);\n validKey = String.fromCharCode(keyIdent).toLowerCase();\n } else {\n validKey = keyIdent.toLowerCase();\n }\n }\n return validKey;\n}\n\nfunction transformKeyCode(keyCode) {\n var validKey = '';\n if (Number(keyCode)) {\n if (keyCode >= 65 && keyCode <= 90) {\n // ascii a-z\n // lowercase is 32 offset from uppercase\n validKey = String.fromCharCode(32 + keyCode);\n } else if (keyCode >= 112 && keyCode <= 123) {\n // function keys f1-f12\n validKey = 'f' + (keyCode - 112 + 1);\n } else if (keyCode >= 48 && keyCode <= 57) {\n // top 0-9 keys\n validKey = String(keyCode - 48);\n } else if (keyCode >= 96 && keyCode <= 105) {\n // num pad 0-9\n validKey = String(keyCode - 96);\n } else {\n validKey = KEY_CODE[keyCode];\n }\n }\n return validKey;\n}\n\n/**\n * Calculates the normalized key for a KeyboardEvent.\n * @param {KeyboardEvent} keyEvent\n * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key\n * transformation to alpha-numeric chars. This is useful with key\n * combinations like shift + 2, which on FF for MacOS produces\n * keyEvent.key = @\n * To get 2 returned, set noSpecialChars = true\n * To get @ returned, set noSpecialChars = false\n */\nfunction normalizedKeyForEvent(keyEvent, noSpecialChars) {\n // Fall back from .key, to .detail.key for artifical keyboard events,\n // and then to deprecated .keyIdentifier and .keyCode.\n if (keyEvent.key) {\n return transformKey(keyEvent.key, noSpecialChars);\n }\n if (keyEvent.detail && keyEvent.detail.key) {\n return transformKey(keyEvent.detail.key, noSpecialChars);\n }\n return transformKeyIdentifier(keyEvent.keyIdentifier) ||\n transformKeyCode(keyEvent.keyCode) || '';\n}\n\nfunction keyComboMatchesEvent(keyCombo, event) {\n // For combos with modifiers we support only alpha-numeric keys\n var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);\n return keyEvent === keyCombo.key &&\n (!keyCombo.hasModifiers ||\n (!!event.shiftKey === !!keyCombo.shiftKey &&\n !!event.ctrlKey === !!keyCombo.ctrlKey &&\n !!event.altKey === !!keyCombo.altKey &&\n !!event.metaKey === !!keyCombo.metaKey));\n}\n\nfunction parseKeyComboString(keyComboString) {\n if (keyComboString.length === 1) {\n return {combo: keyComboString, key: keyComboString, event: 'keydown'};\n }\n return keyComboString.split('+')\n .reduce(function(parsedKeyCombo, keyComboPart) {\n var eventParts = keyComboPart.split(':');\n var keyName = eventParts[0];\n var event = eventParts[1];\n\n if (keyName in MODIFIER_KEYS) {\n parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;\n parsedKeyCombo.hasModifiers = true;\n } else {\n parsedKeyCombo.key = keyName;\n parsedKeyCombo.event = event || 'keydown';\n }\n\n return parsedKeyCombo;\n }, {combo: keyComboString.split(':').shift()});\n}\n\nfunction parseEventString(eventString) {\n return eventString.trim().split(' ').map(function(keyComboString) {\n return parseKeyComboString(keyComboString);\n });\n}\n\n/**\n * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing\n * keyboard commands that pertain to [WAI-ARIA best\n * practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding). The\n * element takes care of browser differences with respect to Keyboard events and\n * uses an expressive syntax to filter key presses.\n *\n * Use the `keyBindings` prototype property to express what combination of keys\n * will trigger the callback. A key binding has the format\n * `\"KEY+MODIFIER:EVENT\": \"callback\"` (`\"KEY\": \"callback\"` or\n * `\"KEY:EVENT\": \"callback\"` are valid as well). Some examples:\n *\n * keyBindings: {\n * 'space': '_onKeydown', // same as 'space:keydown'\n * 'shift+tab': '_onKeydown',\n * 'enter:keypress': '_onKeypress',\n * 'esc:keyup': '_onKeyup'\n * }\n *\n * The callback will receive with an event containing the following information\n * in `event.detail`:\n *\n * _onKeydown: function(event) {\n * console.log(event.detail.combo); // KEY+MODIFIER, e.g. \"shift+tab\"\n * console.log(event.detail.key); // KEY only, e.g. \"tab\"\n * console.log(event.detail.event); // EVENT, e.g. \"keydown\"\n * console.log(event.detail.keyboardEvent); // the original KeyboardEvent\n * }\n *\n * Use the `keyEventTarget` attribute to set up event handlers on a specific\n * node.\n *\n * See the [demo source\n * code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html)\n * for an example.\n *\n * @demo demo/index.html\n * @polymerBehavior\n */\nexport const IronA11yKeysBehavior = {\n properties: {\n /**\n * The EventTarget that will be firing relevant KeyboardEvents. Set it to\n * `null` to disable the listeners.\n * @type {?EventTarget}\n */\n keyEventTarget: {\n type: Object,\n value: function() {\n return this;\n }\n },\n\n /**\n * If true, this property will cause the implementing element to\n * automatically stop propagation on any handled KeyboardEvents.\n */\n stopKeyboardEventPropagation: {type: Boolean, value: false},\n\n _boundKeyHandlers: {\n type: Array,\n value: function() {\n return [];\n }\n },\n\n // We use this due to a limitation in IE10 where instances will have\n // own properties of everything on the \"prototype\".\n _imperativeKeyBindings: {\n type: Object,\n value: function() {\n return {};\n }\n }\n },\n\n observers: ['_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'],\n\n\n /**\n * To be used to express what combination of keys will trigger the relative\n * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`\n * @type {!Object}\n */\n keyBindings: {},\n\n registered: function() {\n this._prepKeyBindings();\n },\n\n attached: function() {\n this._listenKeyEventListeners();\n },\n\n detached: function() {\n this._unlistenKeyEventListeners();\n },\n\n /**\n * Can be used to imperatively add a key binding to the implementing\n * element. This is the imperative equivalent of declaring a keybinding\n * in the `keyBindings` prototype property.\n *\n * @param {string} eventString\n * @param {string} handlerName\n */\n addOwnKeyBinding: function(eventString, handlerName) {\n this._imperativeKeyBindings[eventString] = handlerName;\n this._prepKeyBindings();\n this._resetKeyEventListeners();\n },\n\n /**\n * When called, will remove all imperatively-added key bindings.\n */\n removeOwnKeyBindings: function() {\n this._imperativeKeyBindings = {};\n this._prepKeyBindings();\n this._resetKeyEventListeners();\n },\n\n /**\n * Returns true if a keyboard event matches `eventString`.\n *\n * @param {KeyboardEvent} event\n * @param {string} eventString\n * @return {boolean}\n */\n keyboardEventMatchesKeys: function(event, eventString) {\n var keyCombos = parseEventString(eventString);\n for (var i = 0; i < keyCombos.length; ++i) {\n if (keyComboMatchesEvent(keyCombos[i], event)) {\n return true;\n }\n }\n return false;\n },\n\n _collectKeyBindings: function() {\n var keyBindings = this.behaviors.map(function(behavior) {\n return behavior.keyBindings;\n });\n\n if (keyBindings.indexOf(this.keyBindings) === -1) {\n keyBindings.push(this.keyBindings);\n }\n\n return keyBindings;\n },\n\n _prepKeyBindings: function() {\n this._keyBindings = {};\n\n this._collectKeyBindings().forEach(function(keyBindings) {\n for (var eventString in keyBindings) {\n this._addKeyBinding(eventString, keyBindings[eventString]);\n }\n }, this);\n\n for (var eventString in this._imperativeKeyBindings) {\n this._addKeyBinding(\n eventString, this._imperativeKeyBindings[eventString]);\n }\n\n // Give precedence to combos with modifiers to be checked first.\n for (var eventName in this._keyBindings) {\n this._keyBindings[eventName].sort(function(kb1, kb2) {\n var b1 = kb1[0].hasModifiers;\n var b2 = kb2[0].hasModifiers;\n return (b1 === b2) ? 0 : b1 ? -1 : 1;\n })\n }\n },\n\n _addKeyBinding: function(eventString, handlerName) {\n parseEventString(eventString).forEach(function(keyCombo) {\n this._keyBindings[keyCombo.event] =\n this._keyBindings[keyCombo.event] || [];\n\n this._keyBindings[keyCombo.event].push([keyCombo, handlerName]);\n }, this);\n },\n\n _resetKeyEventListeners: function() {\n this._unlistenKeyEventListeners();\n\n if (this.isAttached) {\n this._listenKeyEventListeners();\n }\n },\n\n _listenKeyEventListeners: function() {\n if (!this.keyEventTarget) {\n return;\n }\n Object.keys(this._keyBindings).forEach(function(eventName) {\n var keyBindings = this._keyBindings[eventName];\n var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);\n\n this._boundKeyHandlers.push(\n [this.keyEventTarget, eventName, boundKeyHandler]);\n\n this.keyEventTarget.addEventListener(eventName, boundKeyHandler);\n }, this);\n },\n\n _unlistenKeyEventListeners: function() {\n var keyHandlerTuple;\n var keyEventTarget;\n var eventName;\n var boundKeyHandler;\n\n while (this._boundKeyHandlers.length) {\n // My kingdom for block-scope binding and destructuring assignment..\n keyHandlerTuple = this._boundKeyHandlers.pop();\n keyEventTarget = keyHandlerTuple[0];\n eventName = keyHandlerTuple[1];\n boundKeyHandler = keyHandlerTuple[2];\n\n keyEventTarget.removeEventListener(eventName, boundKeyHandler);\n }\n },\n\n _onKeyBindingEvent: function(keyBindings, event) {\n if (this.stopKeyboardEventPropagation) {\n event.stopPropagation();\n }\n\n // if event has been already prevented, don't do anything\n if (event.defaultPrevented) {\n return;\n }\n\n for (var i = 0; i < keyBindings.length; i++) {\n var keyCombo = keyBindings[i][0];\n var handlerName = keyBindings[i][1];\n if (keyComboMatchesEvent(keyCombo, event)) {\n this._triggerKeyHandler(keyCombo, handlerName, event);\n // exit the loop if eventDefault was prevented\n if (event.defaultPrevented) {\n return;\n }\n }\n }\n },\n\n _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {\n var detail = Object.create(keyCombo);\n detail.keyboardEvent = keyboardEvent;\n var event =\n new CustomEvent(keyCombo.event, {detail: detail, cancelable: true});\n this[handlerName].call(this, event);\n if (event.defaultPrevented) {\n keyboardEvent.preventDefault();\n }\n }\n};\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\n\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\nconst template = html`\n\n \n\n`;\ntemplate.setAttribute('style', 'display: none;');\ndocument.head.appendChild(template.content);\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\nimport '@polymer/iron-icon/iron-icon.js';\nimport '@polymer/paper-styles/default-theme.js';\n\nimport {PaperInkyFocusBehavior} from '@polymer/paper-behaviors/paper-inky-focus-behavior.js';\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\n\nconst template = html`\n\n \n\n`;\ntemplate.setAttribute('style', 'display: none;');\ndocument.body.appendChild(template.content);\n\n/**\nMaterial design: [Icon\ntoggles](https://www.google.com/design/spec/components/buttons.html#buttons-toggle-buttons)\n\n`paper-icon-button` is a button with an image placed at the center. When the\nuser touches the button, a ripple effect emanates from the center of the button.\n\n`paper-icon-button` does not include a default icon set. To use icons from the\ndefault set, include `PolymerElements/iron-icons/iron-icons.html`, and use the\n`icon` attribute to specify which icon from the icon set to use.\n\n \n\nSee [`iron-iconset`](iron-iconset) for more information about\nhow to use a custom icon set.\n\nExample:\n\n \n\n \n \n\nTo use `paper-icon-button` as a link, wrap it in an anchor tag. Since\n`paper-icon-button` will already receive focus, you may want to prevent the\nanchor tag from receiving focus as well by setting its tabindex to -1.\n\n \n \n \n\n### Styling\n\nStyle the button with CSS as you would a normal DOM element. If you are using\nthe icons provided by `iron-icons`, they will inherit the foreground color of\nthe button.\n\n /* make a red \"favorite\" button *\\/\n \n\nBy default, the ripple is the same color as the foreground at 25% opacity. You\nmay customize the color using the `--paper-icon-button-ink-color` custom\nproperty.\n\nThe following custom properties and mixins are available for styling:\n\nCustom property | Description | Default\n----------------|-------------|----------\n`--paper-icon-button-disabled-text` | The color of the disabled button | `--disabled-text-color`\n`--paper-icon-button-ink-color` | Selected/focus ripple color | `--primary-text-color`\n`--paper-icon-button` | Mixin for a button | `{}`\n`--paper-icon-button-disabled` | Mixin for a disabled button | `{}`\n`--paper-icon-button-hover` | Mixin for button on hover | `{}`\n\n@group Paper Elements\n@element paper-icon-button\n@demo demo/index.html\n*/\nPolymer({\n is: 'paper-icon-button',\n\n hostAttributes: {role: 'button', tabindex: '0'},\n\n behaviors: [PaperInkyFocusBehavior],\n\n properties: {\n /**\n * The URL of an image for the icon. If the src property is specified,\n * the icon property should not be.\n */\n src: {type: String},\n\n /**\n * Specifies the icon name or index in the set of icons available in\n * the icon's icon set. If the icon property is specified,\n * the src property should not be.\n */\n icon: {type: String},\n\n /**\n * Specifies the alternate text for the button, for accessibility.\n */\n alt: {type: String, observer: '_altChanged'}\n },\n\n _altChanged: function(newValue, oldValue) {\n var label = this.getAttribute('aria-label');\n\n // Don't stomp over a user-set aria-label.\n if (!label || oldValue == label) {\n this.setAttribute('aria-label', newValue);\n }\n }\n});\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/iron-flex-layout/iron-flex-layout.js';\n\nimport {IronMeta} from '@polymer/iron-meta/iron-meta.js';\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\nimport {Base} from '@polymer/polymer/polymer-legacy.js';\n\n/**\n\nThe `iron-icon` element displays an icon. By default an icon renders as a 24px\nsquare.\n\nExample using src:\n\n \n\nExample setting size to 32px x 32px:\n\n \n\n \n\nThe iron elements include several sets of icons. To use the default set of\nicons, import `iron-icons.js` and use the `icon` attribute to specify an icon:\n\n \n\n \n\nTo use a different built-in set of icons, import the specific\n`iron-icons/-icons.js`, and specify the icon as `:`.\nFor example, to use a communication icon, you would use:\n\n \n\n \n\nYou can also create custom icon sets of bitmap or SVG icons.\n\nExample of using an icon named `cherry` from a custom iconset with the ID\n`fruit`:\n\n \n\nSee `` and `` for more information about how to\ncreate a custom iconset.\n\nSee the `iron-icons` demo to see the icons available in the various iconsets.\n\n### Styling\n\nThe following custom properties are available for styling:\n\nCustom property | Description | Default\n----------------|-------------|----------\n`--iron-icon` | Mixin applied to the icon | {}\n`--iron-icon-width` | Width of the icon | `24px`\n`--iron-icon-height` | Height of the icon | `24px`\n`--iron-icon-fill-color` | Fill color of the svg icon | `currentcolor`\n`--iron-icon-stroke-color` | Stroke color of the svg icon | none\n\n@group Iron Elements\n@element iron-icon\n@demo demo/index.html\n@hero hero.svg\n@homepage polymer.github.io\n*/\nPolymer({\n _template: html`\n \n`,\n\n is: 'iron-icon',\n\n properties: {\n\n /**\n * The name of the icon to use. The name should be of the form:\n * `iconset_name:icon_name`.\n */\n icon: {type: String},\n\n /**\n * The name of the theme to used, if one is specified by the\n * iconset.\n */\n theme: {type: String},\n\n /**\n * If using iron-icon without an iconset, you can set the src to be\n * the URL of an individual icon image file. Note that this will take\n * precedence over a given icon attribute.\n */\n src: {type: String},\n\n /**\n * @type {!IronMeta}\n */\n _meta: {value: Base.create('iron-meta', {type: 'iconset'})}\n\n },\n\n observers: [\n '_updateIcon(_meta, isAttached)',\n '_updateIcon(theme, isAttached)',\n '_srcChanged(src, isAttached)',\n '_iconChanged(icon, isAttached)'\n ],\n\n _DEFAULT_ICONSET: 'icons',\n\n _iconChanged: function(icon) {\n var parts = (icon || '').split(':');\n this._iconName = parts.pop();\n this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;\n this._updateIcon();\n },\n\n _srcChanged: function(src) {\n this._updateIcon();\n },\n\n _usesIconset: function() {\n return this.icon || !this.src;\n },\n\n /** @suppress {visibility} */\n _updateIcon: function() {\n if (this._usesIconset()) {\n if (this._img && this._img.parentNode) {\n dom(this.root).removeChild(this._img);\n }\n if (this._iconName === '') {\n if (this._iconset) {\n this._iconset.removeIcon(this);\n }\n } else if (this._iconsetName && this._meta) {\n this._iconset = /** @type {?Polymer.Iconset} */ (\n this._meta.byKey(this._iconsetName));\n if (this._iconset) {\n this._iconset.applyIcon(this, this._iconName, this.theme);\n this.unlisten(window, 'iron-iconset-added', '_updateIcon');\n } else {\n this.listen(window, 'iron-iconset-added', '_updateIcon');\n }\n }\n } else {\n if (this._iconset) {\n this._iconset.removeIcon(this);\n }\n if (!this._img) {\n this._img = document.createElement('img');\n this._img.style.width = '100%';\n this._img.style.height = '100%';\n this._img.draggable = false;\n }\n this._img.src = this.src;\n dom(this.root).appendChild(this._img);\n }\n }\n});\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\nimport './iron-control-state.js';\n\nimport {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';\nimport {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';\n\n/**\n * @demo demo/index.html\n * @polymerBehavior IronButtonState\n */\nexport const IronButtonStateImpl = {\n\n properties: {\n\n /**\n * If true, the user is currently holding down the button.\n */\n pressed: {\n type: Boolean,\n readOnly: true,\n value: false,\n reflectToAttribute: true,\n observer: '_pressedChanged'\n },\n\n /**\n * If true, the button toggles the active state with each tap or press\n * of the spacebar.\n */\n toggles: {type: Boolean, value: false, reflectToAttribute: true},\n\n /**\n * If true, the button is a toggle and is currently in the active state.\n */\n active:\n {type: Boolean, value: false, notify: true, reflectToAttribute: true},\n\n /**\n * True if the element is currently being pressed by a \"pointer,\" which\n * is loosely defined as mouse or touch input (but specifically excluding\n * keyboard input).\n */\n pointerDown: {type: Boolean, readOnly: true, value: false},\n\n /**\n * True if the input device that caused the element to receive focus\n * was a keyboard.\n */\n receivedFocusFromKeyboard: {type: Boolean, readOnly: true},\n\n /**\n * The aria attribute to be set if the button is a toggle and in the\n * active state.\n */\n ariaActiveAttribute: {\n type: String,\n value: 'aria-pressed',\n observer: '_ariaActiveAttributeChanged'\n }\n },\n\n listeners: {down: '_downHandler', up: '_upHandler', tap: '_tapHandler'},\n\n observers:\n ['_focusChanged(focused)', '_activeChanged(active, ariaActiveAttribute)'],\n\n /**\n * @type {!Object}\n */\n keyBindings: {\n 'enter:keydown': '_asyncClick',\n 'space:keydown': '_spaceKeyDownHandler',\n 'space:keyup': '_spaceKeyUpHandler',\n },\n\n _mouseEventRe: /^mouse/,\n\n _tapHandler: function() {\n if (this.toggles) {\n // a tap is needed to toggle the active state\n this._userActivate(!this.active);\n } else {\n this.active = false;\n }\n },\n\n _focusChanged: function(focused) {\n this._detectKeyboardFocus(focused);\n\n if (!focused) {\n this._setPressed(false);\n }\n },\n\n _detectKeyboardFocus: function(focused) {\n this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);\n },\n\n // to emulate native checkbox, (de-)activations from a user interaction fire\n // 'change' events\n _userActivate: function(active) {\n if (this.active !== active) {\n this.active = active;\n this.fire('change');\n }\n },\n\n _downHandler: function(event) {\n this._setPointerDown(true);\n this._setPressed(true);\n this._setReceivedFocusFromKeyboard(false);\n },\n\n _upHandler: function() {\n this._setPointerDown(false);\n this._setPressed(false);\n },\n\n /**\n * @param {!KeyboardEvent} event .\n */\n _spaceKeyDownHandler: function(event) {\n var keyboardEvent = event.detail.keyboardEvent;\n var target = dom(keyboardEvent).localTarget;\n\n // Ignore the event if this is coming from a focused light child, since that\n // element will deal with it.\n if (this.isLightDescendant(/** @type {Node} */ (target)))\n return;\n\n keyboardEvent.preventDefault();\n keyboardEvent.stopImmediatePropagation();\n this._setPressed(true);\n },\n\n /**\n * @param {!KeyboardEvent} event .\n */\n _spaceKeyUpHandler: function(event) {\n var keyboardEvent = event.detail.keyboardEvent;\n var target = dom(keyboardEvent).localTarget;\n\n // Ignore the event if this is coming from a focused light child, since that\n // element will deal with it.\n if (this.isLightDescendant(/** @type {Node} */ (target)))\n return;\n\n if (this.pressed) {\n this._asyncClick();\n }\n this._setPressed(false);\n },\n\n // trigger click asynchronously, the asynchrony is useful to allow one\n // event handler to unwind before triggering another event\n _asyncClick: function() {\n this.async(function() {\n this.click();\n }, 1);\n },\n\n // any of these changes are considered a change to button state\n\n _pressedChanged: function(pressed) {\n this._changedButtonState();\n },\n\n _ariaActiveAttributeChanged: function(value, oldValue) {\n if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {\n this.removeAttribute(oldValue);\n }\n },\n\n _activeChanged: function(active, ariaActiveAttribute) {\n if (this.toggles) {\n this.setAttribute(this.ariaActiveAttribute, active ? 'true' : 'false');\n } else {\n this.removeAttribute(this.ariaActiveAttribute);\n }\n this._changedButtonState();\n },\n\n _controlStateChanged: function() {\n if (this.disabled) {\n this._setPressed(false);\n } else {\n this._changedButtonState();\n }\n },\n\n // provide hook for follow-on behaviors to react to button-state\n\n _changedButtonState: function() {\n if (this._buttonStateChanged) {\n this._buttonStateChanged(); // abstract\n }\n }\n\n};\n\n/** @polymerBehavior */\nexport const IronButtonState = [IronA11yKeysBehavior, IronButtonStateImpl];\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\n\nexport {}; // ensure this file can only be parsed as a module.\n\n// Give the user the choice to opt out of font loading.\nif (!window.polymerSkipLoadingFontRoboto) {\n const link = document.createElement('link');\n link.rel = 'stylesheet';\n link.type = 'text/css';\n link.crossOrigin = 'anonymous';\n link.href =\n 'https://fonts.googleapis.com/css?family=Roboto+Mono:400,700|Roboto:400,300,300italic,400italic,500,500italic,700,700italic';\n document.head.appendChild(link);\n}\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\n/*\nTypographic styles are provided matching the Material Design standard styles:\nhttp://www.google.com/design/spec/style/typography.html#typography-standard-styles\n\nNote that these are English/Latin centric styles. You may need to further adjust\nline heights and weights for CJK typesetting. See the notes in the Material\nDesign typography section.\n*/\n\nimport '@polymer/polymer/polymer-legacy.js';\nimport '@polymer/font-roboto/roboto.js';\n\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\nconst template = html`\n \n`;\ntemplate.setAttribute('style', 'display: none;');\ndocument.head.appendChild(template.content);\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport '@polymer/iron-flex-layout/iron-flex-layout.js';\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\n\n/**\napp-toolbar is a horizontal toolbar containing items that can be used for\nlabel, navigation, search and actions.\n\n### Example\n\nAdd a title to the toolbar.\n\n```html\n\n
App name
\n
\n```\n\nAdd a button to the left and right side of the toolbar.\n\n```html\n\n \n
App name
\n \n
\n```\n\nYou can use the attributes `top-item` or `bottom-item` to completely fit an\nelement to the top or bottom of the toolbar respectively.\n\n### Content attributes\n\nAttribute | Description\n---------------------|---------------------------------------------------------\n`main-title` | The main title element.\n`condensed-title` | The title element if used inside a condensed app-header.\n`spacer` | Adds a left margin of `64px`.\n`bottom-item` | Sticks the element to the bottom of the toolbar.\n`top-item` | Sticks the element to the top of the toolbar.\n\n### Styling\n\nCustom property | Description | Default\n-----------------------------|------------------------------|-----------------------\n`--app-toolbar-font-size` | Toolbar font size | 20px\n\n@group App Elements\n@element app-toolbar\n@demo app-toolbar/demo/index.html\n*/\nPolymer({\n _template: html`\n \n\n \n`,\n\n is: 'app-toolbar'\n});\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\nimport '@polymer/paper-ripple/paper-ripple.js';\n\nimport {IronButtonStateImpl} from '@polymer/iron-behaviors/iron-button-state.js';\nimport {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';\n\n/**\n * `PaperRippleBehavior` dynamically implements a ripple when the element has\n * focus via pointer or keyboard.\n *\n * NOTE: This behavior is intended to be used in conjunction with and after\n * `IronButtonState` and `IronControlState`.\n *\n * @polymerBehavior PaperRippleBehavior\n */\nexport const PaperRippleBehavior = {\n properties: {\n /**\n * If true, the element will not produce a ripple effect when interacted\n * with via the pointer.\n */\n noink: {type: Boolean, observer: '_noinkChanged'},\n\n /**\n * @type {Element|undefined}\n */\n _rippleContainer: {\n type: Object,\n }\n },\n\n /**\n * Ensures a `` element is available when the element is\n * focused.\n */\n _buttonStateChanged: function() {\n if (this.focused) {\n this.ensureRipple();\n }\n },\n\n /**\n * In addition to the functionality provided in `IronButtonState`, ensures\n * a ripple effect is created when the element is in a `pressed` state.\n */\n _downHandler: function(event) {\n IronButtonStateImpl._downHandler.call(this, event);\n if (this.pressed) {\n this.ensureRipple(event);\n }\n },\n\n /**\n * Ensures this element contains a ripple effect. For startup efficiency\n * the ripple effect is dynamically on demand when needed.\n * @param {!Event=} optTriggeringEvent (optional) event that triggered the\n * ripple.\n */\n ensureRipple: function(optTriggeringEvent) {\n if (!this.hasRipple()) {\n this._ripple = this._createRipple();\n this._ripple.noink = this.noink;\n var rippleContainer = this._rippleContainer || this.root;\n if (rippleContainer) {\n dom(rippleContainer).appendChild(this._ripple);\n }\n if (optTriggeringEvent) {\n // Check if the event happened inside of the ripple container\n // Fall back to host instead of the root because distributed text\n // nodes are not valid event targets\n var domContainer = dom(this._rippleContainer || this);\n var target = dom(optTriggeringEvent).rootTarget;\n if (domContainer.deepContains(/** @type {Node} */ (target))) {\n this._ripple.uiDownAction(optTriggeringEvent);\n }\n }\n }\n },\n\n /**\n * Returns the `` element used by this element to create\n * ripple effects. The element's ripple is created on demand, when\n * necessary, and calling this method will force the\n * ripple to be created.\n */\n getRipple: function() {\n this.ensureRipple();\n return this._ripple;\n },\n\n /**\n * Returns true if this element currently contains a ripple effect.\n * @return {boolean}\n */\n hasRipple: function() {\n return Boolean(this._ripple);\n },\n\n /**\n * Create the element's ripple effect via creating a ``.\n * Override this method to customize the ripple element.\n * @return {!PaperRippleElement} Returns a `` element.\n */\n _createRipple: function() {\n var element = /** @type {!PaperRippleElement} */ (\n document.createElement('paper-ripple'));\n return element;\n },\n\n _noinkChanged: function(noink) {\n if (this.hasRipple()) {\n this._ripple.noink = noink;\n }\n }\n};\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\n\n/**\n`iron-a11y-announcer` is a singleton element that is intended to add a11y\nto features that require on-demand announcement from screen readers. In\norder to make use of the announcer, it is best to request its availability\nin the announcing element.\n\nExample:\n\n Polymer({\n\n is: 'x-chatty',\n\n attached: function() {\n // This will create the singleton element if it has not\n // been created yet:\n Polymer.IronA11yAnnouncer.requestAvailability();\n }\n });\n\nAfter the `iron-a11y-announcer` has been made available, elements can\nmake announces by firing bubbling `iron-announce` events.\n\nExample:\n\n this.fire('iron-announce', {\n text: 'This is an announcement!'\n }, { bubbles: true });\n\nNote: announcements are only audible if you have a screen reader enabled.\n\n@group Iron Elements\n@demo demo/index.html\n*/\nexport const IronA11yAnnouncer = Polymer({\n _template: html`\n \n
[[_text]]
\n`,\n\n is: 'iron-a11y-announcer',\n\n properties: {\n\n /**\n * The value of mode is used to set the `aria-live` attribute\n * for the element that will be announced. Valid values are: `off`,\n * `polite` and `assertive`.\n */\n mode: {type: String, value: 'polite'},\n\n _text: {type: String, value: ''}\n },\n\n created: function() {\n if (!IronA11yAnnouncer.instance) {\n IronA11yAnnouncer.instance = this;\n }\n\n document.body.addEventListener(\n 'iron-announce', this._onIronAnnounce.bind(this));\n },\n\n /**\n * Cause a text string to be announced by screen readers.\n *\n * @param {string} text The text that should be announced.\n */\n announce: function(text) {\n this._text = '';\n this.async(function() {\n this._text = text;\n }, 100);\n },\n\n _onIronAnnounce: function(event) {\n if (event.detail && event.detail.text) {\n this.announce(event.detail.text);\n }\n }\n});\n\nIronA11yAnnouncer.instance = null;\n\nIronA11yAnnouncer.requestAvailability = function() {\n if (!IronA11yAnnouncer.instance) {\n IronA11yAnnouncer.instance = document.createElement('iron-a11y-announcer');\n }\n\n document.body.appendChild(IronA11yAnnouncer.instance);\n};\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\nimport {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer.js';\nimport {IronValidatableBehavior} from '@polymer/iron-validatable-behavior/iron-validatable-behavior.js';\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\n\n/**\n`` is a wrapper to a native `` element, that adds two-way\nbinding and prevention of invalid input. To use it, you must distribute a native\n`` yourself. You can continue to use the native `input` as you would\nnormally:\n\n \n \n \n\n \n \n \n\n### Two-way binding\n\nBy default you can only get notified of changes to a native ``'s `value`\ndue to user input:\n\n \n\nThis means that if you imperatively set the value (i.e. `someNativeInput.value =\n'foo'`), no events will be fired and this change cannot be observed.\n\n`iron-input` adds the `bind-value` property that mirrors the native `input`'s\n'`value` property; this property can be used for two-way data binding.\n`bind-value` will notify if it is changed either by user input or by script.\n\n \n \n \n\nNote: this means that if you want to imperatively set the native `input`'s, you\n_must_ set `bind-value` instead, so that the wrapper `iron-input` can be\nnotified.\n\n### Validation\n\n`iron-input` uses the native `input`'s validation. For simplicity, `iron-input`\nhas a `validate()` method (which internally just checks the distributed\n`input`'s validity), which sets an `invalid` attribute that can also be used for\nstyling.\n\nTo validate automatically as you type, you can use the `auto-validate`\nattribute.\n\n`iron-input` also fires an `iron-input-validate` event after `validate()` is\ncalled. You can use it to implement a custom validator:\n\n var CatsOnlyValidator = {\n validate: function(ironInput) {\n var valid = !ironInput.bindValue || ironInput.bindValue === 'cat';\n ironInput.invalid = !valid;\n return valid;\n }\n }\n ironInput.addEventListener('iron-input-validate', function() {\n CatsOnly.validate(input2);\n });\n\nYou can also use an element implementing an\n[`IronValidatorBehavior`](/element/PolymerElements/iron-validatable-behavior).\nThis example can also be found in the demo for this element:\n\n \n \n \n\n### Preventing invalid input\n\nIt may be desirable to only allow users to enter certain characters. You can use\nthe `allowed-pattern` attribute to accomplish this. This feature is separate\nfrom validation, and `allowed-pattern` does not affect how the input is\nvalidated.\n\n // Only allow typing digits, but a valid input has exactly 5 digits.\n \n \n \n\n@demo demo/index.html\n*/\nPolymer({\n _template: html`\n \n \n`,\n\n is: 'iron-input',\n behaviors: [IronValidatableBehavior],\n\n /**\n * Fired whenever `validate()` is called.\n *\n * @event iron-input-validate\n */\n\n properties: {\n\n /**\n * Use this property instead of `value` for two-way data binding, or to\n * set a default value for the input. **Do not** use the distributed\n * input's `value` property to set a default value.\n */\n bindValue: {type: String, value: ''},\n\n /**\n * Computed property that echoes `bindValue` (mostly used for Polymer 1.0\n * backcompatibility, if you were one-way binding to the Polymer 1.0\n * `input is=\"iron-input\"` value attribute).\n */\n value: {type: String, computed: '_computeValue(bindValue)'},\n\n /**\n * Regex-like list of characters allowed as input; all characters not in the\n * list will be rejected. The recommended format should be a list of allowed\n * characters, for example, `[a-zA-Z0-9.+-!;:]`.\n *\n * This pattern represents the allowed characters for the field; as the user\n * inputs text, each individual character will be checked against the\n * pattern (rather than checking the entire value as a whole). If a\n * character is not a match, it will be rejected.\n *\n * Pasted input will have each character checked individually; if any\n * character doesn't match `allowedPattern`, the entire pasted string will\n * be rejected.\n *\n * Note: if you were using `iron-input` in 1.0, you were also required to\n * set `prevent-invalid-input`. This is no longer needed as of Polymer 2.0,\n * and will be set automatically for you if an `allowedPattern` is provided.\n *\n */\n allowedPattern: {type: String},\n\n /**\n * Set to true to auto-validate the input value as you type.\n */\n autoValidate: {type: Boolean, value: false},\n\n /**\n * The native input element.\n */\n _inputElement: Object,\n },\n\n observers: ['_bindValueChanged(bindValue, _inputElement)'],\n listeners: {'input': '_onInput', 'keypress': '_onKeypress'},\n\n created: function() {\n IronA11yAnnouncer.requestAvailability();\n this._previousValidInput = '';\n this._patternAlreadyChecked = false;\n },\n\n attached: function() {\n // If the input is added at a later time, update the internal reference.\n this._observer = dom(this).observeNodes(function(info) {\n this._initSlottedInput();\n }.bind(this));\n },\n\n detached: function() {\n if (this._observer) {\n dom(this).unobserveNodes(this._observer);\n this._observer = null;\n }\n },\n\n /**\n * Returns the distributed input element.\n */\n get inputElement() {\n return this._inputElement;\n },\n\n _initSlottedInput: function() {\n this._inputElement = this.getEffectiveChildren()[0];\n\n if (this.inputElement && this.inputElement.value) {\n this.bindValue = this.inputElement.value;\n }\n\n this.fire('iron-input-ready');\n },\n\n get _patternRegExp() {\n var pattern;\n if (this.allowedPattern) {\n pattern = new RegExp(this.allowedPattern);\n } else {\n switch (this.inputElement.type) {\n case 'number':\n pattern = /[0-9.,e-]/;\n break;\n }\n }\n return pattern;\n },\n\n /**\n * @suppress {checkTypes}\n */\n _bindValueChanged: function(bindValue, inputElement) {\n // The observer could have run before attached() when we have actually\n // initialized this property.\n if (!inputElement) {\n return;\n }\n\n if (bindValue === undefined) {\n inputElement.value = null;\n } else if (bindValue !== inputElement.value) {\n this.inputElement.value = bindValue;\n }\n\n if (this.autoValidate) {\n this.validate();\n }\n\n // manually notify because we don't want to notify until after setting value\n this.fire('bind-value-changed', {value: bindValue});\n },\n\n _onInput: function() {\n // Need to validate each of the characters pasted if they haven't\n // been validated inside `_onKeypress` already.\n if (this.allowedPattern && !this._patternAlreadyChecked) {\n var valid = this._checkPatternValidity();\n if (!valid) {\n this._announceInvalidCharacter(\n 'Invalid string of characters not entered.');\n this.inputElement.value = this._previousValidInput;\n }\n }\n this.bindValue = this._previousValidInput = this.inputElement.value;\n this._patternAlreadyChecked = false;\n },\n\n _isPrintable: function(event) {\n // What a control/printable character is varies wildly based on the browser.\n // - most control characters (arrows, backspace) do not send a `keypress`\n // event\n // in Chrome, but the *do* on Firefox\n // - in Firefox, when they do send a `keypress` event, control chars have\n // a charCode = 0, keyCode = xx (for ex. 40 for down arrow)\n // - printable characters always send a keypress event.\n // - in Firefox, printable chars always have a keyCode = 0. In Chrome, the\n // keyCode\n // always matches the charCode.\n // None of this makes any sense.\n\n // For these keys, ASCII code == browser keycode.\n var anyNonPrintable = (event.keyCode == 8) || // backspace\n (event.keyCode == 9) || // tab\n (event.keyCode == 13) || // enter\n (event.keyCode == 27); // escape\n\n // For these keys, make sure it's a browser keycode and not an ASCII code.\n var mozNonPrintable = (event.keyCode == 19) || // pause\n (event.keyCode == 20) || // caps lock\n (event.keyCode == 45) || // insert\n (event.keyCode == 46) || // delete\n (event.keyCode == 144) || // num lock\n (event.keyCode == 145) || // scroll lock\n (event.keyCode > 32 &&\n event.keyCode < 41) || // page up/down, end, home, arrows\n (event.keyCode > 111 && event.keyCode < 124); // fn keys\n\n return !anyNonPrintable && !(event.charCode == 0 && mozNonPrintable);\n },\n\n _onKeypress: function(event) {\n if (!this.allowedPattern && this.inputElement.type !== 'number') {\n return;\n }\n var regexp = this._patternRegExp;\n if (!regexp) {\n return;\n }\n\n // Handle special keys and backspace\n if (event.metaKey || event.ctrlKey || event.altKey) {\n return;\n }\n\n // Check the pattern either here or in `_onInput`, but not in both.\n this._patternAlreadyChecked = true;\n\n var thisChar = String.fromCharCode(event.charCode);\n if (this._isPrintable(event) && !regexp.test(thisChar)) {\n event.preventDefault();\n this._announceInvalidCharacter(\n 'Invalid character ' + thisChar + ' not entered.');\n }\n },\n\n _checkPatternValidity: function() {\n var regexp = this._patternRegExp;\n if (!regexp) {\n return true;\n }\n for (var i = 0; i < this.inputElement.value.length; i++) {\n if (!regexp.test(this.inputElement.value[i])) {\n return false;\n }\n }\n return true;\n },\n\n /**\n * Returns true if `value` is valid. The validator provided in `validator`\n * will be used first, then any constraints.\n * @return {boolean} True if the value is valid.\n */\n validate: function() {\n if (!this.inputElement) {\n this.invalid = false;\n return true;\n }\n\n // Use the nested input's native validity.\n var valid = this.inputElement.checkValidity();\n\n // Only do extra checking if the browser thought this was valid.\n if (valid) {\n // Empty, required input is invalid\n if (this.required && this.bindValue === '') {\n valid = false;\n } else if (this.hasValidator()) {\n valid = IronValidatableBehavior.validate.call(this, this.bindValue);\n }\n }\n\n this.invalid = !valid;\n this.fire('iron-input-validate');\n return valid;\n },\n\n _announceInvalidCharacter: function(message) {\n this.fire('iron-announce', {text: message});\n },\n\n _computeValue: function(bindValue) {\n return bindValue;\n }\n});\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\n\n/**\n * Use `Polymer.PaperInputAddonBehavior` to implement an add-on for\n * ``. A add-on appears below the input, and may display\n * information based on the input value and validity such as a character counter\n * or an error message.\n * @polymerBehavior\n */\nexport const PaperInputAddonBehavior = {\n attached: function() {\n this.fire('addon-attached');\n },\n\n /**\n * The function called by `` when the input value or\n * validity changes.\n * @param {{\n * invalid: boolean,\n * inputElement: (Element|undefined),\n * value: (string|undefined)\n * }} state -\n * inputElement: The input element.\n * value: The input value.\n * invalid: True if the input value is invalid.\n */\n update: function(state) {}\n\n};\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\nimport '@polymer/paper-styles/typography.js';\n\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\n\nimport {PaperInputAddonBehavior} from './paper-input-addon-behavior.js';\n\n/*\n`` is a character counter for use with\n``. It shows the number of characters entered in the\ninput and the max length if it is specified.\n\n \n \n \n \n\n### Styling\n\nThe following mixin is available for styling:\n\nCustom property | Description | Default\n----------------|-------------|----------\n`--paper-input-char-counter` | Mixin applied to the element | `{}`\n*/\nPolymer({\n _template: html`\n \n\n [[_charCounterStr]]\n`,\n\n is: 'paper-input-char-counter',\n behaviors: [PaperInputAddonBehavior],\n properties: {_charCounterStr: {type: String, value: '0'}},\n\n /**\n * This overrides the update function in PaperInputAddonBehavior.\n * @param {{\n * inputElement: (Element|undefined),\n * value: (string|undefined),\n * invalid: boolean\n * }} state -\n * inputElement: The input element.\n * value: The input value.\n * invalid: True if the input value is invalid.\n */\n update: function(state) {\n if (!state.inputElement) {\n return;\n }\n\n state.value = state.value || '';\n\n var counter = state.value.toString().length.toString();\n\n if (state.inputElement.hasAttribute('maxlength')) {\n counter += '/' + state.inputElement.getAttribute('maxlength');\n }\n\n this._charCounterStr = counter;\n }\n});\n","/**\n@license\nCopyright (c) 2015 The Polymer Project Authors. All rights reserved.\nThis code may only be used under the BSD style license found at\nhttp://polymer.github.io/LICENSE.txt The complete set of authors may be found at\nhttp://polymer.github.io/AUTHORS.txt The complete set of contributors may be\nfound at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as\npart of the polymer project is also subject to an additional IP rights grant\nfound at http://polymer.github.io/PATENTS.txt\n*/\nimport '@polymer/polymer/polymer-legacy.js';\nimport '@polymer/iron-flex-layout/iron-flex-layout.js';\nimport '@polymer/paper-styles/default-theme.js';\nimport '@polymer/paper-styles/typography.js';\n\nimport {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';\nimport {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';\nimport {dashToCamelCase} from '@polymer/polymer/lib/utils/case-map.js';\nimport {html} from '@polymer/polymer/lib/utils/html-tag.js';\nconst template = html`\n\n \n\n`;\ntemplate.setAttribute('style', 'display: none;');\ndocument.head.appendChild(template.content);\n\n/*\n`` is a container for a `