Compare commits

...

61 Commits
0.68 ... 0.75

Author SHA1 Message Date
Pascal Vizeli
6c75957578 Fix version merge conflict 2017-11-24 22:18:13 +01:00
Pascal Vizeli
3a8307acfe Fix panel (#258) 2017-11-24 21:54:56 +01:00
Pascal Vizeli
f20c7d42ee Update home-assistant to version 0.58.1 2017-11-24 14:13:16 +01:00
Pascal Vizeli
9419fbff94 Add new pannel (#257) 2017-11-24 14:01:17 +01:00
Pascal Vizeli
3ac6c03637 Update Hass.io to 0.75 2017-11-22 16:46:14 +01:00
Pascal Vizeli
a95274f1b3 Use tini for home-assistant docker (#255) 2017-11-21 15:55:10 +01:00
Markus
9d2fb87cec typo (#254) 2017-11-20 13:48:04 +01:00
Paulus Schoutsen
ce9c3565b6 Hassio panel split (#249)
* Allow serving two types of panels

* Remove old panel

* Make backwards compatible

* Add comment
2017-11-20 13:44:04 +01:00
Pascal Vizeli
b0ec58ed1b Update Home-Assistant to 0.57.3 2017-11-13 20:47:36 +01:00
Pascal Vizeli
893a5f8dd3 Update Home-Assistant to 0.57.3 2017-11-13 18:44:12 +01:00
Pascal Vizeli
98064f6a90 Update Home-Assistant to 0.57.2 2017-11-06 11:25:25 +01:00
Pascal Vizeli
5146f89354 Update Home-Assistant to 0.57.2 2017-11-06 07:24:04 +01:00
Pascal Vizeli
fb46592d48 Pump version to 0.75 2017-11-05 00:35:04 +01:00
Pascal Vizeli
b4fb5ac681 Fix merge conflict 2017-11-05 00:32:17 +01:00
Pascal Vizeli
4b7201dc59 Update Home-Assistant to 0.57.1 2017-11-05 00:13:11 +01:00
Pascal Vizeli
3a5a4e4c27 Update hass.io to 0.74 2017-11-05 00:12:49 +01:00
Pascal Vizeli
70104a9280 Set api token for access requirements (#238)
* Set api token for access requirements

* fix uuid

* make robust

* fix names
2017-11-05 00:07:49 +01:00
Pascal Vizeli
efbc7b17a1 Use init system for add-ons (#237)
* Use init system for add-ons

* Update const.py

* Update validate.py

* Update addon.py

* Update addon.py

* remove options

* remove options p2

* remove options p3

* Update addon.py
2017-11-04 21:52:41 +01:00
Pascal Vizeli
64c5e20fc4 Update Home-Assistant to 0.57 2017-11-04 11:43:04 +01:00
Pascal Vizeli
13498afa97 Update Home-Assistant to 0.57 2017-11-04 11:33:55 +01:00
Pascal Vizeli
f6375f1bd6 Pump version to 0.74 2017-10-25 12:12:13 +02:00
pvizeli
8fd1599173 Fix merge conflict version 2017-10-25 12:09:33 +02:00
Pascal Vizeli
63302b73b0 Update hass.io to 0.73 2017-10-25 12:01:27 +02:00
Pascal Vizeli
f591f67a2a Show hardware GPIO interface (#233)
* Update hardware.py

* Update host.py

* Update API.md

* Update API.md

* fix lint
2017-10-25 11:50:00 +02:00
Pascal Vizeli
cda3184a55 Add support for legacy mode (#232)
* Add support for legacy mode

* Update const.py

* add legacy mode

* Update addon.py

* Update addon.py

* Update addon.py

* Update addon.py
2017-10-24 17:23:33 +02:00
Pascal Vizeli
afc811e975 Update Home-Assistant to 0.56.2 2017-10-24 06:59:19 +02:00
Pascal Vizeli
2e169dcb42 Update Home-Assistant to 0.56.2 2017-10-24 00:08:57 +02:00
Pascal Vizeli
34e24e184f Update Home-Assistant to 0.56.1 2017-10-23 00:26:49 +02:00
Pascal Vizeli
2e4751ed7d Update version.json 2017-10-23 00:16:38 +02:00
Pascal Vizeli
8c82c467d4 Fix aiohttp 2.3.1 (#231) 2017-10-22 14:05:45 +02:00
Pascal Vizeli
f3f6771534 Add a static entry for hassio api (#230) 2017-10-22 13:53:41 +02:00
Pascal Vizeli
0a75a4dcbc Update Home-Assistant to 0.56 2017-10-22 11:16:51 +02:00
Pascal Vizeli
1a4542fc4e Update Home-Assistant to 0.56 2017-10-22 10:34:31 +02:00
Pascal Vizeli
7e0525749e Pump version to 0.73 2017-10-17 22:49:35 +02:00
Pascal Vizeli
b33b26018d fix version conflict 2017-10-17 22:47:20 +02:00
Pascal Vizeli
66c93e7176 Minimize downtime to 1 sec (#223) 2017-10-17 22:25:29 +02:00
Pascal Vizeli
5674d32bad Update hass.io version 0.72 2017-10-17 21:54:10 +02:00
Pascal Vizeli
7a84972770 Better close/loop handling (#221)
* Better close/loop handling

* Update bootstrap.py

* Update __main__.py

* Update core.py

* Update __main__.py

* Update __main__.py

* Update supervisor.py

* Update supervisor.py

* Update const.py

* fix lint
2017-10-17 16:04:37 +02:00
Pascal Vizeli
638f0f5371 Update Home-Assistant to 0.55.2 2017-10-17 00:07:42 +02:00
Pascal Vizeli
dca1b6f1d3 Update Home-Assistant to 0.55.2 2017-10-17 00:02:17 +02:00
Pascal Vizeli
2b0ee109d6 Rollback Home-Assistant 0.55.1 2017-10-16 19:06:17 +02:00
Pascal Vizeli
e7430d87d7 Update docker image validate (#220) 2017-10-16 17:15:09 +02:00
Pascal Vizeli
9751c1de79 Update Home-Assistant to 0.55.1 2017-10-16 15:50:48 +02:00
Pascal Vizeli
c497167b64 Update Home-Assistant to 0.55.1 2017-10-16 15:50:08 +02:00
Pascal Vizeli
7fb2aca88b Pump version to 0.72 2017-10-13 22:28:15 +02:00
Pascal Vizeli
0d544845b1 Update hass.io to version 0.71 2017-10-13 22:14:11 +02:00
Pascal Vizeli
602eb472f9 Allow to set a option als optional (#218)
* Update validate.py

* Update validate.py

* Update validate.py

* Update validate.py

* Update validate.py

* fix bug

* Extend schema

* Update validate.py

* fix lint

* Update validate.py

* Update validate.py

* Fix deepmerge

* Update setup.py

* Update validate.py
2017-10-13 22:02:41 +02:00
Pascal Vizeli
f22fa46bdb Pump version to 0.71 2017-10-10 07:24:35 +02:00
Pascal Vizeli
4171a28260 Update Hass.io to 0.70 2017-10-10 07:10:01 +02:00
Pascal Vizeli
55365a631a Bugfix weburl (#217) 2017-10-10 07:08:56 +02:00
Pascal Vizeli
547415b30b Pump version to 0.70 2017-10-09 15:38:49 +02:00
pvizeli
cbf79f1fab Fix version merge conflict 2017-10-09 15:25:30 +02:00
Pascal Vizeli
31cc1dce82 Update Hass.io to version 0.69 2017-10-09 15:15:30 +02:00
Pascal Vizeli
8a11e6c845 Check if a option is missing inside nested lists (#216)
* Update validate.py

* fix lint
2017-10-09 14:08:29 +02:00
Pascal Vizeli
2df4f80aa5 More log output (#214)
* Update snapshot.py

* Update __init__.py

* Update snapshot.py

* Update snapshot.py

* fix lint
2017-10-09 13:30:15 +02:00
Pascal Vizeli
68566ee9e1 Update homeassistant.py (#215) 2017-10-09 13:21:21 +02:00
Pascal Vizeli
fe04b7ec59 Remove dedicated API calls (#212)
* Update addons.py

* Update API.md

* Update addon.py

* Update addon.py

* Update addons.py
2017-10-09 10:48:17 +02:00
Pascal Vizeli
38f96d7ddd Remove unknown options from input (#213)
* Update validate.py

* Update validate.py

* Cleanup unneeded code
2017-10-09 10:09:43 +02:00
Pascal Vizeli
2b2edd6e98 Update Home-Assistant to version 0.55 2017-10-08 09:49:06 +02:00
Pascal Vizeli
361969aca2 Update Home-Assistant to version 0.55 2017-10-08 09:43:07 +02:00
Pascal Vizeli
e61e7f41f2 Pump version to 0.69 2017-10-03 17:37:48 +02:00
25 changed files with 354 additions and 226 deletions

17
API.md
View File

@@ -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.

View File

@@ -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)

View File

@@ -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:

View File

@@ -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))

View File

@@ -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()

View File

@@ -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)

View File

@@ -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,
} }

View File

@@ -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")

View File

@@ -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'

View File

@@ -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)

View File

@@ -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,

View File

@@ -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={

View File

@@ -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

View File

@@ -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."""

View File

@@ -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):

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -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))

View File

@@ -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)

View File

@@ -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'
] ]
) )

View File

@@ -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",