Merge pull request #1979 from home-assistant/dev

Release 236
This commit is contained in:
Pascal Vizeli 2020-08-27 10:52:18 +02:00 committed by GitHub
commit cea6e7a9f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
223 changed files with 8364 additions and 6078 deletions

1
.github/stale.yml vendored
View File

@ -6,6 +6,7 @@ daysUntilClose: 7
exemptLabels: exemptLabels:
- pinned - pinned
- security - security
- rfc
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: stale staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable

View File

@ -12,7 +12,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Sentry Release - name: Sentry Release
uses: getsentry/action-release@v1.0.0 uses: getsentry/action-release@v1.0.1
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 19.10b0 rev: 20.8b1
hooks: hooks:
- id: black - id: black
args: args:

65
API.md
View File

@ -442,6 +442,65 @@ Proxy to Home Assistant Core websocket.
} }
``` ```
### Network
Network operations over the API
#### GET `/network/info`
Get network information
```json
{
"interfaces": {
"enp0s31f6": {
"ip_address": "192.168.2.148/24",
"gateway": "192.168.2.1",
"id": "Wired connection 1",
"type": "802-3-ethernet",
"nameservers": ["192.168.2.1"],
"method": "static",
"primary": true
}
}
}
```
#### GET `/network/{interface}/info`
Get information for a single interface
```json
{
"ip_address": "192.168.2.148/24",
"gateway": "192.168.2.1",
"id": "Wired connection 1",
"type": "802-3-ethernet",
"nameservers": ["192.168.2.1"],
"method": "dhcp",
"primary": true
}
```
#### POST `/network/{interface}/update`
Update information for a single interface
**Options:**
| Option | Description |
| --------- | ---------------------------------------------------------------------- |
| `address` | The new IP address for the interface in the X.X.X.X/XX format |
| `dns` | Comma seperated list of DNS servers to use |
| `gateway` | The gateway the interface should use |
| `method` | Set if the interface should use DHCP or not, can be `dhcp` or `static` |
_All options are optional._
**NB!: If you change the `address` or `gateway` you may need to reconnect to the new address**
The result will be a updated object.
### RESTful for API add-ons ### RESTful for API add-ons
If an add-on will call itself, you can use `/addons/self/...`. If an add-on will call itself, you can use `/addons/self/...`.
@ -550,7 +609,8 @@ Get all available add-ons.
"ingress_entry": "null|/api/hassio_ingress/slug", "ingress_entry": "null|/api/hassio_ingress/slug",
"ingress_url": "null|/api/hassio_ingress/slug/entry.html", "ingress_url": "null|/api/hassio_ingress/slug/entry.html",
"ingress_port": "null|int", "ingress_port": "null|int",
"ingress_panel": "null|bool" "ingress_panel": "null|bool",
"watchdog": "null|bool"
} }
``` ```
@ -570,7 +630,8 @@ Get all available add-ons.
"options": {}, "options": {},
"audio_output": "null|0,0", "audio_output": "null|0,0",
"audio_input": "null|0,0", "audio_input": "null|0,0",
"ingress_panel": "bool" "ingress_panel": "bool",
"watchdog": "bool"
} }
``` ```

View File

@ -14,8 +14,7 @@ RUN \
libffi \ libffi \
libpulse \ libpulse \
musl \ musl \
openssl \ openssl
socat
ARG BUILD_ARCH ARG BUILD_ARCH
WORKDIR /usr/src WORKDIR /usr/src

@ -1 +1 @@
Subproject commit 77b25f5132820c0596ccae82dd501ce67f101e72 Subproject commit c1a4b27bc7fc68dfb4af6b382238c010340e7912

View File

@ -1,12 +1,12 @@
aiohttp==3.6.2 aiohttp==3.6.2
async_timeout==3.0.1 async_timeout==3.0.1
attrs==19.3.0 attrs==20.1.0
cchardet==2.1.6 cchardet==2.1.6
colorlog==4.2.1 colorlog==4.2.1
cpe==1.2.1 cpe==1.2.1
cryptography==3.0 cryptography==3.1
debugpy==1.0.0rc1 debugpy==1.0.0rc2
docker==4.3.0 docker==4.3.1
gitpython==3.1.7 gitpython==3.1.7
jinja2==2.11.2 jinja2==2.11.2
packaging==20.4 packaging==20.4
@ -14,6 +14,6 @@ pulsectl==20.5.1
pytz==2020.1 pytz==2020.1
pyudev==0.22.0 pyudev==0.22.0
ruamel.yaml==0.15.100 ruamel.yaml==0.15.100
sentry-sdk==0.16.5 sentry-sdk==0.17.0
uvloop==0.14.0 uvloop==0.14.0
voluptuous==0.11.7 voluptuous==0.11.7

View File

@ -1,12 +1,13 @@
black==19.10b0 black==20.8b1
codecov==2.1.8 codecov==2.1.9
coverage==5.2.1 coverage==5.2.1
flake8-docstrings==1.5.0 flake8-docstrings==1.5.0
flake8==3.8.3 flake8==3.8.3
pre-commit==2.6.0 pre-commit==2.7.1
pydocstyle==5.0.2 pydocstyle==5.1.0
pylint==2.5.3 pylint==2.6.0
pytest-aiohttp==0.3.0 pytest-aiohttp==0.3.0
pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16)
pytest-cov==2.10.1 pytest-cov==2.10.1
pytest-timeout==1.4.2 pytest-timeout==1.4.2
pytest==6.0.1 pytest==6.0.1

View File

@ -35,10 +35,18 @@ setup(
"supervisor.docker", "supervisor.docker",
"supervisor.addons", "supervisor.addons",
"supervisor.api", "supervisor.api",
"supervisor.dbus",
"supervisor.discovery",
"supervisor.discovery.services",
"supervisor.services",
"supervisor.services.modules",
"supervisor.homeassistant",
"supervisor.host",
"supervisor.misc", "supervisor.misc",
"supervisor.utils", "supervisor.utils",
"supervisor.plugins", "supervisor.plugins",
"supervisor.snapshots", "supervisor.snapshots",
"supervisor.store",
], ],
include_package_data=True, include_package_data=True,
) )

View File

