mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-27 11:06:32 +00:00
commit
cea6e7a9f2
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/sentry.yaml
vendored
2
.github/workflows/sentry.yaml
vendored
@ -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 }}
|
||||||
|
@ -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
65
API.md
@ -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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
8
setup.py
8
setup.py
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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]]:
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
98
supervisor/api/network.py
Normal 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))
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
@ -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
|
Binary file not shown.
@ -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
Binary file not shown.
@ -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
@ -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.
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.1f998a3d7e32b7b3c45c.js","sources":["webpack:///chunk.1f998a3d7e32b7b3c45c.js"],"mappings":";AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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
Binary file not shown.
@ -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
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.4a9b56271bdf6fea0f78.js","sources":["webpack:///chunk.4a9b56271bdf6fea0f78.js"],"mappings":"AAAA","sourceRoot":""}
|
@ -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
|
|
Binary file not shown.
@ -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
Binary file not shown.
@ -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
Binary file not shown.
@ -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
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"chunk.6abfd4fdbd7486588838.js","sources":["webpack:///chunk.6abfd4fdbd7486588838.js"],"mappings":"AAAA","sourceRoot":""}
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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
Binary file not shown.
@ -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
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"chunk.9de6943cce77c49b4586.js","sources":["webpack:///chunk.9de6943cce77c49b4586.js"],"mappings":";AAAA","sourceRoot":""}
|
|
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"chunk.adad578706ef56ae55ba.js","sources":["webpack:///chunk.adad578706ef56ae55ba.js"],"mappings":"AAAA","sourceRoot":""}
|
|
Binary file not shown.
@ -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
Binary file not shown.
@ -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
Binary file not shown.
@ -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
Binary file not shown.
@ -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
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"entrypoint.19035830.js","sources":["webpack:///entrypoint.19035830.js"],"mappings":";AAAA","sourceRoot":""}
|
|
3
supervisor/api/panel/frontend_es5/entrypoint.9b944a05.js
Normal file
3
supervisor/api/panel/frontend_es5/entrypoint.9b944a05.js
Normal file
File diff suppressed because one or more lines are too long
@ -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.
|
BIN
supervisor/api/panel/frontend_es5/entrypoint.9b944a05.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/entrypoint.9b944a05.js.gz
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"entrypoint.9b944a05.js","sources":["webpack:///entrypoint.9b944a05.js"],"mappings":";AAAA","sourceRoot":""}
|
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"entrypoint.js": "/api/hassio/app/frontend_es5/entrypoint.19035830.js"
|
"entrypoint.js": "/api/hassio/app/frontend_es5/entrypoint.9b944a05.js"
|
||||||
}
|
}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.19fdeb1dc19e9028c877.js","sources":["webpack:///chunk.19fdeb1dc19e9028c877.js"],"mappings":"AAAA;AA0OA;AACA;AACA;AANA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA","sourceRoot":""}
|
Binary file not shown.
@ -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
@ -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.
|
Binary file not shown.
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.3071be0252919da82a50.js","sources":["webpack:///chunk.3071be0252919da82a50.js"],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAy7EA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkfA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"chunk.46e2dce2dcad3c8f5df8.js","sources":["webpack:///chunk.46e2dce2dcad3c8f5df8.js"],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoyFA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgrMA","sourceRoot":""}
|
|
@ -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
Loading…
x
Reference in New Issue
Block a user