mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-12 10:39:21 +00:00
Compare commits
98 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
160fbb2589 | ||
![]() |
c85aa664e1 | ||
![]() |
51dcbf5db7 | ||
![]() |
fa114a4a03 | ||
![]() |
d7fd58bdb9 | ||
![]() |
38b0aea8e2 | ||
![]() |
41eade9325 | ||
![]() |
e64cf41aec | ||
![]() |
02872b5e75 | ||
![]() |
e4d49bb459 | ||
![]() |
d38b7d5a82 | ||
![]() |
537c5d3197 | ||
![]() |
575df2fcf6 | ||
![]() |
c08c3c6b37 | ||
![]() |
2acf28609e | ||
![]() |
bb59d0431e | ||
![]() |
1c7b1f1462 | ||
![]() |
f32d17d924 | ||
![]() |
928a4d8dce | ||
![]() |
dd3ba93308 | ||
![]() |
7e1b179cdd | ||
![]() |
a9a2c35f06 | ||
![]() |
58b88a6919 | ||
![]() |
f937876a1b | ||
![]() |
8193f43634 | ||
![]() |
1d3f880f82 | ||
![]() |
ef2fa8d2e2 | ||
![]() |
51997b3e7c | ||
![]() |
98785b00e2 | ||
![]() |
8d3694884d | ||
![]() |
a2821a98ad | ||
![]() |
8d552ae15c | ||
![]() |
6db4c60f47 | ||
![]() |
805c0385a0 | ||
![]() |
cea6e7a9f2 | ||
![]() |
127073c01b | ||
![]() |
30fe36ae05 | ||
![]() |
58bd677832 | ||
![]() |
1a3b369dd7 | ||
![]() |
6e38216abd | ||
![]() |
efcfc1f841 | ||
![]() |
8dea50ce83 | ||
![]() |
7a5a01bdcc | ||
![]() |
bd1450a682 | ||
![]() |
c538c1ce7f | ||
![]() |
b6d59c4f64 | ||
![]() |
a758ccaf5c | ||
![]() |
e8b04cc20a | ||
![]() |
9bcb15dbc0 | ||
![]() |
1e953167b6 | ||
![]() |
979586cdb2 | ||
![]() |
cd31fad56d | ||
![]() |
ff57d88e2a | ||
![]() |
06cb5e171e | ||
![]() |
a8b70a2e13 | ||
![]() |
948019ccee | ||
![]() |
89ed109505 | ||
![]() |
fae246c503 | ||
![]() |
2411b4287d | ||
![]() |
b3308ecbe0 | ||
![]() |
3541cbff5e | ||
![]() |
838ba7ff36 | ||
![]() |
e9802f92c9 | ||
![]() |
016fd24859 | ||
![]() |
d315e81ab2 | ||
![]() |
97c38b8534 | ||
![]() |
011e2b3df5 | ||
![]() |
e3ee9a299f | ||
![]() |
d73c10f874 | ||
![]() |
9e448b46ba | ||
![]() |
9f09c46789 | ||
![]() |
fe6634551a | ||
![]() |
22a7931a7c | ||
![]() |
94f112512f | ||
![]() |
b6509dca1f | ||
![]() |
620234e708 | ||
![]() |
d50e866cec | ||
![]() |
76ad6dca02 | ||
![]() |
cdb1520a63 | ||
![]() |
bbef706a33 | ||
![]() |
835509901f | ||
![]() |
b51f9586c4 | ||
![]() |
fc83cb9559 | ||
![]() |
f5f5f829ac | ||
![]() |
930eed4500 | ||
![]() |
01a8b58054 | ||
![]() |
eba1d01fc2 | ||
![]() |
84755836c9 | ||
![]() |
c9585033cb | ||
![]() |
2d312c276f | ||
![]() |
3b0d0e9928 | ||
![]() |
8307b153e3 | ||
![]() |
dfaffe3ec5 | ||
![]() |
8d7b15cbeb | ||
![]() |
00969a67ac | ||
![]() |
a374d4e817 | ||
![]() |
f5dda39f63 | ||
![]() |
fb5d54d5fe |
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -6,6 +6,7 @@ daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- rfc
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -429,4 +429,4 @@ jobs:
|
||||
coverage report
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1.0.12
|
||||
uses: codecov/codecov-action@v1.0.13
|
||||
|
21
.github/workflows/sentry.yaml
vendored
Normal file
21
.github/workflows/sentry.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Sentry Release
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
release:
|
||||
types: [published, prereleased]
|
||||
|
||||
jobs:
|
||||
createSentryRelease:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Sentry Release
|
||||
uses: getsentry/action-release@v1.0.1
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
environment: production
|
@@ -1,11 +1,13 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 19.10b0
|
||||
rev: 20.8b1
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
- --safe
|
||||
- --quiet
|
||||
- --target-version
|
||||
- py38
|
||||
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.3
|
||||
|
83
API.md
83
API.md
@@ -25,7 +25,7 @@ On success / Code 200:
|
||||
|
||||
To access the API you need use an authorization header with a `Bearer` token.
|
||||
|
||||
The token is available for add-ons and Home Assistant using the
|
||||
The token is available for add-ons and Home Assistant using the
|
||||
`SUPERVISOR_TOKEN` environment variable.
|
||||
|
||||
### Supervisor
|
||||
@@ -242,13 +242,16 @@ return:
|
||||
|
||||
```json
|
||||
{
|
||||
"hostname": "hostname|null",
|
||||
"features": ["shutdown", "reboot", "hostname", "services", "hassos"],
|
||||
"operating_system": "HassOS XY|Ubuntu 16.4|null",
|
||||
"kernel": "4.15.7|null",
|
||||
"chassis": "specific|null",
|
||||
"cpe": "xy|null",
|
||||
"deployment": "stable|beta|dev|null",
|
||||
"cpe": "xy|null"
|
||||
"disk_total": 32.0,
|
||||
"disk_used": 30.0,
|
||||
"disk_free": 2.0,
|
||||
"features": ["shutdown", "reboot", "hostname", "services", "hassos"],
|
||||
"hostname": "hostname|null",
|
||||
"kernel": "4.15.7|null",
|
||||
"operating_system": "HassOS XY|Ubuntu 16.4|null"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -439,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/{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/{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` | 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
|
||||
|
||||
If an add-on will call itself, you can use `/addons/self/...`.
|
||||
@@ -503,6 +565,7 @@ Get all available add-ons.
|
||||
"version": "null|VERSION_INSTALLED",
|
||||
"version_latest": "version_latest",
|
||||
"state": "none|started|stopped",
|
||||
"startup": "initialize|system|services|application|once",
|
||||
"boot": "auto|manual",
|
||||
"build": "bool",
|
||||
"options": "{}",
|
||||
@@ -532,6 +595,7 @@ Get all available add-ons.
|
||||
"stdin": "bool",
|
||||
"webui": "null|http(s)://[HOST]:port/xy/zx",
|
||||
"gpio": "bool",
|
||||
"usb": "[physical_path_to_usb_device]",
|
||||
"kernel_modules": "bool",
|
||||
"devicetree": "bool",
|
||||
"docker_api": "bool",
|
||||
@@ -546,7 +610,8 @@ Get all available add-ons.
|
||||
"ingress_entry": "null|/api/hassio_ingress/slug",
|
||||
"ingress_url": "null|/api/hassio_ingress/slug/entry.html",
|
||||
"ingress_port": "null|int",
|
||||
"ingress_panel": "null|bool"
|
||||
"ingress_panel": "null|bool",
|
||||
"watchdog": "null|bool"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -555,6 +620,7 @@ Get all available add-ons.
|
||||
- GET `/addons/{addon}/changelog`
|
||||
- GET `/addons/{addon}/documentation`
|
||||
- POST `/addons/{addon}/options`
|
||||
- POST `/addons/{addon}/options/validate`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -566,7 +632,8 @@ Get all available add-ons.
|
||||
"options": {},
|
||||
"audio_output": "null|0,0",
|
||||
"audio_input": "null|0,0",
|
||||
"ingress_panel": "bool"
|
||||
"ingress_panel": "bool",
|
||||
"watchdog": "bool"
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -14,8 +14,7 @@ RUN \
|
||||
libffi \
|
||||
libpulse \
|
||||
musl \
|
||||
openssl \
|
||||
socat
|
||||
openssl
|
||||
|
||||
ARG BUILD_ARCH
|
||||
WORKDIR /usr/src
|
||||
|
@@ -7,3 +7,5 @@ coverage:
|
||||
target: 40
|
||||
threshold: 0.09
|
||||
comment: false
|
||||
github_checks:
|
||||
annotations: false
|
Submodule home-assistant-polymer updated: 77b25f5132...faee2c3e1b
@@ -1,19 +1,19 @@
|
||||
aiohttp==3.6.2
|
||||
async_timeout==3.0.1
|
||||
attrs==19.3.0
|
||||
attrs==20.1.0
|
||||
cchardet==2.1.6
|
||||
colorlog==4.2.1
|
||||
cpe==1.2.1
|
||||
cryptography==3.0
|
||||
debugpy==1.0.0rc1
|
||||
docker==4.3.0
|
||||
gitpython==3.1.7
|
||||
cryptography==3.1
|
||||
debugpy==1.0.0rc2
|
||||
docker==4.3.1
|
||||
gitpython==3.1.8
|
||||
jinja2==2.11.2
|
||||
packaging==20.4
|
||||
pulsectl==20.5.1
|
||||
pytz==2020.1
|
||||
pyudev==0.22.0
|
||||
ruamel.yaml==0.15.100
|
||||
sentry-sdk==0.16.3
|
||||
sentry-sdk==0.17.3
|
||||
uvloop==0.14.0
|
||||
voluptuous==0.11.7
|
||||
|
@@ -1,13 +1,14 @@
|
||||
black==19.10b0
|
||||
codecov==2.1.8
|
||||
black==20.8b1
|
||||
codecov==2.1.9
|
||||
coverage==5.2.1
|
||||
flake8-docstrings==1.5.0
|
||||
flake8==3.8.3
|
||||
pre-commit==2.6.0
|
||||
pydocstyle==5.0.2
|
||||
pylint==2.5.3
|
||||
pre-commit==2.7.1
|
||||
pydocstyle==5.1.1
|
||||
pylint==2.6.0
|
||||
pytest-aiohttp==0.3.0
|
||||
pytest-cov==2.10.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-timeout==1.4.2
|
||||
pytest==6.0.1
|
||||
pyupgrade==2.7.2
|
||||
|
10
setup.py
10
setup.py
@@ -35,10 +35,20 @@ setup(
|
||||
"supervisor.docker",
|
||||
"supervisor.addons",
|
||||
"supervisor.api",
|
||||
"supervisor.dbus",
|
||||
"supervisor.dbus.payloads",
|
||||
"supervisor.dbus.network",
|
||||
"supervisor.discovery",
|
||||
"supervisor.discovery.services",
|
||||
"supervisor.services",
|
||||
"supervisor.services.modules",
|
||||
"supervisor.homeassistant",
|
||||
"supervisor.host",
|
||||
"supervisor.misc",
|
||||
"supervisor.utils",
|
||||
"supervisor.plugins",
|
||||
"supervisor.snapshots",
|
||||
"supervisor.store",
|
||||
],
|
||||
include_package_data=True,
|
||||
)
|
||||
|
@@ -5,7 +5,7 @@ import logging
|
||||
import tarfile
|
||||
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 ..exceptions import (
|
||||
AddonsError,
|
||||
@@ -45,7 +45,7 @@ class AddonManager(CoreSysAttributes):
|
||||
"""Return a list of all installed add-ons."""
|
||||
return list(self.local.values())
|
||||
|
||||
def get(self, addon_slug: str) -> Optional[AnyAddon]:
|
||||
def get(self, addon_slug: str, local_only: bool = False) -> Optional[AnyAddon]:
|
||||
"""Return an add-on from slug.
|
||||
|
||||
Prio:
|
||||
@@ -54,7 +54,9 @@ class AddonManager(CoreSysAttributes):
|
||||
"""
|
||||
if addon_slug in self.local:
|
||||
return self.local[addon_slug]
|
||||
return self.store.get(addon_slug)
|
||||
if not local_only:
|
||||
return self.store.get(addon_slug)
|
||||
return None
|
||||
|
||||
def from_token(self, token: str) -> Optional[Addon]:
|
||||
"""Return an add-on from Supervisor token."""
|
||||
@@ -106,7 +108,7 @@ class AddonManager(CoreSysAttributes):
|
||||
"""Shutdown addons."""
|
||||
tasks: List[Addon] = []
|
||||
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
|
||||
tasks.append(addon)
|
||||
|
||||
@@ -151,12 +153,17 @@ class AddonManager(CoreSysAttributes):
|
||||
|
||||
try:
|
||||
await addon.instance.install(store.version, store.image)
|
||||
except DockerAPIError:
|
||||
except DockerAPIError as err:
|
||||
self.data.uninstall(addon)
|
||||
raise AddonsError() from None
|
||||
raise AddonsError() from err
|
||||
else:
|
||||
self.local[slug] = addon
|
||||
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
||||
|
||||
# Reload ingress tokens
|
||||
if addon.with_ingress:
|
||||
await self.sys_ingress.reload()
|
||||
|
||||
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
||||
|
||||
async def uninstall(self, slug: str) -> None:
|
||||
"""Remove an add-on."""
|
||||
@@ -167,8 +174,10 @@ class AddonManager(CoreSysAttributes):
|
||||
|
||||
try:
|
||||
await addon.instance.remove()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
except DockerAPIError as err:
|
||||
raise AddonsError() from err
|
||||
else:
|
||||
addon.state = AddonState.UNKNOWN
|
||||
|
||||
await addon.remove_data()
|
||||
|
||||
@@ -188,7 +197,9 @@ class AddonManager(CoreSysAttributes):
|
||||
await self.sys_ingress.update_hass_panel(addon)
|
||||
|
||||
# Cleanup Ingress dynamic port assignment
|
||||
self.sys_ingress.del_dynamic_port(slug)
|
||||
if addon.with_ingress:
|
||||
self.sys_create_task(self.sys_ingress.reload())
|
||||
self.sys_ingress.del_dynamic_port(slug)
|
||||
|
||||
# Cleanup discovery data
|
||||
for message in self.sys_discovery.list_messages:
|
||||
@@ -229,15 +240,15 @@ class AddonManager(CoreSysAttributes):
|
||||
raise AddonsNotSupportedError()
|
||||
|
||||
# Update instance
|
||||
last_state = await addon.state()
|
||||
last_state: AddonState = addon.state
|
||||
try:
|
||||
await addon.instance.update(store.version, store.image)
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
await addon.instance.cleanup()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
except DockerAPIError as err:
|
||||
raise AddonsError() from err
|
||||
else:
|
||||
self.data.update(store)
|
||||
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
||||
@@ -246,7 +257,7 @@ class AddonManager(CoreSysAttributes):
|
||||
await addon.install_apparmor()
|
||||
|
||||
# restore state
|
||||
if last_state == STATE_STARTED:
|
||||
if last_state == AddonState.STARTED:
|
||||
await addon.start()
|
||||
|
||||
async def rebuild(self, slug: str) -> None:
|
||||
@@ -270,18 +281,18 @@ class AddonManager(CoreSysAttributes):
|
||||
raise AddonsNotSupportedError()
|
||||
|
||||
# remove docker container but not addon config
|
||||
last_state = await addon.state()
|
||||
last_state: AddonState = addon.state
|
||||
try:
|
||||
await addon.instance.remove()
|
||||
await addon.instance.install(addon.version)
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
except DockerAPIError as err:
|
||||
raise AddonsError() from err
|
||||
else:
|
||||
self.data.update(store)
|
||||
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
||||
|
||||
# restore state
|
||||
if last_state == STATE_STARTED:
|
||||
if last_state == AddonState.STARTED:
|
||||
await addon.start()
|
||||
|
||||
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
||||
@@ -302,6 +313,7 @@ class AddonManager(CoreSysAttributes):
|
||||
|
||||
# Update ingress
|
||||
if addon.with_ingress:
|
||||
await self.sys_ingress.reload()
|
||||
with suppress(HomeAssistantAPIError):
|
||||
await self.sys_ingress.update_hass_panel(addon)
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor add-ons."""
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from copy import deepcopy
|
||||
from ipaddress import IPv4Address
|
||||
@@ -11,6 +12,7 @@ import tarfile
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any, Awaitable, Dict, List, Optional
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
@@ -35,20 +37,23 @@ from ..const import (
|
||||
ATTR_USER,
|
||||
ATTR_UUID,
|
||||
ATTR_VERSION,
|
||||
ATTR_WATCHDOG,
|
||||
DNS_SUFFIX,
|
||||
STATE_STARTED,
|
||||
STATE_STOPPED,
|
||||
AddonStartup,
|
||||
AddonState,
|
||||
)
|
||||
from ..coresys import CoreSys
|
||||
from ..docker.addon import DockerAddon
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import (
|
||||
AddonConfigurationError,
|
||||
AddonsError,
|
||||
AddonsNotSupportedError,
|
||||
DockerAPIError,
|
||||
HostAppArmorError,
|
||||
JsonFileError,
|
||||
)
|
||||
from ..utils import check_port
|
||||
from ..utils.apparmor import adjust_profile
|
||||
from ..utils.json import read_json_file, write_json_file
|
||||
from ..utils.tar import atomic_contents_add, secure_path
|
||||
@@ -63,8 +68,15 @@ RE_WEBUI = re.compile(
|
||||
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+")
|
||||
|
||||
WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||
|
||||
|
||||
class Addon(AddonModel):
|
||||
"""Hold data for add-on inside Supervisor."""
|
||||
@@ -73,12 +85,24 @@ class Addon(AddonModel):
|
||||
"""Initialize data holder."""
|
||||
super().__init__(coresys, slug)
|
||||
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 initialize of object."""
|
||||
with suppress(DockerAPIError):
|
||||
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
|
||||
def ip_address(self) -> IPv4Address:
|
||||
"""Return IP of add-on instance."""
|
||||
@@ -154,6 +178,21 @@ class Addon(AddonModel):
|
||||
"""Set auto update."""
|
||||
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."""
|
||||
if value and self.startup == AddonStartup.ONCE:
|
||||
_LOGGER.warning(
|
||||
"Ignoring watchdog for %s because startup type is 'once'", self.slug
|
||||
)
|
||||
else:
|
||||
self.persist[ATTR_WATCHDOG] = value
|
||||
|
||||
@property
|
||||
def uuid(self) -> str:
|
||||
"""Return an API token for this add-on."""
|
||||
@@ -229,8 +268,6 @@ class Addon(AddonModel):
|
||||
if not url:
|
||||
return None
|
||||
webui = RE_WEBUI.match(url)
|
||||
if not webui:
|
||||
return None
|
||||
|
||||
# extract arguments
|
||||
t_port = webui.group("t_port")
|
||||
@@ -244,10 +281,6 @@ class Addon(AddonModel):
|
||||
else:
|
||||
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
|
||||
if t_proto:
|
||||
proto = "https" if self.options.get(t_proto) else "http"
|
||||
@@ -352,13 +385,55 @@ class Addon(AddonModel):
|
||||
"""Save data of add-on."""
|
||||
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:
|
||||
"""Return True if add-on options is written to data."""
|
||||
schema = self.schema
|
||||
options = self.options
|
||||
|
||||
# Update secrets for validation
|
||||
await self.sys_secrets.reload()
|
||||
await self.sys_homeassistant.secrets.reload()
|
||||
|
||||
try:
|
||||
options = schema(options)
|
||||
@@ -375,7 +450,7 @@ class Addon(AddonModel):
|
||||
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
||||
return
|
||||
|
||||
raise AddonsError()
|
||||
raise AddonConfigurationError()
|
||||
|
||||
async def remove_data(self) -> None:
|
||||
"""Remove add-on data."""
|
||||
@@ -461,12 +536,6 @@ class Addon(AddonModel):
|
||||
return False
|
||||
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:
|
||||
"""Set options and start add-on."""
|
||||
if await self.instance.is_running():
|
||||
@@ -487,15 +556,21 @@ class Addon(AddonModel):
|
||||
# Start Add-on
|
||||
try:
|
||||
await self.instance.run()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
except DockerAPIError as err:
|
||||
self.state = AddonState.ERROR
|
||||
raise AddonsError(err) from None
|
||||
else:
|
||||
self.state = AddonState.STARTED
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop add-on."""
|
||||
try:
|
||||
return await self.instance.stop()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
except DockerAPIError as err:
|
||||
self.state = AddonState.ERROR
|
||||
raise AddonsError() from err
|
||||
else:
|
||||
self.state = AddonState.STOPPED
|
||||
|
||||
async def restart(self) -> None:
|
||||
"""Restart add-on."""
|
||||
@@ -510,12 +585,19 @@ class Addon(AddonModel):
|
||||
"""
|
||||
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:
|
||||
"""Return stats of container."""
|
||||
try:
|
||||
return await self.instance.stats()
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
except DockerAPIError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
async def write_stdin(self, data) -> None:
|
||||
"""Write data to add-on stdin.
|
||||
@@ -528,8 +610,8 @@ class Addon(AddonModel):
|
||||
|
||||
try:
|
||||
return await self.instance.write_stdin(data)
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
except DockerAPIError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
|
||||
"""Snapshot state of an add-on."""
|
||||
@@ -540,31 +622,31 @@ class Addon(AddonModel):
|
||||
if self.need_build:
|
||||
try:
|
||||
await self.instance.export_image(temp_path.joinpath("image.tar"))
|
||||
except DockerAPIError:
|
||||
raise AddonsError() from None
|
||||
except DockerAPIError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
data = {
|
||||
ATTR_USER: self.persist,
|
||||
ATTR_SYSTEM: self.data,
|
||||
ATTR_VERSION: self.version,
|
||||
ATTR_STATE: await self.state(),
|
||||
ATTR_STATE: self.state,
|
||||
}
|
||||
|
||||
# Store local configs/state
|
||||
try:
|
||||
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)
|
||||
raise AddonsError() from None
|
||||
raise AddonsError() from err
|
||||
|
||||
# Store AppArmor Profile
|
||||
if self.sys_host.apparmor.exists(self.slug):
|
||||
profile = temp_path.joinpath("apparmor.txt")
|
||||
try:
|
||||
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
||||
except HostAppArmorError:
|
||||
except HostAppArmorError as err:
|
||||
_LOGGER.error("Can't backup AppArmor profile")
|
||||
raise AddonsError() from None
|
||||
raise AddonsError() from err
|
||||
|
||||
# write into tarfile
|
||||
def _write_tarfile():
|
||||
@@ -587,7 +669,7 @@ class Addon(AddonModel):
|
||||
await self.sys_run_in_executor(_write_tarfile)
|
||||
except (tarfile.TarError, OSError) as err:
|
||||
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
||||
raise AddonsError() from None
|
||||
raise AddonsError() from err
|
||||
|
||||
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
||||
|
||||
@@ -604,13 +686,13 @@ class Addon(AddonModel):
|
||||
await self.sys_run_in_executor(_extract_tarfile)
|
||||
except tarfile.TarError as err:
|
||||
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
||||
raise AddonsError() from None
|
||||
raise AddonsError() from err
|
||||
|
||||
# Read snapshot data
|
||||
try:
|
||||
data = read_json_file(Path(temp, "addon.json"))
|
||||
except JsonFileError:
|
||||
raise AddonsError() from None
|
||||
except JsonFileError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
# Validate
|
||||
try:
|
||||
@@ -621,7 +703,7 @@ class Addon(AddonModel):
|
||||
self.slug,
|
||||
humanize_error(data, err),
|
||||
)
|
||||
raise AddonsError() from None
|
||||
raise AddonsError() from err
|
||||
|
||||
# If available
|
||||
if not self._available(data[ATTR_SYSTEM]):
|
||||
@@ -668,21 +750,21 @@ class Addon(AddonModel):
|
||||
await self.sys_run_in_executor(_restore_data)
|
||||
except shutil.Error as err:
|
||||
_LOGGER.error("Can't restore origin data: %s", err)
|
||||
raise AddonsError() from None
|
||||
raise AddonsError() from err
|
||||
|
||||
# Restore AppArmor
|
||||
profile_file = Path(temp, "apparmor.txt")
|
||||
if profile_file.exists():
|
||||
try:
|
||||
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
||||
except HostAppArmorError:
|
||||
except HostAppArmorError as err:
|
||||
_LOGGER.error(
|
||||
"Can't restore AppArmor profile for add-on %s", self.slug
|
||||
)
|
||||
raise AddonsError() from None
|
||||
raise AddonsError() from err
|
||||
|
||||
# Run add-on
|
||||
if data[ATTR_STATE] == STATE_STARTED:
|
||||
if data[ATTR_STATE] == AddonState.STARTED:
|
||||
return await self.start()
|
||||
|
||||
_LOGGER.info("Finish restore for add-on %s", self.slug)
|
||||
|
@@ -58,13 +58,15 @@ from ..const import (
|
||||
ATTR_TMPFS,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
ATTR_VERSION,
|
||||
ATTR_VIDEO,
|
||||
ATTR_WATCHDOG,
|
||||
ATTR_WEBUI,
|
||||
SECURITY_DEFAULT,
|
||||
SECURITY_DISABLE,
|
||||
SECURITY_PROFILE,
|
||||
AddonStages,
|
||||
AddonStage,
|
||||
AddonStartup,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
@@ -205,7 +207,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_ADVANCED]
|
||||
|
||||
@property
|
||||
def stage(self) -> AddonStages:
|
||||
def stage(self) -> AddonStage:
|
||||
"""Return stage mode of add-on."""
|
||||
return self.data[ATTR_STAGE]
|
||||
|
||||
@@ -247,6 +249,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return URL to webui or None."""
|
||||
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
|
||||
def ingress_port(self) -> Optional[int]:
|
||||
"""Return Ingress port."""
|
||||
@@ -292,11 +299,6 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return devices of add-on."""
|
||||
return self.data.get(ATTR_DEVICES, [])
|
||||
|
||||
@property
|
||||
def auto_uart(self) -> bool:
|
||||
"""Return True if we should map all UART device."""
|
||||
return self.data[ATTR_AUTO_UART]
|
||||
|
||||
@property
|
||||
def tmpfs(self) -> Optional[str]:
|
||||
"""Return tmpfs of add-on."""
|
||||
@@ -376,6 +378,16 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return True if the add-on access to GPIO interface."""
|
||||
return self.data[ATTR_GPIO]
|
||||
|
||||
@property
|
||||
def with_usb(self) -> bool:
|
||||
"""Return True if the add-on need USB access."""
|
||||
return self.data[ATTR_USB]
|
||||
|
||||
@property
|
||||
def with_uart(self) -> bool:
|
||||
"""Return True if we should map all UART device."""
|
||||
return self.data[ATTR_AUTO_UART]
|
||||
|
||||
@property
|
||||
def with_udev(self) -> bool:
|
||||
"""Return True if the add-on have his own udev."""
|
||||
|
@@ -75,26 +75,27 @@ from ..const import (
|
||||
ATTR_TMPFS,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
ATTR_USER,
|
||||
ATTR_UUID,
|
||||
ATTR_VERSION,
|
||||
ATTR_VIDEO,
|
||||
ATTR_WATCHDOG,
|
||||
ATTR_WEBUI,
|
||||
BOOT_AUTO,
|
||||
BOOT_MANUAL,
|
||||
PRIVILEGED_ALL,
|
||||
ROLE_ALL,
|
||||
ROLE_DEFAULT,
|
||||
STATE_STARTED,
|
||||
STATE_STOPPED,
|
||||
AddonStages,
|
||||
AddonStage,
|
||||
AddonStartup,
|
||||
AddonState,
|
||||
)
|
||||
from ..coresys import CoreSys
|
||||
from ..discovery.validate import valid_discovery_service
|
||||
from ..validate import (
|
||||
DOCKER_PORTS,
|
||||
DOCKER_PORTS_DESCRIPTION,
|
||||
docker_ports,
|
||||
docker_ports_description,
|
||||
network_port,
|
||||
token,
|
||||
uuid_match,
|
||||
@@ -195,9 +196,12 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_STAGE, default=AddonStages.STABLE): vol.Coerce(AddonStages),
|
||||
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
|
||||
vol.Optional(ATTR_PORTS_DESCRIPTION): DOCKER_PORTS_DESCRIPTION,
|
||||
vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage),
|
||||
vol.Optional(ATTR_PORTS): docker_ports,
|
||||
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(
|
||||
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||
),
|
||||
@@ -226,6 +230,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_USB, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
||||
@@ -299,11 +304,12 @@ SCHEMA_ADDON_USER = vol.Schema(
|
||||
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
||||
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
||||
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_INPUT): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
@@ -329,7 +335,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
||||
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),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
@@ -376,20 +382,20 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
|
||||
"""Validate a single element."""
|
||||
# if required argument
|
||||
if value is None:
|
||||
raise vol.Invalid(f"Missing required option '{key}'")
|
||||
raise vol.Invalid(f"Missing required option '{key}'") from None
|
||||
|
||||
# Lookup secret
|
||||
if str(value).startswith("!secret "):
|
||||
secret: str = value.partition(" ")[2]
|
||||
value = coresys.secrets.get(secret)
|
||||
value = coresys.homeassistant.secrets.get(secret)
|
||||
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
|
||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||
|
||||
if not match:
|
||||
raise vol.Invalid(f"Unknown type {typ}")
|
||||
raise vol.Invalid(f"Unknown type {typ}") from None
|
||||
|
||||
# prepare range
|
||||
range_args = {}
|
||||
@@ -417,7 +423,7 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
|
||||
elif typ.startswith(V_LIST):
|
||||
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):
|
||||
@@ -426,7 +432,7 @@ def _nested_validate_list(coresys, typ, data_list, key):
|
||||
|
||||
# Make sure it is a 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
|
||||
for element in data_list:
|
||||
@@ -446,7 +452,7 @@ def _nested_validate_dict(coresys, typ, data_dict, key):
|
||||
|
||||
# Make sure it is a 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
|
||||
for c_key, c_value in data_dict.items():
|
||||
@@ -473,7 +479,7 @@ def _check_missing_options(origin, exists, root):
|
||||
for miss_opt in missing:
|
||||
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
||||
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]]:
|
||||
|
@@ -18,6 +18,7 @@ from .host import APIHost
|
||||
from .info import APIInfo
|
||||
from .ingress import APIIngress
|
||||
from .multicast import APIMulticast
|
||||
from .network import APINetwork
|
||||
from .os import APIOS
|
||||
from .proxy import APIProxy
|
||||
from .security import SecurityMiddleware
|
||||
@@ -54,6 +55,7 @@ class RestAPI(CoreSysAttributes):
|
||||
self._register_os()
|
||||
self._register_cli()
|
||||
self._register_multicast()
|
||||
self._register_network()
|
||||
self._register_hardware()
|
||||
self._register_homeassistant()
|
||||
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:
|
||||
"""Register OS functions."""
|
||||
api_os = APIOS()
|
||||
@@ -248,6 +268,9 @@ class RestAPI(CoreSysAttributes):
|
||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
||||
web.post("/addons/{addon}/update", api_addons.update),
|
||||
web.post("/addons/{addon}/options", api_addons.options),
|
||||
web.post(
|
||||
"/addons/{addon}/options/validate", api_addons.options_validate
|
||||
),
|
||||
web.post("/addons/{addon}/rebuild", api_addons.rebuild),
|
||||
web.get("/addons/{addon}/logs", api_addons.logs),
|
||||
web.get("/addons/{addon}/icon", api_addons.icon),
|
||||
|
@@ -5,6 +5,7 @@ from typing import Any, Awaitable, Dict, List
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from ..addons import AnyAddon
|
||||
from ..addons.addon import Addon
|
||||
@@ -61,6 +62,7 @@ from ..const import (
|
||||
ATTR_MEMORY_LIMIT,
|
||||
ATTR_MEMORY_PERCENT,
|
||||
ATTR_MEMORY_USAGE,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_NAME,
|
||||
ATTR_NETWORK,
|
||||
ATTR_NETWORK_DESCRIPTION,
|
||||
@@ -77,13 +79,17 @@ from ..const import (
|
||||
ATTR_SLUG,
|
||||
ATTR_SOURCE,
|
||||
ATTR_STAGE,
|
||||
ATTR_STARTUP,
|
||||
ATTR_STATE,
|
||||
ATTR_STDIN,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
ATTR_VALID,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
ATTR_VIDEO,
|
||||
ATTR_WATCHDOG,
|
||||
ATTR_WEBUI,
|
||||
BOOT_AUTO,
|
||||
BOOT_MANUAL,
|
||||
@@ -91,12 +97,12 @@ from ..const import (
|
||||
CONTENT_TYPE_PNG,
|
||||
CONTENT_TYPE_TEXT,
|
||||
REQUEST_FROM,
|
||||
STATE_NONE,
|
||||
AddonState,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import APIError
|
||||
from ..validate import DOCKER_PORTS
|
||||
from ..validate import docker_ports
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@@ -107,11 +113,12 @@ SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
||||
SCHEMA_OPTIONS = vol.Schema(
|
||||
{
|
||||
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_AUDIO_OUTPUT): 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_WATCHDOG): vol.Boolean(),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -212,7 +219,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_MACHINE: addon.supported_machine,
|
||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||
ATTR_URL: addon.url,
|
||||
ATTR_STATE: STATE_NONE,
|
||||
ATTR_STATE: AddonState.UNKNOWN,
|
||||
ATTR_DETACHED: addon.is_detached,
|
||||
ATTR_AVAILABLE: addon.available,
|
||||
ATTR_BUILD: addon.need_build,
|
||||
@@ -237,6 +244,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_AUTH_API: addon.access_auth_api,
|
||||
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
||||
ATTR_GPIO: addon.with_gpio,
|
||||
ATTR_USB: addon.with_usb,
|
||||
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||
ATTR_DEVICETREE: addon.with_devicetree,
|
||||
ATTR_UDEV: addon.with_udev,
|
||||
@@ -245,6 +253,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_AUDIO: addon.with_audio,
|
||||
ATTR_AUDIO_INPUT: None,
|
||||
ATTR_AUDIO_OUTPUT: None,
|
||||
ATTR_STARTUP: addon.startup,
|
||||
ATTR_SERVICES: _pretty_services(addon),
|
||||
ATTR_DISCOVERY: addon.discovery,
|
||||
ATTR_IP_ADDRESS: None,
|
||||
@@ -253,12 +262,13 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_INGRESS_URL: None,
|
||||
ATTR_INGRESS_PORT: None,
|
||||
ATTR_INGRESS_PANEL: None,
|
||||
ATTR_WATCHDOG: None,
|
||||
}
|
||||
|
||||
if isinstance(addon, Addon) and addon.is_installed:
|
||||
data.update(
|
||||
{
|
||||
ATTR_STATE: await addon.state(),
|
||||
ATTR_STATE: addon.state,
|
||||
ATTR_WEBUI: addon.webui,
|
||||
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
||||
ATTR_INGRESS_URL: addon.ingress_url,
|
||||
@@ -269,6 +279,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_AUTO_UPDATE: addon.auto_update,
|
||||
ATTR_IP_ADDRESS: str(addon.ip_address),
|
||||
ATTR_VERSION: addon.version,
|
||||
ATTR_WATCHDOG: addon.watchdog,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -280,7 +291,7 @@ class APIAddons(CoreSysAttributes):
|
||||
addon = self._extract_addon_installed(request)
|
||||
|
||||
# Update secrets for validation
|
||||
await self.sys_secrets.reload()
|
||||
await self.sys_homeassistant.secrets.reload()
|
||||
|
||||
# Extend schema with add-on specific validation
|
||||
addon_schema = SCHEMA_OPTIONS.extend(
|
||||
@@ -304,9 +315,24 @@ class APIAddons(CoreSysAttributes):
|
||||
if ATTR_INGRESS_PANEL in body:
|
||||
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
|
||||
await self.sys_ingress.update_hass_panel(addon)
|
||||
if ATTR_WATCHDOG in body:
|
||||
addon.watchdog = body[ATTR_WATCHDOG]
|
||||
|
||||
addon.save_persist()
|
||||
|
||||
@api_process
|
||||
async def options_validate(self, request: web.Request) -> None:
|
||||
"""Validate user options for add-on."""
|
||||
addon = self._extract_addon_installed(request)
|
||||
data = {ATTR_MESSAGE: "", ATTR_VALID: True}
|
||||
try:
|
||||
addon.schema(addon.options)
|
||||
except vol.Invalid as ex:
|
||||
data[ATTR_MESSAGE] = humanize_error(addon.options, ex)
|
||||
data[ATTR_VALID] = False
|
||||
|
||||
return data
|
||||
|
||||
@api_process
|
||||
async def security(self, request: web.Request) -> None:
|
||||
"""Store security options for add-on."""
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Init file for Supervisor hardware RESTful API."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Awaitable, Dict
|
||||
from typing import Any, Awaitable, Dict, List
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
@@ -12,6 +12,7 @@ from ..const import (
|
||||
ATTR_INPUT,
|
||||
ATTR_OUTPUT,
|
||||
ATTR_SERIAL,
|
||||
ATTR_USB,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .utils import api_process
|
||||
@@ -25,13 +26,24 @@ class APIHardware(CoreSysAttributes):
|
||||
@api_process
|
||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Show hardware info."""
|
||||
serial: List[str] = []
|
||||
|
||||
# Create Serial list with device links
|
||||
for device in self.sys_hardware.serial_devices:
|
||||
serial.append(device.path.as_posix())
|
||||
for link in device.links:
|
||||
serial.append(link.as_posix())
|
||||
|
||||
return {
|
||||
ATTR_SERIAL: list(
|
||||
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id
|
||||
),
|
||||
ATTR_SERIAL: serial,
|
||||
ATTR_INPUT: list(self.sys_hardware.input_devices),
|
||||
ATTR_DISK: list(self.sys_hardware.disk_devices),
|
||||
ATTR_DISK: [
|
||||
device.path.as_posix() for device in self.sys_hardware.disk_devices
|
||||
],
|
||||
ATTR_GPIO: list(self.sys_hardware.gpio_devices),
|
||||
ATTR_USB: [
|
||||
device.path.as_posix() for device in self.sys_hardware.usb_devices
|
||||
],
|
||||
ATTR_AUDIO: self.sys_hardware.audio_devices,
|
||||
}
|
||||
|
||||
|
@@ -117,7 +117,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
||||
@api_process
|
||||
async def stats(self, request: web.Request) -> Dict[Any, str]:
|
||||
"""Return resource information."""
|
||||
stats = await self.sys_homeassistant.stats()
|
||||
stats = await self.sys_homeassistant.core.stats()
|
||||
if not stats:
|
||||
raise APIError("No stats available")
|
||||
|
||||
@@ -138,36 +138,36 @@ class APIHomeAssistant(CoreSysAttributes):
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
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
|
||||
def stop(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Stop Home Assistant."""
|
||||
return asyncio.shield(self.sys_homeassistant.stop())
|
||||
return asyncio.shield(self.sys_homeassistant.core.stop())
|
||||
|
||||
@api_process
|
||||
def start(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Start Home Assistant."""
|
||||
return asyncio.shield(self.sys_homeassistant.start())
|
||||
return asyncio.shield(self.sys_homeassistant.core.start())
|
||||
|
||||
@api_process
|
||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Restart Home Assistant."""
|
||||
return asyncio.shield(self.sys_homeassistant.restart())
|
||||
return asyncio.shield(self.sys_homeassistant.core.restart())
|
||||
|
||||
@api_process
|
||||
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Rebuild Home Assistant."""
|
||||
return asyncio.shield(self.sys_homeassistant.rebuild())
|
||||
return asyncio.shield(self.sys_homeassistant.core.rebuild())
|
||||
|
||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||
"""Return Home Assistant Docker logs."""
|
||||
return self.sys_homeassistant.logs()
|
||||
return self.sys_homeassistant.core.logs()
|
||||
|
||||
@api_process
|
||||
async def check(self, request: web.Request) -> None:
|
||||
"""Check configuration of Home Assistant."""
|
||||
result = await self.sys_homeassistant.check_config()
|
||||
result = await self.sys_homeassistant.core.check_config()
|
||||
if not result.valid:
|
||||
raise APIError(result.log)
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""Init file for Supervisor host RESTful API."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
@@ -11,6 +10,9 @@ from ..const import (
|
||||
ATTR_CPE,
|
||||
ATTR_DEPLOYMENT,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DISK_FREE,
|
||||
ATTR_DISK_TOTAL,
|
||||
ATTR_DISK_USED,
|
||||
ATTR_FEATURES,
|
||||
ATTR_HOSTNAME,
|
||||
ATTR_KERNEL,
|
||||
@@ -23,8 +25,6 @@ from ..const import (
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
SERVICE = "service"
|
||||
|
||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)})
|
||||
@@ -39,11 +39,14 @@ class APIHost(CoreSysAttributes):
|
||||
return {
|
||||
ATTR_CHASSIS: self.sys_host.info.chassis,
|
||||
ATTR_CPE: self.sys_host.info.cpe,
|
||||
ATTR_FEATURES: self.sys_host.supperted_features,
|
||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
||||
ATTR_DISK_FREE: self.sys_host.info.free_space,
|
||||
ATTR_DISK_TOTAL: self.sys_host.info.total_space,
|
||||
ATTR_DISK_USED: self.sys_host.info.used_space,
|
||||
ATTR_FEATURES: self.sys_host.supported_features,
|
||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||
ATTR_KERNEL: self.sys_host.info.kernel,
|
||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||
}
|
||||
|
||||
@api_process
|
||||
|
@@ -39,8 +39,8 @@ class APIInfo(CoreSysAttributes):
|
||||
ATTR_MACHINE: self.sys_machine,
|
||||
ATTR_ARCH: self.sys_arch.default,
|
||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||
ATTR_SUPPORTED: self.sys_supported,
|
||||
ATTR_SUPPORTED: self.sys_core.supported,
|
||||
ATTR_CHANNEL: self.sys_updater.channel,
|
||||
ATTR_LOGGING: self.sys_config.logging,
|
||||
ATTR_TIMEZONE: self.sys_timezone,
|
||||
ATTR_TIMEZONE: self.sys_config.timezone,
|
||||
}
|
||||
|
@@ -104,7 +104,7 @@ class APIIngress(CoreSysAttributes):
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Ingress error: %s", err)
|
||||
|
||||
raise HTTPBadGateway() from None
|
||||
raise HTTPBadGateway()
|
||||
|
||||
async def _handle_websocket(
|
||||
self, request: web.Request, addon: Addon, path: 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.reload())
|
||||
|
||||
return await asyncio.shield(self.interface_info(request))
|
@@ -1,9 +1,9 @@
|
||||
|
||||
try {
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.855567b9.js')")();
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.e3f59ee9.js')")();
|
||||
} catch (err) {
|
||||
var el = document.createElement('script');
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.19035830.js';
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.6086a806.js';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.0a106e488ed654ffce49.js","sources":["webpack:///chunk.0a106e488ed654ffce49.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
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.2b590ee397502865577d.js","sources":["webpack:///chunk.2b590ee397502865577d.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.2b6daa2d9335da34f364.js","sources":["webpack:///chunk.2b6daa2d9335da34f364.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
|
||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.2d61d2e5cad84705d92e.js","sources":["webpack:///chunk.2d61d2e5cad84705d92e.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.41e6f35d848b66d50d12.js","sources":["webpack:///chunk.41e6f35d848b66d50d12.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.
@@ -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.6d5d163209aca7b3c446.js","sources":["webpack:///chunk.6d5d163209aca7b3c446.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":""}
|
@@ -0,0 +1,2 @@
|
||||
(self.webpackJsonp=self.webpackJsonp||[]).push([[1],{175: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(164),o=n.n(a),s=n(171),t=(n(172),n(173),n(11));o.a.commands.save=function(e){Object(t.a)(e.getWrapperElement(),"editor-save")};var c=o.a,i=s.a}}]);
|
||||
//# sourceMappingURL=chunk.bb770cb9ffe5dd250148.js.map
|
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.bb770cb9ffe5dd250148.js","sources":["webpack:///chunk.bb770cb9ffe5dd250148.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.cddd872dbf23dca66986.js","sources":["webpack:///chunk.cddd872dbf23dca66986.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.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.d7c7c8539f98733903e7.js","sources":["webpack:///chunk.d7c7c8539f98733903e7.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.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"entrypoint.19035830.js","sources":["webpack:///entrypoint.19035830.js"],"mappings":";AAAA","sourceRoot":""}
|
3
supervisor/api/panel/frontend_es5/entrypoint.6086a806.js
Normal file
3
supervisor/api/panel/frontend_es5/entrypoint.6086a806.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.
|
||||
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.
|
||||
***************************************************************************** */
|
||||
|
||||
/**
|
||||
* @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
|
||||
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||
@@ -104,6 +129,23 @@ and limitations under the License.
|
||||
* 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
|
||||
* Copyright 2019 Google Inc.
|
BIN
supervisor/api/panel/frontend_es5/entrypoint.6086a806.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/entrypoint.6086a806.js.gz
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"entrypoint.6086a806.js","sources":["webpack:///entrypoint.6086a806.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.6086a806.js"
|
||||
}
|
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
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
|
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.5066cbaab136d2561122.js","sources":["webpack:///chunk.5066cbaab136d2561122.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.5533de14161bf4721cab.js","sources":["webpack:///chunk.5533de14161bf4721cab.js"],"mappings":"AAAA;AA0OA;AACA;AACA;AANA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +1 @@
|
||||
{"version":3,"file":"chunk.c108fa5618a9a08e44f8.js","sources":["webpack:///chunk.c108fa5618a9a08e44f8.js"],"mappings":"AAAA;;;;AAwNA;AACA;;;AAGA;AACA;AACA;;;;AAIA;AACA;;AAIA;;AAEA;;;AAGA;;AAGA;AACA;;AAEA;;;;AAKA;AACA;;;AAGA;;AAGA;AACA;;AAEA;;;;AAKA;AACA;;;;;AAKA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;;;;AAKA;;;AAGA;;;AAGA;;AAEA;;;AAGA;;;AAGA;AACA;AACA;;;;AAzFA;;;;;;;;;;;;;;;;;;;;;;;;;AA+HA","sourceRoot":""}
|
||||
{"version":3,"file":"chunk.64b648e525a4fd321c8a.js","sources":["webpack:///chunk.64b648e525a4fd321c8a.js"],"mappings":"AAAA;;;;AAqOA;AACA;;;AAGA;AACA;AACA;;;;AAIA;AACA;;AAIA;;AAEA;;;AAGA;;AAGA;AACA;;AAEA;;;;AAKA;AACA;;;AAGA;;AAGA;AACA;;AAEA;;;;AAKA;AACA;;;;;AAKA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;;;;AAKA;;;AAGA;;;AAGA;;AAEA;;;AAGA;;;AAGA;AACA;AACA;;;;AAzFA;;;;;;;;;;;;;;;;;;;;;;;;;AA+HA","sourceRoot":""}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user