@ -5,7 +5,7 @@ import logging
import tarfile import tarfile
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from ..const import BOOT_AUTO, STATE_STARTED, AddonStartup from ..const import BOOT_AUTO, AddonStartup, AddonState
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ( from ..exceptions import (
AddonsError, AddonsError,
@ -108,7 +108,7 @@ class AddonManager(CoreSysAttributes):
"""Shutdown addons.""" """Shutdown addons."""
tasks: List[Addon] = [] tasks: List[Addon] = []
for addon in self.installed: for addon in self.installed:
if await addon.state() != STATE_STARTED or addon.startup != stage: if addon.state != AddonState.STARTED or addon.startup != stage:
continue continue
tasks.append(addon) tasks.append(addon)
@ -153,9 +153,9 @@ class AddonManager(CoreSysAttributes):
try: try:
await addon.instance.install(store.version, store.image) await addon.instance.install(store.version, store.image)
except DockerAPIError: except DockerAPIError as err:
self.data.uninstall(addon) self.data.uninstall(addon)
raise AddonsError() raise AddonsError() from err
else: else:
self.local[slug] = addon self.local[slug] = addon
@ -174,8 +174,10 @@ class AddonManager(CoreSysAttributes):
try: try:
await addon.instance.remove() await addon.instance.remove()
except DockerAPIError: except DockerAPIError as err:
raise AddonsError() raise AddonsError() from err
else:
addon.state = AddonState.UNKNOWN
await addon.remove_data() await addon.remove_data()
@ -238,15 +240,15 @@ class AddonManager(CoreSysAttributes):
raise AddonsNotSupportedError() raise AddonsNotSupportedError()
# Update instance # Update instance
last_state = await addon.state() last_state: AddonState = addon.state
try: try:
await addon.instance.update(store.version, store.image) await addon.instance.update(store.version, store.image)
# Cleanup # Cleanup
with suppress(DockerAPIError): with suppress(DockerAPIError):
await addon.instance.cleanup() await addon.instance.cleanup()
except DockerAPIError: except DockerAPIError as err:
raise AddonsError() raise AddonsError() from err
else: else:
self.data.update(store) self.data.update(store)
_LOGGER.info("Add-on '%s' successfully updated", slug) _LOGGER.info("Add-on '%s' successfully updated", slug)
@ -255,7 +257,7 @@ class AddonManager(CoreSysAttributes):
await addon.install_apparmor() await addon.install_apparmor()
# restore state # restore state
if last_state == STATE_STARTED: if last_state == AddonState.STARTED:
await addon.start() await addon.start()
async def rebuild(self, slug: str) -> None: async def rebuild(self, slug: str) -> None:
@ -279,18 +281,18 @@ class AddonManager(CoreSysAttributes):
raise AddonsNotSupportedError() raise AddonsNotSupportedError()
# remove docker container but not addon config # remove docker container but not addon config
last_state = await addon.state() last_state: AddonState = addon.state
try: try:
await addon.instance.remove() await addon.instance.remove()
await addon.instance.install(addon.version) await addon.instance.install(addon.version)
except DockerAPIError: except DockerAPIError as err:
raise AddonsError() raise AddonsError() from err
else: else:
self.data.update(store) self.data.update(store)
_LOGGER.info("Add-on '%s' successfully rebuilt", slug) _LOGGER.info("Add-on '%s' successfully rebuilt", slug)
# restore state # restore state
if last_state == STATE_STARTED: if last_state == AddonState.STARTED:
await addon.start() await addon.start()
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None: async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:

View File

@ -1,4 +1,5 @@
"""Init file for Supervisor add-ons.""" """Init file for Supervisor add-ons."""
import asyncio
from contextlib import suppress from contextlib import suppress
from copy import deepcopy from copy import deepcopy
from ipaddress import IPv4Address from ipaddress import IPv4Address
@ -11,6 +12,7 @@ import tarfile
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any, Awaitable, Dict, List, Optional from typing import Any, Awaitable, Dict, List, Optional
import aiohttp
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
@ -35,9 +37,9 @@ from ..const import (
ATTR_USER, ATTR_USER,
ATTR_UUID, ATTR_UUID,
ATTR_VERSION, ATTR_VERSION,
ATTR_WATCHDOG,
DNS_SUFFIX, DNS_SUFFIX,
STATE_STARTED, AddonState,
STATE_STOPPED,
) )
from ..coresys import CoreSys from ..coresys import CoreSys
from ..docker.addon import DockerAddon from ..docker.addon import DockerAddon
@ -50,6 +52,7 @@ from ..exceptions import (
HostAppArmorError, HostAppArmorError,
JsonFileError, JsonFileError,
) )
from ..utils import check_port
from ..utils.apparmor import adjust_profile from ..utils.apparmor import adjust_profile
from ..utils.json import read_json_file, write_json_file from ..utils.json import read_json_file, write_json_file
from ..utils.tar import atomic_contents_add, secure_path from ..utils.tar import atomic_contents_add, secure_path
@ -64,8 +67,15 @@ RE_WEBUI = re.compile(
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$" r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
) )
RE_WATCHDOG = re.compile(
r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])"
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
)
RE_OLD_AUDIO = re.compile(r"\d+,\d+") RE_OLD_AUDIO = re.compile(r"\d+,\d+")
WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10)
class Addon(AddonModel): class Addon(AddonModel):
"""Hold data for add-on inside Supervisor.""" """Hold data for add-on inside Supervisor."""
@ -74,12 +84,24 @@ class Addon(AddonModel):
"""Initialize data holder.""" """Initialize data holder."""
super().__init__(coresys, slug) super().__init__(coresys, slug)
self.instance: DockerAddon = DockerAddon(coresys, self) self.instance: DockerAddon = DockerAddon(coresys, self)
self.state: AddonState = AddonState.UNKNOWN
@property
def in_progress(self) -> bool:
"""Return True if a task is in progress."""
return self.instance.in_progress
async def load(self) -> None: async def load(self) -> None:
"""Async initialize of object.""" """Async initialize of object."""
with suppress(DockerAPIError): with suppress(DockerAPIError):
await self.instance.attach(tag=self.version) await self.instance.attach(tag=self.version)
# Evaluate state
if await self.instance.is_running():
self.state = AddonState.STARTED
else:
self.state = AddonState.STOPPED
@property @property
def ip_address(self) -> IPv4Address: def ip_address(self) -> IPv4Address:
"""Return IP of add-on instance.""" """Return IP of add-on instance."""
@ -155,6 +177,16 @@ class Addon(AddonModel):
"""Set auto update.""" """Set auto update."""
self.persist[ATTR_AUTO_UPDATE] = value self.persist[ATTR_AUTO_UPDATE] = value
@property
def watchdog(self) -> bool:
"""Return True if watchdog is enable."""
return self.persist[ATTR_WATCHDOG]
@watchdog.setter
def watchdog(self, value: bool) -> None:
"""Set watchdog enable/disable."""
self.persist[ATTR_WATCHDOG] = value
@property @property
def uuid(self) -> str: def uuid(self) -> str:
"""Return an API token for this add-on.""" """Return an API token for this add-on."""
@ -230,8 +262,6 @@ class Addon(AddonModel):
if not url: if not url:
return None return None
webui = RE_WEBUI.match(url) webui = RE_WEBUI.match(url)
if not webui:
return None
# extract arguments # extract arguments
t_port = webui.group("t_port") t_port = webui.group("t_port")
@ -245,10 +275,6 @@ class Addon(AddonModel):
else: else:
port = self.ports.get(f"{t_port}/tcp", t_port) port = self.ports.get(f"{t_port}/tcp", t_port)
# for interface config or port lists
if isinstance(port, (tuple, list)):
port = port[-1]
# lookup the correct protocol from config # lookup the correct protocol from config
if t_proto: if t_proto:
proto = "https" if self.options.get(t_proto) else "http" proto = "https" if self.options.get(t_proto) else "http"
@ -353,13 +379,55 @@ class Addon(AddonModel):
"""Save data of add-on.""" """Save data of add-on."""
self.sys_addons.data.save_data() self.sys_addons.data.save_data()
async def watchdog_application(self) -> bool:
"""Return True if application is running."""
url = super().watchdog
if not url:
return True
application = RE_WATCHDOG.match(url)
# extract arguments
t_port = application.group("t_port")
t_proto = application.group("t_proto")
s_prefix = application.group("s_prefix") or ""
s_suffix = application.group("s_suffix") or ""
# search host port for this docker port
if self.host_network:
port = self.ports.get(f"{t_port}/tcp", t_port)
else:
port = t_port
# TCP monitoring
if s_prefix == "tcp":
return await self.sys_run_in_executor(check_port, self.ip_address, port)
# lookup the correct protocol from config
if t_proto:
proto = "https" if self.options.get(t_proto) else "http"
else:
proto = s_prefix
# Make HTTP request
try:
url = f"{proto}://{self.ip_address}:{port}{s_suffix}"
async with self.sys_websession_ssl.get(
url, timeout=WATCHDOG_TIMEOUT
) as req:
if req.status < 300:
return True
except (asyncio.TimeoutError, aiohttp.ClientError):
pass
return False
async def write_options(self) -> None: async def write_options(self) -> None:
"""Return True if add-on options is written to data.""" """Return True if add-on options is written to data."""
schema = self.schema schema = self.schema
options = self.options options = self.options
# Update secrets for validation # Update secrets for validation
await self.sys_secrets.reload() await self.sys_homeassistant.secrets.reload()
try: try:
options = schema(options) options = schema(options)
@ -462,12 +530,6 @@ class Addon(AddonModel):
return False return False
return True return True
async def state(self) -> str:
"""Return running state of add-on."""
if await self.instance.is_running():
return STATE_STARTED
return STATE_STOPPED
async def start(self) -> None: async def start(self) -> None:
"""Set options and start add-on.""" """Set options and start add-on."""
if await self.instance.is_running(): if await self.instance.is_running():
@ -488,15 +550,21 @@ class Addon(AddonModel):
# Start Add-on # Start Add-on
try: try:
await self.instance.run() await self.instance.run()
except DockerAPIError: except DockerAPIError as err:
raise AddonsError() self.state = AddonState.ERROR
raise AddonsError() from err
else:
self.state = AddonState.STARTED
async def stop(self) -> None: async def stop(self) -> None:
"""Stop add-on.""" """Stop add-on."""
try: try:
return await self.instance.stop() return await self.instance.stop()
except DockerAPIError: except DockerAPIError as err:
raise AddonsError() self.state = AddonState.ERROR
raise AddonsError() from err
else:
self.state = AddonState.STOPPED
async def restart(self) -> None: async def restart(self) -> None:
"""Restart add-on.""" """Restart add-on."""
@ -511,12 +579,19 @@ class Addon(AddonModel):
""" """
return self.instance.logs() return self.instance.logs()
def is_running(self) -> Awaitable[bool]:
"""Return True if Docker container is running.
Return a coroutine.
"""
return self.instance.is_running()
async def stats(self) -> DockerStats: async def stats(self) -> DockerStats:
"""Return stats of container.""" """Return stats of container."""
try: try:
return await self.instance.stats() return await self.instance.stats()
except DockerAPIError: except DockerAPIError as err:
raise AddonsError() raise AddonsError() from err
async def write_stdin(self, data) -> None: async def write_stdin(self, data) -> None:
"""Write data to add-on stdin. """Write data to add-on stdin.
@ -529,8 +604,8 @@ class Addon(AddonModel):
try: try:
return await self.instance.write_stdin(data) return await self.instance.write_stdin(data)
except DockerAPIError: except DockerAPIError as err:
raise AddonsError() raise AddonsError() from err
async def snapshot(self, tar_file: tarfile.TarFile) -> None: async def snapshot(self, tar_file: tarfile.TarFile) -> None:
"""Snapshot state of an add-on.""" """Snapshot state of an add-on."""
@ -541,31 +616,31 @@ class Addon(AddonModel):
if self.need_build: if self.need_build:
try: try:
await self.instance.export_image(temp_path.joinpath("image.tar")) await self.instance.export_image(temp_path.joinpath("image.tar"))
except DockerAPIError: except DockerAPIError as err:
raise AddonsError() raise AddonsError() from err
data = { data = {
ATTR_USER: self.persist, ATTR_USER: self.persist,
ATTR_SYSTEM: self.data, ATTR_SYSTEM: self.data,
ATTR_VERSION: self.version, ATTR_VERSION: self.version,
ATTR_STATE: await self.state(), ATTR_STATE: self.state,
} }
# Store local configs/state # Store local configs/state
try: try:
write_json_file(temp_path.joinpath("addon.json"), data) write_json_file(temp_path.joinpath("addon.json"), data)
except JsonFileError: except JsonFileError as err:
_LOGGER.error("Can't save meta for %s", self.slug) _LOGGER.error("Can't save meta for %s", self.slug)
raise AddonsError() raise AddonsError() from err
# Store AppArmor Profile # Store AppArmor Profile
if self.sys_host.apparmor.exists(self.slug): if self.sys_host.apparmor.exists(self.slug):
profile = temp_path.joinpath("apparmor.txt") profile = temp_path.joinpath("apparmor.txt")
try: try:
self.sys_host.apparmor.backup_profile(self.slug, profile) self.sys_host.apparmor.backup_profile(self.slug, profile)
except HostAppArmorError: except HostAppArmorError as err:
_LOGGER.error("Can't backup AppArmor profile") _LOGGER.error("Can't backup AppArmor profile")
raise AddonsError() raise AddonsError() from err
# write into tarfile # write into tarfile
def _write_tarfile(): def _write_tarfile():
@ -588,7 +663,7 @@ class Addon(AddonModel):
await self.sys_run_in_executor(_write_tarfile) await self.sys_run_in_executor(_write_tarfile)
except (tarfile.TarError, OSError) as err: except (tarfile.TarError, OSError) as err:
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err) _LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
raise AddonsError() raise AddonsError() from err
_LOGGER.info("Finish snapshot for addon %s", self.slug) _LOGGER.info("Finish snapshot for addon %s", self.slug)
@ -605,13 +680,13 @@ class Addon(AddonModel):
await self.sys_run_in_executor(_extract_tarfile) await self.sys_run_in_executor(_extract_tarfile)
except tarfile.TarError as err: except tarfile.TarError as err:
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err) _LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
raise AddonsError() raise AddonsError() from err
# Read snapshot data # Read snapshot data
try: try:
data = read_json_file(Path(temp, "addon.json")) data = read_json_file(Path(temp, "addon.json"))
except JsonFileError: except JsonFileError as err:
raise AddonsError() raise AddonsError() from err
# Validate # Validate
try: try:
@ -622,7 +697,7 @@ class Addon(AddonModel):
self.slug, self.slug,
humanize_error(data, err), humanize_error(data, err),
) )
raise AddonsError() raise AddonsError() from err
# If available # If available
if not self._available(data[ATTR_SYSTEM]): if not self._available(data[ATTR_SYSTEM]):
@ -669,21 +744,21 @@ class Addon(AddonModel):
await self.sys_run_in_executor(_restore_data) await self.sys_run_in_executor(_restore_data)
except shutil.Error as err: except shutil.Error as err:
_LOGGER.error("Can't restore origin data: %s", err) _LOGGER.error("Can't restore origin data: %s", err)
raise AddonsError() raise AddonsError() from err
# Restore AppArmor # Restore AppArmor
profile_file = Path(temp, "apparmor.txt") profile_file = Path(temp, "apparmor.txt")
if profile_file.exists(): if profile_file.exists():
try: try:
await self.sys_host.apparmor.load_profile(self.slug, profile_file) await self.sys_host.apparmor.load_profile(self.slug, profile_file)
except HostAppArmorError: except HostAppArmorError as err:
_LOGGER.error( _LOGGER.error(
"Can't restore AppArmor profile for add-on %s", self.slug "Can't restore AppArmor profile for add-on %s", self.slug
) )
raise AddonsError() raise AddonsError() from err
# Run add-on # Run add-on
if data[ATTR_STATE] == STATE_STARTED: if data[ATTR_STATE] == AddonState.STARTED:
return await self.start() return await self.start()
_LOGGER.info("Finish restore for add-on %s", self.slug) _LOGGER.info("Finish restore for add-on %s", self.slug)

