mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-09-04 02:44:54 +00:00
Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6c75957578 | ||
![]() |
3a8307acfe | ||
![]() |
f20c7d42ee | ||
![]() |
9419fbff94 | ||
![]() |
3ac6c03637 | ||
![]() |
a95274f1b3 | ||
![]() |
9d2fb87cec | ||
![]() |
ce9c3565b6 | ||
![]() |
b0ec58ed1b | ||
![]() |
893a5f8dd3 | ||
![]() |
98064f6a90 | ||
![]() |
5146f89354 | ||
![]() |
fb46592d48 | ||
![]() |
b4fb5ac681 | ||
![]() |
4b7201dc59 | ||
![]() |
3a5a4e4c27 | ||
![]() |
70104a9280 | ||
![]() |
efbc7b17a1 | ||
![]() |
64c5e20fc4 | ||
![]() |
13498afa97 | ||
![]() |
f6375f1bd6 | ||
![]() |
8fd1599173 | ||
![]() |
63302b73b0 | ||
![]() |
f591f67a2a | ||
![]() |
cda3184a55 | ||
![]() |
afc811e975 | ||
![]() |
2e169dcb42 | ||
![]() |
34e24e184f | ||
![]() |
2e4751ed7d | ||
![]() |
8c82c467d4 | ||
![]() |
f3f6771534 | ||
![]() |
0a75a4dcbc | ||
![]() |
1a4542fc4e | ||
![]() |
7e0525749e | ||
![]() |
b33b26018d | ||
![]() |
66c93e7176 | ||
![]() |
5674d32bad | ||
![]() |
7a84972770 | ||
![]() |
638f0f5371 | ||
![]() |
dca1b6f1d3 | ||
![]() |
2b0ee109d6 | ||
![]() |
e7430d87d7 | ||
![]() |
9751c1de79 | ||
![]() |
c497167b64 | ||
![]() |
7fb2aca88b | ||
![]() |
0d544845b1 | ||
![]() |
602eb472f9 | ||
![]() |
f22fa46bdb | ||
![]() |
4171a28260 | ||
![]() |
55365a631a | ||
![]() |
547415b30b | ||
![]() |
cbf79f1fab | ||
![]() |
31cc1dce82 | ||
![]() |
8a11e6c845 | ||
![]() |
2df4f80aa5 | ||
![]() |
68566ee9e1 | ||
![]() |
fe04b7ec59 | ||
![]() |
38f96d7ddd | ||
![]() |
2b2edd6e98 | ||
![]() |
361969aca2 | ||
![]() |
e61e7f41f2 |
17
API.md
17
API.md
@@ -243,6 +243,7 @@ Optional:
|
|||||||
"serial": ["/dev/xy"],
|
"serial": ["/dev/xy"],
|
||||||
"input": ["Input device name"],
|
"input": ["Input device name"],
|
||||||
"disk": ["/dev/sdax"],
|
"disk": ["/dev/sdax"],
|
||||||
|
"gpio": ["gpiochip0", "gpiochip100"],
|
||||||
"audio": {
|
"audio": {
|
||||||
"CARD_ID": {
|
"CARD_ID": {
|
||||||
"name": "xy",
|
"name": "xy",
|
||||||
@@ -430,26 +431,10 @@ For reset custom network/audio settings, set it `null`.
|
|||||||
|
|
||||||
- POST `/addons/{addon}/install`
|
- POST `/addons/{addon}/install`
|
||||||
|
|
||||||
Optional:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "VERSION"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/addons/{addon}/uninstall`
|
- POST `/addons/{addon}/uninstall`
|
||||||
|
|
||||||
- POST `/addons/{addon}/update`
|
- POST `/addons/{addon}/update`
|
||||||
|
|
||||||
Optional:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "VERSION"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- GET `/addons/{addon}/logs`
|
- GET `/addons/{addon}/logs`
|
||||||
|
|
||||||
Output is the raw Docker log.
|
Output is the raw Docker log.
|
||||||
|
@@ -13,11 +13,12 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bootstrap.initialize_logging()
|
bootstrap.initialize_logging()
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
if not bootstrap.check_environment():
|
if not bootstrap.check_environment():
|
||||||
exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
# init executor pool
|
||||||
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
||||||
loop.set_default_executor(executor)
|
loop.set_default_executor(executor)
|
||||||
|
|
||||||
@@ -27,19 +28,20 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
bootstrap.migrate_system_env(config)
|
bootstrap.migrate_system_env(config)
|
||||||
|
|
||||||
_LOGGER.info("Run Hassio setup")
|
_LOGGER.info("Setup HassIO")
|
||||||
loop.run_until_complete(hassio.setup())
|
loop.run_until_complete(hassio.setup())
|
||||||
|
|
||||||
_LOGGER.info("Start Hassio")
|
|
||||||
loop.call_soon_threadsafe(loop.create_task, hassio.start())
|
loop.call_soon_threadsafe(loop.create_task, hassio.start())
|
||||||
loop.call_soon_threadsafe(bootstrap.reg_signal, loop, hassio)
|
loop.call_soon_threadsafe(bootstrap.reg_signal, loop)
|
||||||
|
|
||||||
_LOGGER.info("Run Hassio loop")
|
try:
|
||||||
loop.run_forever()
|
_LOGGER.info("Run HassIO")
|
||||||
|
loop.run_forever()
|
||||||
_LOGGER.info("Cleanup system")
|
finally:
|
||||||
executor.shutdown(wait=False)
|
_LOGGER.info("Stopping HassIO")
|
||||||
loop.close()
|
loop.run_until_complete(hassio.stop())
|
||||||
|
executor.shutdown(wait=False)
|
||||||
|
loop.close()
|
||||||
|
|
||||||
_LOGGER.info("Close Hassio")
|
_LOGGER.info("Close Hassio")
|
||||||
sys.exit(hassio.exit_code)
|
sys.exit(0)
|
||||||
|
@@ -8,7 +8,6 @@ import shutil
|
|||||||
import tarfile
|
import tarfile
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from deepmerge import Merger
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
@@ -18,11 +17,11 @@ from ..const import (
|
|||||||
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
|
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
|
||||||
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
|
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
|
||||||
ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT,
|
ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT,
|
||||||
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP,
|
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP, ATTR_UUID,
|
||||||
STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM,
|
STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM,
|
||||||
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI,
|
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI,
|
||||||
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
|
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
|
||||||
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN)
|
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY)
|
||||||
from .util import check_installed
|
from .util import check_installed
|
||||||
from ..dock.addon import DockerAddon
|
from ..dock.addon import DockerAddon
|
||||||
from ..tools import write_json_file, read_json_file
|
from ..tools import write_json_file, read_json_file
|
||||||
@@ -33,8 +32,6 @@ RE_WEBUI = re.compile(
|
|||||||
r"^(?:(?P<s_prefix>https?)|\[PROTO:(?P<t_proto>\w+)\])"
|
r"^(?:(?P<s_prefix>https?)|\[PROTO:(?P<t_proto>\w+)\])"
|
||||||
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$")
|
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$")
|
||||||
|
|
||||||
MERGE_OPT = Merger([(dict, ['merge'])], ['override'], ['override'])
|
|
||||||
|
|
||||||
|
|
||||||
class Addon(object):
|
class Addon(object):
|
||||||
"""Hold data for addon inside HassIO."""
|
"""Hold data for addon inside HassIO."""
|
||||||
@@ -109,10 +106,10 @@ class Addon(object):
|
|||||||
def options(self):
|
def options(self):
|
||||||
"""Return options with local changes."""
|
"""Return options with local changes."""
|
||||||
if self.is_installed:
|
if self.is_installed:
|
||||||
return MERGE_OPT.merge(
|
return {
|
||||||
self.data.system[self._id][ATTR_OPTIONS],
|
**self.data.system[self._id][ATTR_OPTIONS],
|
||||||
self.data.user[self._id][ATTR_OPTIONS],
|
**self.data.user[self._id][ATTR_OPTIONS]
|
||||||
)
|
}
|
||||||
return self.data.cache[self._id][ATTR_OPTIONS]
|
return self.data.cache[self._id][ATTR_OPTIONS]
|
||||||
|
|
||||||
@options.setter
|
@options.setter
|
||||||
@@ -156,6 +153,12 @@ class Addon(object):
|
|||||||
"""Return timeout of addon for docker stop."""
|
"""Return timeout of addon for docker stop."""
|
||||||
return self._mesh[ATTR_TIMEOUT]
|
return self._mesh[ATTR_TIMEOUT]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_token(self):
|
||||||
|
"""Return a API token for this add-on."""
|
||||||
|
if self.is_installed:
|
||||||
|
return self.data.user[self._id][ATTR_UUID]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
"""Return description of addon."""
|
"""Return description of addon."""
|
||||||
@@ -219,9 +222,9 @@ class Addon(object):
|
|||||||
|
|
||||||
# search host port for this docker port
|
# search host port for this docker port
|
||||||
if self.ports is None:
|
if self.ports is None:
|
||||||
port = self.ports.get("{}/tcp".format(t_port), t_port)
|
|
||||||
else:
|
|
||||||
port = t_port
|
port = t_port
|
||||||
|
else:
|
||||||
|
port = self.ports.get("{}/tcp".format(t_port), t_port)
|
||||||
|
|
||||||
# for interface config or port lists
|
# for interface config or port lists
|
||||||
if isinstance(port, (tuple, list)):
|
if isinstance(port, (tuple, list)):
|
||||||
@@ -260,6 +263,11 @@ class Addon(object):
|
|||||||
"""Return list of privilege."""
|
"""Return list of privilege."""
|
||||||
return self._mesh.get(ATTR_PRIVILEGED)
|
return self._mesh.get(ATTR_PRIVILEGED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def legacy(self):
|
||||||
|
"""Return if the add-on don't support hass labels."""
|
||||||
|
return self._mesh.get(ATTR_LEGACY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_hassio_api(self):
|
def access_hassio_api(self):
|
||||||
"""Return True if the add-on access to hassio api."""
|
"""Return True if the add-on access to hassio api."""
|
||||||
@@ -447,7 +455,7 @@ class Addon(object):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def install(self, version=None):
|
async def install(self):
|
||||||
"""Install a addon."""
|
"""Install a addon."""
|
||||||
if self.config.arch not in self.supported_arch:
|
if self.config.arch not in self.supported_arch:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
@@ -463,11 +471,10 @@ class Addon(object):
|
|||||||
"Create Home-Assistant addon data folder %s", self.path_data)
|
"Create Home-Assistant addon data folder %s", self.path_data)
|
||||||
self.path_data.mkdir()
|
self.path_data.mkdir()
|
||||||
|
|
||||||
version = version or self.last_version
|
if not await self.docker.install(self.last_version):
|
||||||
if not await self.docker.install(version):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._set_install(version)
|
self._set_install(self.last_version)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
@@ -510,19 +517,18 @@ class Addon(object):
|
|||||||
return self.docker.stop()
|
return self.docker.stop()
|
||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
async def update(self, version=None):
|
async def update(self):
|
||||||
"""Update addon."""
|
"""Update addon."""
|
||||||
version = version or self.last_version
|
|
||||||
last_state = await self.state()
|
last_state = await self.state()
|
||||||
|
|
||||||
if version == self.version_installed:
|
if self.last_version == self.version_installed:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Addon %s is already installed in %s", self._id, version)
|
"No update available for Addon %s", self._id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not await self.docker.update(version):
|
if not await self.docker.update(self.last_version):
|
||||||
return False
|
return False
|
||||||
self._set_update(version)
|
self._set_update(self.last_version)
|
||||||
|
|
||||||
# restore state
|
# restore state
|
||||||
if last_state == STATE_STARTED:
|
if last_state == STATE_STARTED:
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
"""Validate addons options schema."""
|
"""Validate addons options schema."""
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -11,12 +13,14 @@ from ..const import (
|
|||||||
ATTR_ARCH, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF,
|
ATTR_ARCH, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF,
|
||||||
ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED,
|
ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED,
|
||||||
ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED,
|
ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED,
|
||||||
ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK,
|
ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK, ATTR_UUID,
|
||||||
ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT,
|
ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT,
|
||||||
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
|
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
|
||||||
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN)
|
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY)
|
||||||
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL
|
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$")
|
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$")
|
||||||
|
|
||||||
@@ -35,7 +39,7 @@ RE_SCHEMA_ELEMENT = re.compile(
|
|||||||
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
||||||
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
|
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
|
||||||
r"|match\((?P<match>.*)\)"
|
r"|match\((?P<match>.*)\)"
|
||||||
r")$"
|
r")\??$"
|
||||||
)
|
)
|
||||||
|
|
||||||
SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
|
SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
|
||||||
@@ -99,15 +103,21 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
|
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
|
||||||
vol.Required(ATTR_OPTIONS): dict,
|
vol.Required(ATTR_OPTIONS): dict,
|
||||||
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
|
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
|
||||||
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
|
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
|
||||||
vol.Any(SCHEMA_ELEMENT, {vol.Coerce(str): SCHEMA_ELEMENT})
|
vol.Any(
|
||||||
], vol.Schema({vol.Coerce(str): SCHEMA_ELEMENT}))
|
SCHEMA_ELEMENT,
|
||||||
|
{vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])}
|
||||||
|
),
|
||||||
|
], vol.Schema({
|
||||||
|
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])
|
||||||
|
}))
|
||||||
}), False),
|
}), False),
|
||||||
vol.Optional(ATTR_IMAGE): vol.Match(r"^[\-\w{}]+/[\-\w{}]+$"),
|
vol.Optional(ATTR_IMAGE): vol.Match(r"^[\w{}]+/[\-\w{}]+$"),
|
||||||
vol.Optional(ATTR_TIMEOUT, default=10):
|
vol.Optional(ATTR_TIMEOUT, default=10):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=10, max=120))
|
vol.All(vol.Coerce(int), vol.Range(min=10, max=120)),
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
@@ -122,18 +132,20 @@ SCHEMA_REPOSITORY_CONFIG = vol.Schema({
|
|||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_BUILD_CONFIG = vol.Schema({
|
SCHEMA_BUILD_CONFIG = vol.Schema({
|
||||||
vol.Optional(ATTR_BUILD_FROM, default=BASE_IMAGE): vol.Schema({
|
vol.Optional(ATTR_BUILD_FROM, default=BASE_IMAGE): vol.Schema({
|
||||||
vol.In(ARCH_ALL): vol.Match(r"^[\-\w{}]+/[\-\w{}]+:[\-\w{}]+$"),
|
vol.In(ARCH_ALL): vol.Match(r"(?:^[\w{}]+/)?[\-\w{}]+:[\.\-\w{}]+$"),
|
||||||
}),
|
}),
|
||||||
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
|
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ARGS, default={}): vol.Schema({
|
vol.Optional(ATTR_ARGS, default={}): vol.Schema({
|
||||||
vol.Coerce(str): vol.Coerce(str)
|
vol.Coerce(str): vol.Coerce(str)
|
||||||
}),
|
}),
|
||||||
})
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_ADDON_USER = vol.Schema({
|
SCHEMA_ADDON_USER = vol.Schema({
|
||||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
||||||
|
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
|
||||||
|
vol.Match(r"^[0-9a-f]{32}$"),
|
||||||
vol.Optional(ATTR_OPTIONS, default={}): dict,
|
vol.Optional(ATTR_OPTIONS, default={}): dict,
|
||||||
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_BOOT):
|
vol.Optional(ATTR_BOOT):
|
||||||
@@ -141,7 +153,7 @@ SCHEMA_ADDON_USER = vol.Schema({
|
|||||||
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
|
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
|
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
|
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
|
||||||
})
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend({
|
SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend({
|
||||||
@@ -176,8 +188,10 @@ def validate_options(raw_schema):
|
|||||||
|
|
||||||
# read options
|
# read options
|
||||||
for key, value in struct.items():
|
for key, value in struct.items():
|
||||||
|
# Ignore unknown options / remove from list
|
||||||
if key not in raw_schema:
|
if key not in raw_schema:
|
||||||
raise vol.Invalid("Unknown options {}.".format(key))
|
_LOGGER.warning("Unknown options %s", key)
|
||||||
|
continue
|
||||||
|
|
||||||
typ = raw_schema[key]
|
typ = raw_schema[key]
|
||||||
try:
|
try:
|
||||||
@@ -194,6 +208,7 @@ def validate_options(raw_schema):
|
|||||||
raise vol.Invalid(
|
raise vol.Invalid(
|
||||||
"Type error for {}.".format(key)) from None
|
"Type error for {}.".format(key)) from None
|
||||||
|
|
||||||
|
_check_missing_options(raw_schema, options, 'root')
|
||||||
return options
|
return options
|
||||||
|
|
||||||
return validate
|
return validate
|
||||||
@@ -202,42 +217,38 @@ def validate_options(raw_schema):
|
|||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
def _single_validate(typ, value, key):
|
def _single_validate(typ, value, key):
|
||||||
"""Validate a single element."""
|
"""Validate a single element."""
|
||||||
try:
|
# if required argument
|
||||||
# if required argument
|
if value is None:
|
||||||
if value is None:
|
raise vol.Invalid("Missing required option '{}'.".format(key))
|
||||||
raise vol.Invalid("Missing required option '{}'.".format(key))
|
|
||||||
|
|
||||||
# parse extend data from type
|
# parse extend data from type
|
||||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||||
|
|
||||||
# prepare range
|
# prepare range
|
||||||
range_args = {}
|
range_args = {}
|
||||||
for group_name in ('i_min', 'i_max', 'f_min', 'f_max'):
|
for group_name in ('i_min', 'i_max', 'f_min', 'f_max'):
|
||||||
group_value = match.group(group_name)
|
group_value = match.group(group_name)
|
||||||
if group_value:
|
if group_value:
|
||||||
range_args[group_name[2:]] = float(group_value)
|
range_args[group_name[2:]] = float(group_value)
|
||||||
|
|
||||||
if typ.startswith(V_STR):
|
if typ.startswith(V_STR):
|
||||||
return str(value)
|
return str(value)
|
||||||
elif typ.startswith(V_INT):
|
elif typ.startswith(V_INT):
|
||||||
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
||||||
elif typ.startswith(V_FLOAT):
|
elif typ.startswith(V_FLOAT):
|
||||||
return vol.All(vol.Coerce(float), vol.Range(**range_args))(value)
|
return vol.All(vol.Coerce(float), vol.Range(**range_args))(value)
|
||||||
elif typ.startswith(V_BOOL):
|
elif typ.startswith(V_BOOL):
|
||||||
return vol.Boolean()(value)
|
return vol.Boolean()(value)
|
||||||
elif typ.startswith(V_EMAIL):
|
elif typ.startswith(V_EMAIL):
|
||||||
return vol.Email()(value)
|
return vol.Email()(value)
|
||||||
elif typ.startswith(V_URL):
|
elif typ.startswith(V_URL):
|
||||||
return vol.Url()(value)
|
return vol.Url()(value)
|
||||||
elif typ.startswith(V_PORT):
|
elif typ.startswith(V_PORT):
|
||||||
return NETWORK_PORT(value)
|
return NETWORK_PORT(value)
|
||||||
elif typ.startswith(V_MATCH):
|
elif typ.startswith(V_MATCH):
|
||||||
return vol.Match(match.group('match'))(str(value))
|
return vol.Match(match.group('match'))(str(value))
|
||||||
|
|
||||||
raise vol.Invalid("Fatal error for {} type {}".format(key, typ))
|
raise vol.Invalid("Fatal error for {} type {}".format(key, typ))
|
||||||
except ValueError:
|
|
||||||
raise vol.Invalid(
|
|
||||||
"Type {} error for '{}' on {}.".format(typ, value, key)) from None
|
|
||||||
|
|
||||||
|
|
||||||
def _nested_validate_list(typ, data_list, key):
|
def _nested_validate_list(typ, data_list, key):
|
||||||
@@ -245,17 +256,10 @@ def _nested_validate_list(typ, data_list, key):
|
|||||||
options = []
|
options = []
|
||||||
|
|
||||||
for element in data_list:
|
for element in data_list:
|
||||||
# dict list
|
# Nested?
|
||||||
if isinstance(typ, dict):
|
if isinstance(typ, dict):
|
||||||
c_options = {}
|
c_options = _nested_validate_dict(typ, element, key)
|
||||||
for c_key, c_value in element.items():
|
|
||||||
if c_key not in typ:
|
|
||||||
raise vol.Invalid(
|
|
||||||
"Unknown nested options {}".format(c_key))
|
|
||||||
|
|
||||||
c_options[c_key] = _single_validate(typ[c_key], c_value, c_key)
|
|
||||||
options.append(c_options)
|
options.append(c_options)
|
||||||
# normal list
|
|
||||||
else:
|
else:
|
||||||
options.append(_single_validate(typ, element, key))
|
options.append(_single_validate(typ, element, key))
|
||||||
|
|
||||||
@@ -267,9 +271,28 @@ def _nested_validate_dict(typ, data_dict, key):
|
|||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
for c_key, c_value in data_dict.items():
|
for c_key, c_value in data_dict.items():
|
||||||
|
# Ignore unknown options / remove from list
|
||||||
if c_key not in typ:
|
if c_key not in typ:
|
||||||
raise vol.Invalid("Unknow nested dict options {}".format(c_key))
|
_LOGGER.warning("Unknown options %s", c_key)
|
||||||
|
continue
|
||||||
|
|
||||||
options[c_key] = _single_validate(typ[c_key], c_value, c_key)
|
# Nested?
|
||||||
|
if isinstance(typ[c_key], list):
|
||||||
|
options[c_key] = _nested_validate_list(typ[c_key][0],
|
||||||
|
c_value, c_key)
|
||||||
|
else:
|
||||||
|
options[c_key] = _single_validate(typ[c_key], c_value, c_key)
|
||||||
|
|
||||||
|
_check_missing_options(typ, options, key)
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def _check_missing_options(origin, exists, root):
|
||||||
|
"""Check if all options are exists."""
|
||||||
|
missing = set(origin) - set(exists)
|
||||||
|
for miss_opt in missing:
|
||||||
|
if isinstance(origin[miss_opt], str) and \
|
||||||
|
origin[miss_opt].endswith("?"):
|
||||||
|
continue
|
||||||
|
raise vol.Invalid(
|
||||||
|
"Missing option {} in {}".format(miss_opt, root))
|
||||||
|
@@ -139,13 +139,18 @@ class RestAPI(object):
|
|||||||
|
|
||||||
def register_panel(self):
|
def register_panel(self):
|
||||||
"""Register panel for homeassistant."""
|
"""Register panel for homeassistant."""
|
||||||
panel = Path(__file__).parents[1].joinpath('panel/hassio-main.html')
|
def create_panel_response(build_type):
|
||||||
|
"""Create a function to generate a response."""
|
||||||
|
path = Path(__file__).parents[1].joinpath(
|
||||||
|
'panel/hassio-main-{}.html'.format(build_type))
|
||||||
|
|
||||||
def get_panel(request):
|
return lambda request: web.FileResponse(path)
|
||||||
"""Return file response with panel."""
|
|
||||||
return web.FileResponse(panel)
|
|
||||||
|
|
||||||
self.webapp.router.add_get('/panel', get_panel)
|
# This route is for backwards compatibility with HA < 0.58
|
||||||
|
self.webapp.router.add_get('/panel', create_panel_response('es5'))
|
||||||
|
self.webapp.router.add_get('/panel_es5', create_panel_response('es5'))
|
||||||
|
self.webapp.router.add_get(
|
||||||
|
'/panel_latest', create_panel_response('latest'))
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Run rest api webserver."""
|
"""Run rest api webserver."""
|
||||||
@@ -166,5 +171,5 @@ class RestAPI(object):
|
|||||||
await self.webapp.shutdown()
|
await self.webapp.shutdown()
|
||||||
|
|
||||||
if self._handler:
|
if self._handler:
|
||||||
await self._handler.finish_connections(60)
|
await self._handler.shutdown(60)
|
||||||
await self.webapp.cleanup()
|
await self.webapp.cleanup()
|
||||||
|
@@ -166,14 +166,10 @@ class APIAddons(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def install(self, request):
|
def install(self, request):
|
||||||
"""Install addon."""
|
"""Install addon."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
|
||||||
addon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request, check_installed=False)
|
||||||
version = body.get(ATTR_VERSION, addon.last_version)
|
return asyncio.shield(addon.install(), loop=self.loop)
|
||||||
|
|
||||||
return await asyncio.shield(
|
|
||||||
addon.install(version=version), loop=self.loop)
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def uninstall(self, request):
|
def uninstall(self, request):
|
||||||
@@ -202,17 +198,14 @@ class APIAddons(object):
|
|||||||
return asyncio.shield(addon.stop(), loop=self.loop)
|
return asyncio.shield(addon.stop(), loop=self.loop)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def update(self, request):
|
def update(self, request):
|
||||||
"""Update addon."""
|
"""Update addon."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
version = body.get(ATTR_VERSION, addon.last_version)
|
|
||||||
|
|
||||||
if version == addon.version_installed:
|
if addon.last_version == addon.version_installed:
|
||||||
raise RuntimeError("Version %s is already in use", version)
|
raise RuntimeError("No update available!")
|
||||||
|
|
||||||
return await asyncio.shield(
|
return asyncio.shield(addon.update(), loop=self.loop)
|
||||||
addon.update(version=version), loop=self.loop)
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request):
|
def restart(self, request):
|
||||||
@@ -253,4 +246,4 @@ class APIAddons(object):
|
|||||||
raise RuntimeError("STDIN not supported by addons")
|
raise RuntimeError("STDIN not supported by addons")
|
||||||
|
|
||||||
data = await request.read()
|
data = await request.read()
|
||||||
return asyncio.shield(addon.write_stdin(data), loop=self.loop)
|
return await asyncio.shield(addon.write_stdin(data), loop=self.loop)
|
||||||
|
@@ -8,7 +8,7 @@ from .util import api_process_hostcontrol, api_process, api_validate
|
|||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
|
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
|
||||||
ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO, ATTR_AUDIO_INPUT,
|
ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO, ATTR_AUDIO_INPUT,
|
||||||
ATTR_AUDIO_OUTPUT)
|
ATTR_AUDIO_OUTPUT, ATTR_GPIO)
|
||||||
from ..validate import ALSA_CHANNEL
|
from ..validate import ALSA_CHANNEL
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -83,8 +83,9 @@ class APIHost(object):
|
|||||||
async def hardware(self, request):
|
async def hardware(self, request):
|
||||||
"""Return local hardware infos."""
|
"""Return local hardware infos."""
|
||||||
return {
|
return {
|
||||||
ATTR_SERIAL: self.local_hw.serial_devices,
|
ATTR_SERIAL: list(self.local_hw.serial_devices),
|
||||||
ATTR_INPUT: self.local_hw.input_devices,
|
ATTR_INPUT: list(self.local_hw.input_devices),
|
||||||
ATTR_DISK: self.local_hw.disk_devices,
|
ATTR_DISK: list(self.local_hw.disk_devices),
|
||||||
|
ATTR_GPIO: list(self.local_hw.gpio_devices),
|
||||||
ATTR_AUDIO: self.local_hw.audio_devices,
|
ATTR_AUDIO: self.local_hw.audio_devices,
|
||||||
}
|
}
|
||||||
|
@@ -123,22 +123,22 @@ def check_environment():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def reg_signal(loop, hassio):
|
def reg_signal(loop):
|
||||||
"""Register SIGTERM, SIGKILL to stop system."""
|
"""Register SIGTERM, SIGKILL to stop system."""
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(
|
loop.add_signal_handler(
|
||||||
signal.SIGTERM, lambda: loop.create_task(hassio.stop()))
|
signal.SIGTERM, lambda: loop.call_soon(loop.stop))
|
||||||
except (ValueError, RuntimeError):
|
except (ValueError, RuntimeError):
|
||||||
_LOGGER.warning("Could not bind to SIGTERM")
|
_LOGGER.warning("Could not bind to SIGTERM")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(
|
loop.add_signal_handler(
|
||||||
signal.SIGHUP, lambda: loop.create_task(hassio.stop()))
|
signal.SIGHUP, lambda: loop.call_soon(loop.stop))
|
||||||
except (ValueError, RuntimeError):
|
except (ValueError, RuntimeError):
|
||||||
_LOGGER.warning("Could not bind to SIGHUP")
|
_LOGGER.warning("Could not bind to SIGHUP")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(
|
loop.add_signal_handler(
|
||||||
signal.SIGINT, lambda: loop.create_task(hassio.stop()))
|
signal.SIGINT, lambda: loop.call_soon(loop.stop))
|
||||||
except (ValueError, RuntimeError):
|
except (ValueError, RuntimeError):
|
||||||
_LOGGER.warning("Could not bind to SIGINT")
|
_LOGGER.warning("Could not bind to SIGINT")
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
|
|
||||||
HASSIO_VERSION = '0.68'
|
HASSIO_VERSION = '0.75'
|
||||||
|
|
||||||
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
|
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
|
||||||
'hassio/{}/version.json')
|
'hassio/{}/version.json')
|
||||||
@@ -20,8 +20,6 @@ RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
|
|||||||
RUN_WATCHDOG_HOMEASSISTANT_API = 300
|
RUN_WATCHDOG_HOMEASSISTANT_API = 300
|
||||||
RUN_CLEANUP_API_SESSIONS = 900
|
RUN_CLEANUP_API_SESSIONS = 900
|
||||||
|
|
||||||
RESTART_EXIT_CODE = 100
|
|
||||||
|
|
||||||
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
|
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
|
||||||
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
|
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
|
||||||
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
|
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
|
||||||
@@ -112,6 +110,7 @@ ATTR_HOMEASSISTANT = 'homeassistant'
|
|||||||
ATTR_HASSIO = 'hassio'
|
ATTR_HASSIO = 'hassio'
|
||||||
ATTR_HASSIO_API = 'hassio_api'
|
ATTR_HASSIO_API = 'hassio_api'
|
||||||
ATTR_HOMEASSISTANT_API = 'homeassistant_api'
|
ATTR_HOMEASSISTANT_API = 'homeassistant_api'
|
||||||
|
ATTR_UUID = 'uuid'
|
||||||
ATTR_FOLDERS = 'folders'
|
ATTR_FOLDERS = 'folders'
|
||||||
ATTR_SIZE = 'size'
|
ATTR_SIZE = 'size'
|
||||||
ATTR_TYPE = 'type'
|
ATTR_TYPE = 'type'
|
||||||
@@ -129,6 +128,7 @@ ATTR_SECURITY = 'security'
|
|||||||
ATTR_BUILD_FROM = 'build_from'
|
ATTR_BUILD_FROM = 'build_from'
|
||||||
ATTR_SQUASH = 'squash'
|
ATTR_SQUASH = 'squash'
|
||||||
ATTR_GPIO = 'gpio'
|
ATTR_GPIO = 'gpio'
|
||||||
|
ATTR_LEGACY = 'legacy'
|
||||||
ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list'
|
ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list'
|
||||||
|
|
||||||
STARTUP_INITIALIZE = 'initialize'
|
STARTUP_INITIALIZE = 'initialize'
|
||||||
|
@@ -177,7 +177,7 @@ class HassIO(object):
|
|||||||
if self.homeassistant.version == 'landingpage':
|
if self.homeassistant.version == 'landingpage':
|
||||||
self.loop.create_task(self.homeassistant.install())
|
self.loop.create_task(self.homeassistant.install())
|
||||||
|
|
||||||
async def stop(self, exit_code=0):
|
async def stop(self):
|
||||||
"""Stop a running orchestration."""
|
"""Stop a running orchestration."""
|
||||||
# don't process scheduler anymore
|
# don't process scheduler anymore
|
||||||
self.scheduler.suspend = True
|
self.scheduler.suspend = True
|
||||||
@@ -185,7 +185,6 @@ class HassIO(object):
|
|||||||
# process stop tasks
|
# process stop tasks
|
||||||
self.websession.close()
|
self.websession.close()
|
||||||
self.homeassistant.websession.close()
|
self.homeassistant.websession.close()
|
||||||
await asyncio.wait([self.api.stop(), self.dns.stop()], loop=self.loop)
|
|
||||||
|
|
||||||
self.exit_code = exit_code
|
# process async stop tasks
|
||||||
self.loop.stop()
|
await asyncio.wait([self.api.stop(), self.dns.stop()], loop=self.loop)
|
||||||
|
@@ -25,6 +25,21 @@ class DockerAddon(DockerInterface):
|
|||||||
config, loop, api, image=addon.image, timeout=addon.timeout)
|
config, loop, api, image=addon.image, timeout=addon.timeout)
|
||||||
self.addon = addon
|
self.addon = addon
|
||||||
|
|
||||||
|
def process_metadata(self, metadata, force=False):
|
||||||
|
"""Use addon data instead meta data with legacy."""
|
||||||
|
if not self.addon.legacy:
|
||||||
|
return super().process_metadata(metadata, force=force)
|
||||||
|
|
||||||
|
# set meta data
|
||||||
|
if not self.version or force:
|
||||||
|
if force: # called on install/update/build
|
||||||
|
self.version = self.addon.last_version
|
||||||
|
else:
|
||||||
|
self.version = self.addon.version_installed
|
||||||
|
|
||||||
|
if not self.arch:
|
||||||
|
self.arch = self.config.arch
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return name of docker container."""
|
"""Return name of docker container."""
|
||||||
@@ -45,6 +60,10 @@ class DockerAddon(DockerInterface):
|
|||||||
'ALSA_INPUT': self.addon.audio_input,
|
'ALSA_INPUT': self.addon.audio_input,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Set api token if any API access is needed
|
||||||
|
if self.addon.access_hassio_api or self.addon.access_homeassistant_api:
|
||||||
|
addon_env['API_TOKEN'] = self.addon.api_token
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**addon_env,
|
**addon_env,
|
||||||
'TZ': self.config.timezone,
|
'TZ': self.config.timezone,
|
||||||
@@ -89,6 +108,7 @@ class DockerAddon(DockerInterface):
|
|||||||
"""Return hosts mapping."""
|
"""Return hosts mapping."""
|
||||||
return {
|
return {
|
||||||
'homeassistant': self.docker.network.gateway,
|
'homeassistant': self.docker.network.gateway,
|
||||||
|
'hassio': self.docker.network.supervisor,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -172,6 +192,7 @@ class DockerAddon(DockerInterface):
|
|||||||
name=self.name,
|
name=self.name,
|
||||||
hostname=self.hostname,
|
hostname=self.hostname,
|
||||||
detach=True,
|
detach=True,
|
||||||
|
init=True,
|
||||||
stdin_open=self.addon.with_stdin,
|
stdin_open=self.addon.with_stdin,
|
||||||
network_mode=self.network_mode,
|
network_mode=self.network_mode,
|
||||||
ports=self.ports,
|
ports=self.ports,
|
||||||
|
@@ -52,6 +52,7 @@ class DockerHomeAssistant(DockerInterface):
|
|||||||
hostname=self.name,
|
hostname=self.name,
|
||||||
detach=True,
|
detach=True,
|
||||||
privileged=True,
|
privileged=True,
|
||||||
|
init=True,
|
||||||
devices=self.devices,
|
devices=self.devices,
|
||||||
network_mode='host',
|
network_mode='host',
|
||||||
environment={
|
environment={
|
||||||
|
@@ -6,7 +6,6 @@ import docker
|
|||||||
|
|
||||||
from .interface import DockerInterface
|
from .interface import DockerInterface
|
||||||
from .util import docker_process
|
from .util import docker_process
|
||||||
from ..const import RESTART_EXIT_CODE
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ class DockerSupervisor(DockerInterface):
|
|||||||
_LOGGER.info("Update supervisor docker to %s:%s", self.image, tag)
|
_LOGGER.info("Update supervisor docker to %s:%s", self.image, tag)
|
||||||
|
|
||||||
if await self.loop.run_in_executor(None, self._install, tag):
|
if await self.loop.run_in_executor(None, self._install, tag):
|
||||||
self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE))
|
self.loop.call_later(1, self.loop.stop)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@@ -19,6 +19,8 @@ RE_DEVICES = re.compile(r"\[.*(\d+)- (\d+).*\]: ([\w ]*)")
|
|||||||
PROC_STAT = Path("/proc/stat")
|
PROC_STAT = Path("/proc/stat")
|
||||||
RE_BOOT_TIME = re.compile(r"btime (\d+)")
|
RE_BOOT_TIME = re.compile(r"btime (\d+)")
|
||||||
|
|
||||||
|
GPIO_DEVICES = Path("/sys/class/gpio")
|
||||||
|
|
||||||
|
|
||||||
class Hardware(object):
|
class Hardware(object):
|
||||||
"""Represent a interface to procfs, sysfs and udev."""
|
"""Represent a interface to procfs, sysfs and udev."""
|
||||||
@@ -35,7 +37,7 @@ class Hardware(object):
|
|||||||
if 'ID_VENDOR' in device:
|
if 'ID_VENDOR' in device:
|
||||||
dev_list.add(device.device_node)
|
dev_list.add(device.device_node)
|
||||||
|
|
||||||
return list(dev_list)
|
return dev_list
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def input_devices(self):
|
def input_devices(self):
|
||||||
@@ -45,7 +47,7 @@ class Hardware(object):
|
|||||||
if 'NAME' in device:
|
if 'NAME' in device:
|
||||||
dev_list.add(device['NAME'].replace('"', ''))
|
dev_list.add(device['NAME'].replace('"', ''))
|
||||||
|
|
||||||
return list(dev_list)
|
return dev_list
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def disk_devices(self):
|
def disk_devices(self):
|
||||||
@@ -55,7 +57,7 @@ class Hardware(object):
|
|||||||
if device.device_node.startswith('/dev/sd'):
|
if device.device_node.startswith('/dev/sd'):
|
||||||
dev_list.add(device.device_node)
|
dev_list.add(device.device_node)
|
||||||
|
|
||||||
return list(dev_list)
|
return dev_list
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audio_devices(self):
|
def audio_devices(self):
|
||||||
@@ -90,6 +92,15 @@ class Hardware(object):
|
|||||||
|
|
||||||
return audio_list
|
return audio_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gpio_devices(self):
|
||||||
|
"""Return list of GPIO interface on device."""
|
||||||
|
dev_list = set()
|
||||||
|
for interface in GPIO_DEVICES.glob("gpio*"):
|
||||||
|
dev_list.add(interface.name)
|
||||||
|
|
||||||
|
return dev_list
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_boot(self):
|
def last_boot(self):
|
||||||
"""Return last boot time."""
|
"""Return last boot time."""
|
||||||
|
@@ -95,7 +95,7 @@ class HomeAssistant(JsonConfig):
|
|||||||
def watchdog(self, value):
|
def watchdog(self, value):
|
||||||
"""Return True if the watchdog should protect Home-Assistant."""
|
"""Return True if the watchdog should protect Home-Assistant."""
|
||||||
self._data[ATTR_WATCHDOG] = value
|
self._data[ATTR_WATCHDOG] = value
|
||||||
self._data.save()
|
self.save()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
|
75
hassio/panel/hassio-main-es5.html
Normal file
75
hassio/panel/hassio-main-es5.html
Normal file
File diff suppressed because one or more lines are too long
BIN
hassio/panel/hassio-main-es5.html.gz
Normal file
BIN
hassio/panel/hassio-main-es5.html.gz
Normal file
Binary file not shown.
75
hassio/panel/hassio-main-latest.html
Normal file
75
hassio/panel/hassio-main-latest.html
Normal file
File diff suppressed because one or more lines are too long
BIN
hassio/panel/hassio-main-latest.html.gz
Normal file
BIN
hassio/panel/hassio-main-latest.html.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -197,6 +197,8 @@ class SnapshotsManager(object):
|
|||||||
await snapshot.restore_folders()
|
await snapshot.restore_folders()
|
||||||
|
|
||||||
# start homeassistant restore
|
# start homeassistant restore
|
||||||
|
_LOGGER.info("Full-Restore %s restore Home-Assistant",
|
||||||
|
snapshot.slug)
|
||||||
snapshot.restore_homeassistant(self.homeassistant)
|
snapshot.restore_homeassistant(self.homeassistant)
|
||||||
task_hass = self.loop.create_task(
|
task_hass = self.loop.create_task(
|
||||||
self.homeassistant.update(snapshot.homeassistant_version))
|
self.homeassistant.update(snapshot.homeassistant_version))
|
||||||
@@ -279,6 +281,8 @@ class SnapshotsManager(object):
|
|||||||
await snapshot.restore_folders(folders)
|
await snapshot.restore_folders(folders)
|
||||||
|
|
||||||
if homeassistant:
|
if homeassistant:
|
||||||
|
_LOGGER.info("Partial-Restore %s restore Home-Assistant",
|
||||||
|
snapshot.slug)
|
||||||
snapshot.restore_homeassistant(self.homeassistant)
|
snapshot.restore_homeassistant(self.homeassistant)
|
||||||
tasks.append(self.homeassistant.update(
|
tasks.append(self.homeassistant.update(
|
||||||
snapshot.homeassistant_version))
|
snapshot.homeassistant_version))
|
||||||
|
@@ -261,7 +261,8 @@ class Snapshot(object):
|
|||||||
"""Async context to close a snapshot."""
|
"""Async context to close a snapshot."""
|
||||||
# exists snapshot or exception on build
|
# exists snapshot or exception on build
|
||||||
if self.tar_file.is_file() or exception_type is not None:
|
if self.tar_file.is_file() or exception_type is not None:
|
||||||
return self._tmp.cleanup()
|
self._tmp.cleanup()
|
||||||
|
return
|
||||||
|
|
||||||
# validate data
|
# validate data
|
||||||
try:
|
try:
|
||||||
@@ -283,7 +284,6 @@ class Snapshot(object):
|
|||||||
_LOGGER.error("Can't write snapshot.json")
|
_LOGGER.error("Can't write snapshot.json")
|
||||||
|
|
||||||
self._tmp.cleanup()
|
self._tmp.cleanup()
|
||||||
self._tmp = None
|
|
||||||
|
|
||||||
async def import_addon(self, addon):
|
async def import_addon(self, addon):
|
||||||
"""Add a addon into snapshot."""
|
"""Add a addon into snapshot."""
|
||||||
@@ -323,9 +323,11 @@ class Snapshot(object):
|
|||||||
origin_dir = Path(self.config.path_hassio, name)
|
origin_dir = Path(self.config.path_hassio, name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
_LOGGER.info("Snapshot folder %s", name)
|
||||||
with tarfile.open(snapshot_tar, "w:gz",
|
with tarfile.open(snapshot_tar, "w:gz",
|
||||||
compresslevel=1) as tar_file:
|
compresslevel=1) as tar_file:
|
||||||
tar_file.add(origin_dir, arcname=".")
|
tar_file.add(origin_dir, arcname=".")
|
||||||
|
_LOGGER.info("Snapshot folder %s done", name)
|
||||||
|
|
||||||
self._data[ATTR_FOLDERS].append(name)
|
self._data[ATTR_FOLDERS].append(name)
|
||||||
except tarfile.TarError as err:
|
except tarfile.TarError as err:
|
||||||
@@ -352,8 +354,10 @@ class Snapshot(object):
|
|||||||
remove_folder(origin_dir)
|
remove_folder(origin_dir)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
_LOGGER.info("Restore folder %s", name)
|
||||||
with tarfile.open(snapshot_tar, "r:gz") as tar_file:
|
with tarfile.open(snapshot_tar, "r:gz") as tar_file:
|
||||||
tar_file.extractall(path=origin_dir)
|
tar_file.extractall(path=origin_dir)
|
||||||
|
_LOGGER.info("Restore folder %s done", name)
|
||||||
except tarfile.TarError as err:
|
except tarfile.TarError as err:
|
||||||
_LOGGER.warning("Can't restore folder %s -> %s", name, err)
|
_LOGGER.warning("Can't restore folder %s -> %s", name, err)
|
||||||
|
|
||||||
|
5
setup.py
5
setup.py
@@ -12,7 +12,7 @@ setup(
|
|||||||
url='https://home-assistant.io/',
|
url='https://home-assistant.io/',
|
||||||
description=('Open-source private cloud os for Home-Assistant'
|
description=('Open-source private cloud os for Home-Assistant'
|
||||||
' based on ResinOS'),
|
' based on ResinOS'),
|
||||||
long_description=('A maintenainless private cloud operator system that'
|
long_description=('A maintainless private cloud operator system that'
|
||||||
'setup a Home-Assistant instance. Based on ResinOS'),
|
'setup a Home-Assistant instance. Based on ResinOS'),
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Intended Audience :: End Users/Desktop',
|
'Intended Audience :: End Users/Desktop',
|
||||||
@@ -47,7 +47,6 @@ setup(
|
|||||||
'pyotp',
|
'pyotp',
|
||||||
'pyqrcode',
|
'pyqrcode',
|
||||||
'pytz',
|
'pytz',
|
||||||
'pyudev',
|
'pyudev'
|
||||||
'deepmerge'
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"hassio": "0.68",
|
"hassio": "0.75",
|
||||||
"homeassistant": "0.54",
|
"homeassistant": "0.57.3",
|
||||||
"resinos": "1.1",
|
"resinos": "1.1",
|
||||||
"resinhup": "0.3",
|
"resinhup": "0.3",
|
||||||
"generic": "0.3",
|
"generic": "0.3",
|
||||||
|
Reference in New Issue
Block a user