View File

@ -61,11 +61,12 @@ from ..const import (
ATTR_USB, ATTR_USB,
ATTR_VERSION, ATTR_VERSION,
ATTR_VIDEO, ATTR_VIDEO,
ATTR_WATCHDOG,
ATTR_WEBUI, ATTR_WEBUI,
SECURITY_DEFAULT, SECURITY_DEFAULT,
SECURITY_DISABLE, SECURITY_DISABLE,
SECURITY_PROFILE, SECURITY_PROFILE,
AddonStages, AddonStage,
AddonStartup, AddonStartup,
) )
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
@ -206,7 +207,7 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_ADVANCED] return self.data[ATTR_ADVANCED]
@property @property
def stage(self) -> AddonStages: def stage(self) -> AddonStage:
"""Return stage mode of add-on.""" """Return stage mode of add-on."""
return self.data[ATTR_STAGE] return self.data[ATTR_STAGE]
@ -248,6 +249,11 @@ class AddonModel(CoreSysAttributes, ABC):
"""Return URL to webui or None.""" """Return URL to webui or None."""
return self.data.get(ATTR_WEBUI) return self.data.get(ATTR_WEBUI)
@property
def watchdog(self) -> Optional[str]:
"""Return URL to for watchdog or None."""
return self.data.get(ATTR_WATCHDOG)
@property @property
def ingress_port(self) -> Optional[int]: def ingress_port(self) -> Optional[int]:
"""Return Ingress port.""" """Return Ingress port."""

View File

@ -80,22 +80,22 @@ from ..const import (
ATTR_UUID, ATTR_UUID,
ATTR_VERSION, ATTR_VERSION,
ATTR_VIDEO, ATTR_VIDEO,
ATTR_WATCHDOG,
ATTR_WEBUI, ATTR_WEBUI,
BOOT_AUTO, BOOT_AUTO,
BOOT_MANUAL, BOOT_MANUAL,
PRIVILEGED_ALL, PRIVILEGED_ALL,
ROLE_ALL, ROLE_ALL,
ROLE_DEFAULT, ROLE_DEFAULT,
STATE_STARTED, AddonStage,
STATE_STOPPED,
AddonStages,
AddonStartup, AddonStartup,
AddonState,
) )
from ..coresys import CoreSys from ..coresys import CoreSys
from ..discovery.validate import valid_discovery_service from ..discovery.validate import valid_discovery_service
from ..validate import ( from ..validate import (
DOCKER_PORTS, docker_ports,
DOCKER_PORTS_DESCRIPTION, docker_ports_description,
network_port, network_port,
token, token,
uuid_match, uuid_match,
@ -196,9 +196,12 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_INIT, default=True): vol.Boolean(), vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(), vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
vol.Optional(ATTR_STAGE, default=AddonStages.STABLE): vol.Coerce(AddonStages), vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage),
vol.Optional(ATTR_PORTS): DOCKER_PORTS, vol.Optional(ATTR_PORTS): docker_ports,
vol.Optional(ATTR_PORTS_DESCRIPTION): DOCKER_PORTS_DESCRIPTION, vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
vol.Optional(ATTR_WATCHDOG): vol.Match(
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:\[PORT:\d+\].*$"
),
vol.Optional(ATTR_WEBUI): vol.Match( vol.Optional(ATTR_WEBUI): vol.Match(
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$" r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
), ),
@ -301,11 +304,12 @@ SCHEMA_ADDON_USER = vol.Schema(
vol.Optional(ATTR_OPTIONS, default=dict): dict, vol.Optional(ATTR_OPTIONS, default=dict): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(), vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): DOCKER_PORTS, vol.Optional(ATTR_NETWORK): docker_ports,
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(), vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(), vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
}, },
extra=vol.REMOVE_EXTRA, extra=vol.REMOVE_EXTRA,
) )
@ -331,7 +335,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema(
{ {
vol.Required(ATTR_USER): SCHEMA_ADDON_USER, vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM, vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
vol.Required(ATTR_STATE): vol.In([STATE_STARTED, STATE_STOPPED]), vol.Required(ATTR_STATE): vol.Coerce(AddonState),
vol.Required(ATTR_VERSION): vol.Coerce(str), vol.Required(ATTR_VERSION): vol.Coerce(str),
}, },
extra=vol.REMOVE_EXTRA, extra=vol.REMOVE_EXTRA,
@ -364,7 +368,7 @@ def validate_options(coresys: CoreSys, raw_schema: Dict[str, Any]):
# normal value # normal value
options[key] = _single_validate(coresys, typ, value, key) options[key] = _single_validate(coresys, typ, value, key)
except (IndexError, KeyError): except (IndexError, KeyError):
raise vol.Invalid(f"Type error for {key}") raise vol.Invalid(f"Type error for {key}") from None
_check_missing_options(raw_schema, options, "root") _check_missing_options(raw_schema, options, "root")
return options return options
@ -378,20 +382,20 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
"""Validate a single element.""" """Validate a single element."""
# if required argument # if required argument
if value is None: if value is None:
raise vol.Invalid(f"Missing required option '{key}'") raise vol.Invalid(f"Missing required option '{key}'") from None
# Lookup secret # Lookup secret
if str(value).startswith("!secret "): if str(value).startswith("!secret "):
secret: str = value.partition(" ")[2] secret: str = value.partition(" ")[2]
value = coresys.secrets.get(secret) value = coresys.secrets.get(secret)
if value is None: if value is None:
raise vol.Invalid(f"Unknown secret {secret}") raise vol.Invalid(f"Unknown secret {secret}") from None
# parse extend data from type # parse extend data from type
match = RE_SCHEMA_ELEMENT.match(typ) match = RE_SCHEMA_ELEMENT.match(typ)
if not match: if not match:
raise vol.Invalid(f"Unknown type {typ}") raise vol.Invalid(f"Unknown type {typ}") from None
# prepare range # prepare range
range_args = {} range_args = {}
@ -419,7 +423,7 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
elif typ.startswith(V_LIST): elif typ.startswith(V_LIST):
return vol.In(match.group("list").split("|"))(str(value)) return vol.In(match.group("list").split("|"))(str(value))
raise vol.Invalid(f"Fatal error for {key} type {typ}") raise vol.Invalid(f"Fatal error for {key} type {typ}") from None
def _nested_validate_list(coresys, typ, data_list, key): def _nested_validate_list(coresys, typ, data_list, key):
@ -428,7 +432,7 @@ def _nested_validate_list(coresys, typ, data_list, key):
# Make sure it is a list # Make sure it is a list
if not isinstance(data_list, list): if not isinstance(data_list, list):
raise vol.Invalid(f"Invalid list for {key}") raise vol.Invalid(f"Invalid list for {key}") from None
# Process list # Process list
for element in data_list: for element in data_list:
@ -448,7 +452,7 @@ def _nested_validate_dict(coresys, typ, data_dict, key):
# Make sure it is a dict # Make sure it is a dict
if not isinstance(data_dict, dict): if not isinstance(data_dict, dict):
raise vol.Invalid(f"Invalid dict for {key}") raise vol.Invalid(f"Invalid dict for {key}") from None
# Process dict # Process dict
for c_key, c_value in data_dict.items(): for c_key, c_value in data_dict.items():
@ -475,7 +479,7 @@ def _check_missing_options(origin, exists, root):
for miss_opt in missing: for miss_opt in missing:
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"): if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
continue continue
raise vol.Invalid(f"Missing option {miss_opt} in {root}") raise vol.Invalid(f"Missing option {miss_opt} in {root}") from None
def schema_ui_options(raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]: def schema_ui_options(raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:

View File

@ -18,6 +18,7 @@ from .host import APIHost
from .info import APIInfo from .info import APIInfo
from .ingress import APIIngress from .ingress import APIIngress
from .multicast import APIMulticast from .multicast import APIMulticast
from .network import APINetwork
from .os import APIOS from .os import APIOS
from .proxy import APIProxy from .proxy import APIProxy
from .security import SecurityMiddleware from .security import SecurityMiddleware
@ -54,6 +55,7 @@ class RestAPI(CoreSysAttributes):
self._register_os() self._register_os()
self._register_cli() self._register_cli()
self._register_multicast() self._register_multicast()
self._register_network()
self._register_hardware() self._register_hardware()
self._register_homeassistant() self._register_homeassistant()
self._register_proxy() self._register_proxy()
@ -89,6 +91,24 @@ class RestAPI(CoreSysAttributes):
] ]
) )
def _register_network(self) -> None:
"""Register network functions."""
api_network = APINetwork()
api_network.coresys = self.coresys
self.webapp.add_routes(
[
web.get("/network/info", api_network.info),
web.get(
"/network/interface/{interface}/info", api_network.interface_info
),
web.post(
"/network/interface/{interface}/update",
api_network.interface_update,
),
]
)
def _register_os(self) -> None: def _register_os(self) -> None:
"""Register OS functions.""" """Register OS functions."""
api_os = APIOS() api_os = APIOS()

View File

@ -85,6 +85,7 @@ from ..const import (
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
ATTR_VIDEO, ATTR_VIDEO,
ATTR_WATCHDOG,
ATTR_WEBUI, ATTR_WEBUI,
BOOT_AUTO, BOOT_AUTO,
BOOT_MANUAL, BOOT_MANUAL,
@ -92,12 +93,12 @@ from ..const import (
CONTENT_TYPE_PNG, CONTENT_TYPE_PNG,
CONTENT_TYPE_TEXT, CONTENT_TYPE_TEXT,
REQUEST_FROM, REQUEST_FROM,
STATE_NONE, AddonState,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import APIError from ..exceptions import APIError
from ..validate import DOCKER_PORTS from ..validate import docker_ports
from .utils import api_process, api_process_raw, api_validate from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -108,11 +109,12 @@ SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
SCHEMA_OPTIONS = vol.Schema( SCHEMA_OPTIONS = vol.Schema(
{ {
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): vol.Maybe(DOCKER_PORTS), vol.Optional(ATTR_NETWORK): vol.Maybe(docker_ports),
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(), vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(), vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
} }
) )
@ -213,7 +215,7 @@ class APIAddons(CoreSysAttributes):
ATTR_MACHINE: addon.supported_machine, ATTR_MACHINE: addon.supported_machine,
ATTR_HOMEASSISTANT: addon.homeassistant_version, ATTR_HOMEASSISTANT: addon.homeassistant_version,
ATTR_URL: addon.url, ATTR_URL: addon.url,
ATTR_STATE: STATE_NONE, ATTR_STATE: AddonState.UNKNOWN,
ATTR_DETACHED: addon.is_detached, ATTR_DETACHED: addon.is_detached,
ATTR_AVAILABLE: addon.available, ATTR_AVAILABLE: addon.available,
ATTR_BUILD: addon.need_build, ATTR_BUILD: addon.need_build,
@ -255,12 +257,13 @@ class APIAddons(CoreSysAttributes):
ATTR_INGRESS_URL: None, ATTR_INGRESS_URL: None,
ATTR_INGRESS_PORT: None, ATTR_INGRESS_PORT: None,
ATTR_INGRESS_PANEL: None, ATTR_INGRESS_PANEL: None,
ATTR_WATCHDOG: None,
} }
if isinstance(addon, Addon) and addon.is_installed: if isinstance(addon, Addon) and addon.is_installed:
data.update( data.update(
{ {
ATTR_STATE: await addon.state(), ATTR_STATE: addon.state,
ATTR_WEBUI: addon.webui, ATTR_WEBUI: addon.webui,
ATTR_INGRESS_ENTRY: addon.ingress_entry, ATTR_INGRESS_ENTRY: addon.ingress_entry,
ATTR_INGRESS_URL: addon.ingress_url, ATTR_INGRESS_URL: addon.ingress_url,
@ -271,6 +274,7 @@ class APIAddons(CoreSysAttributes):
ATTR_AUTO_UPDATE: addon.auto_update, ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_IP_ADDRESS: str(addon.ip_address), ATTR_IP_ADDRESS: str(addon.ip_address),
ATTR_VERSION: addon.version, ATTR_VERSION: addon.version,
ATTR_WATCHDOG: addon.watchdog,
} }
) )
@ -282,7 +286,7 @@ class APIAddons(CoreSysAttributes):
addon = self._extract_addon_installed(request) addon = self._extract_addon_installed(request)
# Update secrets for validation # Update secrets for validation
await self.sys_secrets.reload() await self.sys_homeassistant.secrets.reload()
# Extend schema with add-on specific validation # Extend schema with add-on specific validation
addon_schema = SCHEMA_OPTIONS.extend( addon_schema = SCHEMA_OPTIONS.extend(
@ -306,6 +310,8 @@ class APIAddons(CoreSysAttributes):
if ATTR_INGRESS_PANEL in body: if ATTR_INGRESS_PANEL in body:
addon.ingress_panel = body[ATTR_INGRESS_PANEL] addon.ingress_panel = body[ATTR_INGRESS_PANEL]
await self.sys_ingress.update_hass_panel(addon) await self.sys_ingress.update_hass_panel(addon)
if ATTR_WATCHDOG in body:
addon.watchdog = body[ATTR_WATCHDOG]
addon.save_persist() addon.save_persist()

View File

@ -117,7 +117,7 @@ class APIHomeAssistant(CoreSysAttributes):
@api_process @api_process
async def stats(self, request: web.Request) -> Dict[Any, str]: async def stats(self, request: web.Request) -> Dict[Any, str]:
"""Return resource information.""" """Return resource information."""
stats = await self.sys_homeassistant.stats() stats = await self.sys_homeassistant.core.stats()
if not stats: if not stats:
raise APIError("No stats available") raise APIError("No stats available")
@ -138,36 +138,36 @@ class APIHomeAssistant(CoreSysAttributes):
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version) version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version)
await asyncio.shield(self.sys_homeassistant.update(version)) await asyncio.shield(self.sys_homeassistant.core.update(version))
@api_process @api_process
def stop(self, request: web.Request) -> Awaitable[None]: def stop(self, request: web.Request) -> Awaitable[None]:
"""Stop Home Assistant.""" """Stop Home Assistant."""
return asyncio.shield(self.sys_homeassistant.stop()) return asyncio.shield(self.sys_homeassistant.core.stop())
@api_process @api_process
def start(self, request: web.Request) -> Awaitable[None]: def start(self, request: web.Request) -> Awaitable[None]:
"""Start Home Assistant.""" """Start Home Assistant."""
return asyncio.shield(self.sys_homeassistant.start()) return asyncio.shield(self.sys_homeassistant.core.start())
@api_process @api_process
def restart(self, request: web.Request) -> Awaitable[None]: def restart(self, request: web.Request) -> Awaitable[None]:
"""Restart Home Assistant.""" """Restart Home Assistant."""
return asyncio.shield(self.sys_homeassistant.restart()) return asyncio.shield(self.sys_homeassistant.core.restart())
@api_process @api_process
def rebuild(self, request: web.Request) -> Awaitable[None]: def rebuild(self, request: web.Request) -> Awaitable[None]:
"""Rebuild Home Assistant.""" """Rebuild Home Assistant."""
return asyncio.shield(self.sys_homeassistant.rebuild()) return asyncio.shield(self.sys_homeassistant.core.rebuild())
@api_process_raw(CONTENT_TYPE_BINARY) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request: web.Request) -> Awaitable[bytes]: def logs(self, request: web.Request) -> Awaitable[bytes]:
"""Return Home Assistant Docker logs.""" """Return Home Assistant Docker logs."""
return self.sys_homeassistant.logs() return self.sys_homeassistant.core.logs()
@api_process @api_process
async def check(self, request: web.Request) -> None: async def check(self, request: web.Request) -> None:
"""Check configuration of Home Assistant.""" """Check configuration of Home Assistant."""
result = await self.sys_homeassistant.check_config() result = await self.sys_homeassistant.core.check_config()
if not result.valid: if not result.valid:
raise APIError(result.log) raise APIError(result.log)

View File

@ -1,6 +1,5 @@
"""Init file for Supervisor host RESTful API.""" """Init file for Supervisor host RESTful API."""
import asyncio import asyncio
import logging
from typing import Awaitable from typing import Awaitable
from aiohttp import web from aiohttp import web
@ -26,8 +25,6 @@ from ..const import (
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from .utils import api_process, api_process_raw, api_validate from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__)
SERVICE = "service" SERVICE = "service"
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)}) SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)})

98
supervisor/api/network.py Normal file
View File

@ -0,0 +1,98 @@
"""REST API for network."""
import asyncio
from typing import Any, Dict
from aiohttp import web
import voluptuous as vol
from ..const import (
ATTR_ADDRESS,
ATTR_DNS,
ATTR_GATEWAY,
ATTR_ID,
ATTR_INTERFACE,
ATTR_INTERFACES,
ATTR_IP_ADDRESS,
ATTR_METHOD,
ATTR_METHODS,
ATTR_NAMESERVERS,
ATTR_PRIMARY,
ATTR_TYPE,
)
from ..coresys import CoreSysAttributes
from ..dbus.const import InterfaceMethodSimple
from ..dbus.network.interface import NetworkInterface
from ..dbus.network.utils import int2ip
from ..exceptions import APIError
from .utils import api_process, api_validate
SCHEMA_UPDATE = vol.Schema(
{
vol.Optional(ATTR_ADDRESS): vol.Coerce(str),
vol.Optional(ATTR_METHOD): vol.In(ATTR_METHODS),
vol.Optional(ATTR_GATEWAY): vol.Coerce(str),
vol.Optional(ATTR_DNS): [str],
}
)
def interface_information(interface: NetworkInterface) -> dict:
"""Return a dict with information of a interface to be used in th API."""
return {
ATTR_IP_ADDRESS: f"{interface.ip_address}/{interface.prefix}",
ATTR_GATEWAY: interface.gateway,
ATTR_ID: interface.id,
ATTR_TYPE: interface.type,
ATTR_NAMESERVERS: [int2ip(x) for x in interface.nameservers],
ATTR_METHOD: InterfaceMethodSimple.DHCP
if interface.method == "auto"
else InterfaceMethodSimple.STATIC,
ATTR_PRIMARY: interface.primary,
}
class APINetwork(CoreSysAttributes):
"""Handle REST API for network."""
@api_process
async def info(self, request: web.Request) -> Dict[str, Any]:
"""Return network information."""
interfaces = {}
for interface in self.sys_host.network.interfaces:
interfaces[
self.sys_host.network.interfaces[interface].name
] = interface_information(self.sys_host.network.interfaces[interface])
return {ATTR_INTERFACES: interfaces}
@api_process
async def interface_info(self, request: web.Request) -> Dict[str, Any]:
"""Return network information for a interface."""
req_interface = request.match_info.get(ATTR_INTERFACE)
for interface in self.sys_host.network.interfaces:
if req_interface == self.sys_host.network.interfaces[interface].name:
return interface_information(
self.sys_host.network.interfaces[interface]
)
return {}
@api_process
async def interface_update(self, request: web.Request) -> Dict[str, Any]:
"""Update the configuration of an interface."""
req_interface = request.match_info.get(ATTR_INTERFACE)
if not self.sys_host.network.interfaces.get(req_interface):
raise APIError(f"Interface {req_interface} does not exsist")
args = await api_validate(SCHEMA_UPDATE, request)
if not args:
raise APIError("You need to supply at least one option to update")
await asyncio.shield(
self.sys_host.network.interfaces[req_interface].update_settings(**args)
)
await asyncio.shield(self.sys_host.network.update())
return await asyncio.shield(self.interface_info(request))

View File

@ -1,9 +1,9 @@
try { try {
new Function("import('/api/hassio/app/frontend_latest/entrypoint.855567b9.js')")(); new Function("import('/api/hassio/app/frontend_latest/entrypoint.cfec6eb5.js')")();
} catch (err) { } catch (err) {
var el = document.createElement('script'); var el = document.createElement('script');
el.src = '/api/hassio/app/frontend_es5/entrypoint.19035830.js'; el.src = '/api/hassio/app/frontend_es5/entrypoint.9b944a05.js';
document.body.appendChild(el); document.body.appendChild(el);
} }

View File

@ -0,0 +1,2 @@
(self.webpackJsonp=self.webpackJsonp||[]).push([[1],{177:function(e,r,n){"use strict";n.r(r),n.d(r,"codeMirror",(function(){return c})),n.d(r,"codeMirrorCss",(function(){return i}));var a=n(165),o=n.n(a),s=n(173),t=(n(174),n(175),n(10));o.a.commands.save=function(e){Object(t.a)(e.getWrapperElement(),"editor-save")};var c=o.a,i=s.a}}]);
//# sourceMappingURL=chunk.0d1dbeb0d1afbd18f64e.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.0d1dbeb0d1afbd18f64e.js","sources":["webpack:///chunk.0d1dbeb0d1afbd18f64e.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.1edd93e4d4c4749e55ab.js","sources":["webpack:///chunk.1edd93e4d4c4749e55ab.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,3 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
* @license MIT
*/
/** /**
@license @license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved. Copyright (c) 2015 The Polymer Project Authors. All rights reserved.

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.1f998a3d7e32b7b3c45c.js","sources":["webpack:///chunk.1f998a3d7e32b7b3c45c.js"],"mappings":";AAAA","sourceRoot":""}

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.2dfe3739f6cdf06691c3.js","sources":["webpack:///chunk.2dfe3739f6cdf06691c3.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.2e9e45ecb0d870d27d88.js","sources":["webpack:///chunk.2e9e45ecb0d870d27d88.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.4a9b56271bdf6fea0f78.js","sources":["webpack:///chunk.4a9b56271bdf6fea0f78.js"],"mappings":"AAAA","sourceRoot":""}

View File

@ -1,2 +0,0 @@
(self.webpackJsonp=self.webpackJsonp||[]).push([[1],{168:function(e,r,n){"use strict";n.r(r),n.d(r,"codeMirror",(function(){return c})),n.d(r,"codeMirrorCss",(function(){return i}));var a=n(127),o=n.n(a),s=n(164),t=(n(165),n(166),n(8));o.a.commands.save=function(e){Object(t.a)(e.getWrapperElement(),"editor-save")};var c=o.a,i=s.a}}]);
//# sourceMappingURL=chunk.53ba85527e846b37953a.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.53ba85527e846b37953a.js","sources":["webpack:///chunk.53ba85527e846b37953a.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.5bff531a8ac4ea5d0e8f.js","sources":["webpack:///chunk.5bff531a8ac4ea5d0e8f.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.67ebcbc4ba9d39e9c2b9.js","sources":["webpack:///chunk.67ebcbc4ba9d39e9c2b9.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.6abfd4fdbd7486588838.js","sources":["webpack:///chunk.6abfd4fdbd7486588838.js"],"mappings":"AAAA","sourceRoot":""}

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.7ef6bcb43647bc158e88.js","sources":["webpack:///chunk.7ef6bcb43647bc158e88.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.937f66fb08e0d4aa8818.js","sources":["webpack:///chunk.937f66fb08e0d4aa8818.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.9de6943cce77c49b4586.js","sources":["webpack:///chunk.9de6943cce77c49b4586.js"],"mappings":";AAAA","sourceRoot":""}

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.adad578706ef56ae55ba.js","sources":["webpack:///chunk.adad578706ef56ae55ba.js"],"mappings":"AAAA","sourceRoot":""}

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.d3a71177023db5bc7440.js","sources":["webpack:///chunk.d3a71177023db5bc7440.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.dacd9533a16ebaba94e9.js","sources":["webpack:///chunk.dacd9533a16ebaba94e9.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.e4f91d8c5f0772ea87e5.js","sources":["webpack:///chunk.e4f91d8c5f0772ea87e5.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.f60d6d63bed838a42b65.js","sources":["webpack:///chunk.f60d6d63bed838a42b65.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"entrypoint.19035830.js","sources":["webpack:///entrypoint.19035830.js"],"mappings":";AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,10 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*! ***************************************************************************** /*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved. Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use Licensed under the Apache License, Version 2.0 (the "License"); you may not use
@ -13,6 +20,24 @@ See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License. and limitations under the License.
***************************************************************************** */ ***************************************************************************** */
/**
* @license
* Copyright 2020 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** /**
* @license * @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved. * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
@ -104,6 +129,23 @@ and limitations under the License.
* THE SOFTWARE. * THE SOFTWARE.
*/ */
/**
* @license
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** /**
* @license * @license
* Copyright 2019 Google Inc. * Copyright 2019 Google Inc.

View File

@ -0,0 +1 @@
{"version":3,"file":"entrypoint.9b944a05.js","sources":["webpack:///entrypoint.9b944a05.js"],"mappings":";AAAA","sourceRoot":""}

View File

@ -1,3 +1,3 @@
{ {
"entrypoint.js": "/api/hassio/app/frontend_es5/entrypoint.19035830.js" "entrypoint.js": "/api/hassio/app/frontend_es5/entrypoint.9b944a05.js"
} }

View File

@ -1 +1 @@
{"version":3,"file":"chunk.d7009039727fd352acb9.js","sources":["webpack:///chunk.d7009039727fd352acb9.js"],"mappings":"AAAA;;;AA4LA;AACA;;;AAGA;;;;;AAKA;;AAEA;AAEA;AACA;;;;;;;AAQA;;;;;AAKA;;AAEA;AAEA;AACA;;;;;;;AAQA;;;AAKA;;;;;;;;;;;;;;;;AAuBA;AAohBA;;AAEA;;AAEA;AACA;;AAIA;AAwIA;;;;AAIA;;AAEA;AACA;;;AAGA;;;;AAIA;AACA;;;;;;AAQA;;;;;;;;;;;;;;;;;;;;;;AA6BA;;;AAkMA;AACA;;;;;;;;AAQA;;AAGA;;;AAGA;;AAEA;AACA;;;;AAIA;;;;;;;AAQA;;;AAGA;;;AAvCA;;;;;;;;;;;;;;;AAkEA;;;AAwLA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AApBA;;;;;;;;;;;AA4CA;;;AA6GA;;AAEA;;;;AARA;;;;;;;;;;;;AAiCA;;;;AA8HA;;;AAMA;AACA;;;AAGA;;AAEA;;AAKA;;AAEA;;AAEA;;AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA;AA6LA;;;;AAIA;AACA;AACA;AACA;;;AAGA;;;;;;;;AAQA;AACA;AACA;;;;AAIA;AACA;;;AAGA;;;AAGA;AACA;;;;;;;AAOA;;;;;;;;;AASA;;AAEA;AACA;;;;AAIA;;AAEA;;;;AAIA;;;AAGA;;;;AAIA;AACA;AACA;;;AAGA;;;;;;AAMA;;AAEA;AACA;;;;AAIA;;;AAGA;;AAEA;;AAEA;AACA;AAIA;;;;;;AAMA;;AAEA;AACA;;AAEA;AAKA;;AAEA;;;;AAIA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;AAGA;;AAEA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;AACA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;;AAMA;;;AAGA;;;AAGA;;;;AAIA;AACA;;;;AAIA;;;;AAIA;AACA;;;;AAIA;AACA;;;;AAIA;AACA;AACA;;;AAGA;;;;;AAKA;;AAEA;AACA;;;;;AAKA;;;;;;;AAOA;AACA;;;;AAIA;AACA;AACA;;;AAGA;AACA;;;AAGA;AACA;;;;;;AAMA;AACA;;;;AAIA;;AAEA;AACA;;;;;AAKA;;AAEA;;;;;;;;;;AAUA;AACA;AACA;;;AAGA;;;AAGA;;;;AAIA;;;AAGA;AACA;;;;AAIA;AACA;AACA;;;;;;AAMA;AACA;AACA;;;;;;;;AAQA;;;;AAIA;;;;AAIA;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkIA;;;AA0UA;AACA;AACA;;;AARA;;;;;;AA4BA;AAoGA;;AAEA;;AAEA;AACA;AACA;;;AAGA;;;AAKA;;;;;;;;;AAgBA;;;AAmGA;AACA;;;AAPA;;;;;;AA2BA;;AAmQA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;AACA;;;AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA","sourceRoot":""} {"version":3,"file":"chunk.169d772822215fb022c3.js","sources":["webpack:///chunk.169d772822215fb022c3.js"],"mappings":"AAAA;;;AA4LA;AACA;;;AAGA;;;;;AAKA;;AAEA;AAEA;AACA;;;;;;;AAQA;;;;;AAKA;;AAEA;AAEA;AACA;;;;;;;AAQA;;;AAKA;;;;;;;;;;;;;;;;AAuBA;AA6mBA;;AAEA;;AAEA;AACA;;AAIA;AAwIA;;;;AAIA;;AAEA;AACA;;;AAGA;;;;AAIA;AACA;;;;;;AAQA;;;;;;;;;;;;;;;;;;;;;;AA6BA;;;AAkMA;AACA;;;;;;;;AAQA;;AAGA;;;AAGA;;AAEA;AACA;;;;AAIA;;;;;;;AAQA;;;AAGA;;;AAvCA;;;;;;;;;;;;;;;AAkEA;;;AAwLA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AApBA;;;;;;;;;;;AA4CA;;;AA6GA;;AAEA;;;;AARA;;;;;;;;;;;;AAiCA;;;;AA8HA;;;AAMA;AACA;;;AAGA;;AAEA;;AAKA;;AAEA;;AAEA;;AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA;AAiMA;;;;AAIA;AACA;AACA;AACA;;;AAGA;;;;;;;;AAQA;AACA;AACA;;;;AAIA;AACA;;;AAGA;;;AAGA;AACA;;;;;;;AAOA;;;;;;;;;AASA;;AAEA;AACA;;;;AAIA;;AAEA;;;;AAIA;;;AAGA;;;;AAIA;AACA;AACA;;;AAGA;;;;;;AAMA;;AAEA;AACA;;;;AAIA;;;AAGA;;AAEA;;AAEA;AACA;AAIA;;;;;;AAMA;;AAEA;AACA;;AAEA;AAKA;;AAEA;;;;AAIA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;AAGA;;AAEA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;AACA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;AAKA;;AAEA;AACA;;AAEA;;;;;;AAMA;;;AAGA;;;AAGA;;AAEA;;;;;;;;AAQA;AACA;;;;;AAKA;AACA;;;;;;;;AAQA;AACA;;;;AAIA;AACA;AACA;;;;;;;;;AASA;AACA;;;;AAIA;AACA;AACA;;;;;AAKA;;;AAGA;AACA;AACA;;;;AAIA;AACA;AACA;;;;;;;;AAQA;AACA;;;;AAIA;;AAEA;AACA;;;AAGA;AACA;;;AAGA;AACA;;;;;;AAMA;AACA;;;;AAIA;;AAEA;AACA;;;;;AAKA;;AAEA;;;;;;;;;;AAUA;AACA;AACA;;;AAGA;;;AAGA;;;;AAIA;;;AAGA;AACA;;;;AAIA;AACA;AACA;;;;;;AAMA;AACA;AACA;;;;;;;;AAQA;;;;AAIA;;;;AAIA;AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwZA;;;AAoFA;AACA;AACA;;;AARA;;;;;;AA4BA;AAoGA;;AAEA;;AAEA;AACA;AACA;;;AAGA;;;AAKA;;;;;;;;;AAgBA;;;AAmGA;AACA;;;AAPA;;;;;;AA2BA;;AAmQA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;AACA;;;AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.19fdeb1dc19e9028c877.js","sources":["webpack:///chunk.19fdeb1dc19e9028c877.js"],"mappings":"AAAA;AA0OA;AACA;AACA;AANA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA","sourceRoot":""}

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.1dcdd03de4add9e1f326.js","sources":["webpack:///chunk.1dcdd03de4add9e1f326.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,3 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
* @license MIT
*/
/** /**
@license @license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved. Copyright (c) 2015 The Polymer Project Authors. All rights reserved.

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.3071be0252919da82a50.js","sources":["webpack:///chunk.3071be0252919da82a50.js"],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAy7EA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkfA","sourceRoot":""}

View File

@ -0,0 +1 @@
{"version":3,"file":"chunk.3455ded08421a656fb89.js","sources":["webpack:///chunk.3455ded08421a656fb89.js"],"mappings":"AAAA","sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"version":3,"file":"chunk.46e2dce2dcad3c8f5df8.js","sources":["webpack:///chunk.46e2dce2dcad3c8f5df8.js"],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoyFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgrMA","sourceRoot":""}

View File

@ -1,2 +0,0 @@
(self.webpackJsonp=self.webpackJsonp||[]).push([[1],{163:function(e,r,n){"use strict";n.r(r),n.d(r,"codeMirror",(function(){return c})),n.d(r,"codeMirrorCss",(function(){return i}));var s=n(124),o=n.n(s),t=n(159),a=(n(160),n(161),n(8));o.a.commands.save=e=>{Object(a.a)(e.getWrapperElement(),"editor-save")};const c=o.a,i=t.a}}]);
//# sourceMappingURL=chunk.5066cbaab136d2561122.js.map

Some files were not shown because too many files have changed in this diff Show More