Merge remote-tracking branch 'origin/dev'

This commit is contained in:
Pascal Vizeli 2018-01-18 23:39:57 +01:00
commit cb15602814
21 changed files with 144 additions and 108 deletions

5
API.md
View File

@ -111,7 +111,8 @@ Output is the raw docker log.
{
"slug": "SLUG",
"date": "ISO",
"name": "Custom name"
"name": "Custom name",
"type": "full|partial"
}
]
}
@ -417,7 +418,7 @@ Get all available addons.
}
```
For reset custom network/audio settings, set it `null`.
Reset custom network/audio/options, set it `null`.
- POST `/addons/{addon}/start`

View File

@ -87,25 +87,25 @@ class Addon(CoreSysAttributes):
ATTR_OPTIONS: {},
ATTR_VERSION: version,
}
self._data.save()
self._data.save_data()
def _set_uninstall(self):
"""Set addon as uninstalled."""
self._data.system.pop(self._id, None)
self._data.user.pop(self._id, None)
self._data.save()
self._data.save_data()
def _set_update(self, version):
"""Update version of addon."""
self._data.system[self._id] = deepcopy(self._data.cache[self._id])
self._data.user[self._id][ATTR_VERSION] = version
self._data.save()
self._data.save_data()
def _restore_data(self, user, system):
"""Restore data to addon."""
self._data.user[self._id] = deepcopy(user)
self._data.system[self._id] = deepcopy(system)
self._data.save()
self._data.save_data()
@property
def options(self):
@ -120,8 +120,10 @@ class Addon(CoreSysAttributes):
@options.setter
def options(self, value):
"""Store user addon options."""
self._data.user[self._id][ATTR_OPTIONS] = deepcopy(value)
self._data.save()
if value is None:
self._data.user[self._id][ATTR_OPTIONS] = {}
else:
self._data.user[self._id][ATTR_OPTIONS] = deepcopy(value)
@property
def boot(self):
@ -134,7 +136,6 @@ class Addon(CoreSysAttributes):
def boot(self, value):
"""Store user boot options."""
self._data.user[self._id][ATTR_BOOT] = value
self._data.save()
@property
def auto_update(self):
@ -147,7 +148,6 @@ class Addon(CoreSysAttributes):
def auto_update(self, value):
"""Set auto update."""
self._data.user[self._id][ATTR_AUTO_UPDATE] = value
self._data.save()
@property
def name(self):
@ -225,8 +225,6 @@ class Addon(CoreSysAttributes):
self._data.user[self._id][ATTR_NETWORK] = new_ports
self._data.save()
@property
def webui(self):
"""Return URL to webui or None."""
@ -347,7 +345,6 @@ class Addon(CoreSysAttributes):
self._data.user[self._id].pop(ATTR_AUDIO_OUTPUT, None)
else:
self._data.user[self._id][ATTR_AUDIO_OUTPUT] = value
self._data.save()
@property
def audio_input(self):
@ -367,7 +364,6 @@ class Addon(CoreSysAttributes):
self._data.user[self._id].pop(ATTR_AUDIO_INPUT, None)
else:
self._data.user[self._id][ATTR_AUDIO_INPUT] = value
self._data.save()
@property
def url(self):
@ -448,6 +444,10 @@ class Addon(CoreSysAttributes):
"""Return path to addon changelog."""
return Path(self.path_location, 'CHANGELOG.md')
def save_data(self):
"""Save data of addon."""
self._addons.data.save_data()
def write_options(self):
"""Return True if addon options is written to data."""
schema = self.schema
@ -455,10 +455,14 @@ class Addon(CoreSysAttributes):
try:
schema(options)
return write_json_file(self.path_options, options)
write_json_file(self.path_options, options)
except vol.Invalid as ex:
_LOGGER.error("Addon %s have wrong options: %s", self._id,
humanize_error(options, ex))
except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Addon %s can't write options: %s", self._id, err)
else:
return True
return False
@ -654,8 +658,10 @@ class Addon(CoreSysAttributes):
}
# store local configs/state
if not write_json_file(Path(temp, "addon.json"), data):
_LOGGER.error("Can't write addon.json for %s", self._id)
try:
write_json_file(Path(temp, "addon.json"), data)
except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Can't save meta for %s: %s", self._id, err)
return False
# write into tarfile

View File

@ -10,18 +10,23 @@ from ..utils.json import JsonConfig
class AddonBuild(JsonConfig, CoreSysAttributes):
"""Handle build options for addons."""
def __init__(self, coresys, addon):
def __init__(self, coresys, slug):
"""Initialize addon builder."""
self.coresys = coresys
self.addon = addon
self._id = slug
super().__init__(
Path(addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG)
Path(self.addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG)
def save(self):
def save_data(self):
"""Ignore save function."""
pass
@property
def addon(self):
"""Return addon of build data."""
return self._addons.get(self._id)
@property
def base_image(self):
"""Base images for this addon."""

View File

@ -159,4 +159,4 @@ class Data(JsonConfig, CoreSysAttributes):
have_change = True
if have_change:
self.save()
self.save_data()

View File

@ -101,7 +101,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_AUTO_UART, default=False): vol.Boolean(),
vol.Optional(ATTR_TMPFS):
vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"),
vol.Optional(ATTR_MAP, default=[]): [vol.Match(RE_VOLUME)],
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
@ -141,7 +141,7 @@ SCHEMA_BUILD_CONFIG = vol.Schema({
vol.In(ARCH_ALL): vol.Match(r"(?:^[\w{}]+/)?[\-\w{}]+:[\.\-\w{}]+$"),
}),
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
vol.Optional(ATTR_ARGS, default={}): vol.Schema({
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({
vol.Coerce(str): vol.Coerce(str)
}),
}, extra=vol.REMOVE_EXTRA)
@ -152,7 +152,7 @@ SCHEMA_ADDON_USER = vol.Schema({
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): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]),
@ -169,10 +169,10 @@ SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend({
SCHEMA_ADDON_FILE = vol.Schema({
vol.Optional(ATTR_USER, default={}): {
vol.Optional(ATTR_USER, default=dict): {
vol.Coerce(str): SCHEMA_ADDON_USER,
},
vol.Optional(ATTR_SYSTEM, default={}): {
vol.Optional(ATTR_SYSTEM, default=dict): {
vol.Coerce(str): SCHEMA_ADDON_SYSTEM,
}
})

View File

@ -140,7 +140,7 @@ class APIAddons(CoreSysAttributes):
addon = self._extract_addon(request)
addon_schema = SCHEMA_OPTIONS.extend({
vol.Optional(ATTR_OPTIONS): addon.schema,
vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema),
})
body = await api_validate(addon_schema, request)
@ -158,6 +158,7 @@ class APIAddons(CoreSysAttributes):
if ATTR_AUDIO_OUTPUT in body:
addon.audio_output = body[ATTR_AUDIO_OUTPUT]
addon.save_data()
return True
@api_process

View File

@ -11,7 +11,7 @@ from ..const import (
ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX,
ATTR_BLK_READ, ATTR_BLK_WRITE, CONTENT_TYPE_BINARY)
from ..coresys import CoreSysAttributes
from ..validate import NETWORK_PORT
from ..validate import NETWORK_PORT, DOCKER_IMAGE
_LOGGER = logging.getLogger(__name__)
@ -22,7 +22,7 @@ SCHEMA_OPTIONS = vol.Schema({
vol.Inclusive(ATTR_IMAGE, 'custom_hass'):
vol.Any(None, vol.Coerce(str)),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'):
vol.Any(None, vol.Coerce(str)),
vol.Any(None, DOCKER_IMAGE),
vol.Optional(ATTR_PORT): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_SSL): vol.Boolean(),
@ -75,7 +75,7 @@ class APIHomeAssistant(CoreSysAttributes):
if ATTR_WATCHDOG in body:
self._homeassistant.watchdog = body[ATTR_WATCHDOG]
self._homeassistant.save()
self._homeassistant.save_data()
return True
@api_process

View File

@ -49,6 +49,7 @@ class APIHost(CoreSysAttributes):
if ATTR_AUDIO_INPUT in body:
self._config.audio_input = body[ATTR_AUDIO_INPUT]
self._config.save_data()
return True
@api_process_hostcontrol

View File

@ -18,8 +18,10 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
SCHEMA_RESTORE_PARTIAL = vol.Schema({
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
vol.Optional(ATTR_ADDONS): [vol.Coerce(str)],
vol.Optional(ATTR_FOLDERS): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS):
vol.All([vol.Coerce(str)], vol.Unique()),
vol.Optional(ATTR_FOLDERS):
vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
})
SCHEMA_SNAPSHOT_FULL = vol.Schema({
@ -27,8 +29,10 @@ SCHEMA_SNAPSHOT_FULL = vol.Schema({
})
SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({
vol.Optional(ATTR_ADDONS): [vol.Coerce(str)],
vol.Optional(ATTR_FOLDERS): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS):
vol.All([vol.Coerce(str)], vol.Unique()),
vol.Optional(ATTR_FOLDERS):
vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
})
@ -51,6 +55,7 @@ class APISnapshots(CoreSysAttributes):
ATTR_SLUG: snapshot.slug,
ATTR_NAME: snapshot.name,
ATTR_DATE: snapshot.date,
ATTR_TYPE: snapshot.sys_type,
})
return {

View File

@ -13,14 +13,14 @@ from ..const import (
ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ,
ATTR_BLK_WRITE, CONTENT_TYPE_BINARY)
from ..coresys import CoreSysAttributes
from ..validate import validate_timezone, WAIT_BOOT
from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES
_LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({
# pylint: disable=no-value-for-parameter
vol.Optional(ATTR_BETA_CHANNEL): vol.Boolean(),
vol.Optional(ATTR_ADDONS_REPOSITORIES): [vol.Url()],
vol.Optional(ATTR_ADDONS_REPOSITORIES): REPOSITORIES,
vol.Optional(ATTR_TIMEZONE): validate_timezone,
vol.Optional(ATTR_WAIT_BOOT): WAIT_BOOT,
})
@ -84,8 +84,8 @@ class APISupervisor(CoreSysAttributes):
new = set(body[ATTR_ADDONS_REPOSITORIES])
await asyncio.shield(self._addons.load_repositories(new))
self._updater.save()
self._config.save()
self._updater.save_data()
self._config.save_data()
return True
@api_process

View File

@ -45,7 +45,6 @@ class CoreConfig(JsonConfig):
def timezone(self, value):
"""Set system timezone."""
self._data[ATTR_TIMEZONE] = value
self.save()
@property
def wait_boot(self):
@ -56,7 +55,6 @@ class CoreConfig(JsonConfig):
def wait_boot(self, value):
"""Set wait boot time."""
self._data[ATTR_WAIT_BOOT] = value
self.save()
@property
def last_boot(self):
@ -72,7 +70,6 @@ class CoreConfig(JsonConfig):
def last_boot(self, value):
"""Set last boot datetime."""
self._data[ATTR_LAST_BOOT] = value.isoformat()
self.save()
@property
def path_hassio(self):
@ -170,7 +167,6 @@ class CoreConfig(JsonConfig):
return
self._data[ATTR_ADDONS_CUSTOM_LIST].append(repo)
self.save()
def drop_addon_repository(self, repo):
"""Remove a custom repository from list."""
@ -178,7 +174,6 @@ class CoreConfig(JsonConfig):
return
self._data[ATTR_ADDONS_CUSTOM_LIST].remove(repo)
self.save()
@property
def audio_output(self):
@ -189,7 +184,6 @@ class CoreConfig(JsonConfig):
def audio_output(self, value):
"""Set ALSA audio output card,dev."""
self._data[ATTR_AUDIO_OUTPUT] = value
self.save()
@property
def audio_input(self):
@ -200,4 +194,3 @@ class CoreConfig(JsonConfig):
def audio_input(self, value):
"""Set ALSA audio input card,dev."""
self._data[ATTR_AUDIO_INPUT] = value
self.save()

View File

@ -2,7 +2,7 @@
from pathlib import Path
from ipaddress import ip_network
HASSIO_VERSION = '0.82'
HASSIO_VERSION = '0.83'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/{}/version.json')

View File

@ -26,7 +26,7 @@ class DockerAddon(DockerInterface):
@property
def addon(self):
"""Return name of docker image."""
"""Return addon of docker image."""
return self._addons.get(self._id)
@property
@ -269,7 +269,7 @@ class DockerAddon(DockerInterface):
Need run inside executor.
"""
build_env = AddonBuild(self.coresys, self.addon)
build_env = AddonBuild(self.coresys, self._id)
_LOGGER.info("Start build %s:%s", self.image, tag)
try:

View File

@ -53,7 +53,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
def api_port(self, value):
"""Set network port for home-assistant instance."""
self._data[ATTR_PORT] = value
self.save()
@property
def api_password(self):

View File

@ -112,14 +112,15 @@ class SnapshotsManager(CoreSysAttributes):
_LOGGER.info("Full-Snapshot %s store folders", snapshot.slug)
await snapshot.store_folders()
_LOGGER.info("Full-Snapshot %s done", snapshot.slug)
self.snapshots_obj[snapshot.slug] = snapshot
return True
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Full-Snapshot %s error: %s", snapshot.slug, err)
return False
else:
_LOGGER.info("Full-Snapshot %s done", snapshot.slug)
self.snapshots_obj[snapshot.slug] = snapshot
return True
finally:
self._scheduler.suspend = False
self.lock.release()
@ -157,14 +158,15 @@ class SnapshotsManager(CoreSysAttributes):
snapshot.slug, folders)
await snapshot.store_folders(folders)
_LOGGER.info("Partial-Snapshot %s done", snapshot.slug)
self.snapshots_obj[snapshot.slug] = snapshot
return True
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Partial-Snapshot %s error: %s", snapshot.slug, err)
return False
else:
_LOGGER.info("Partial-Snapshot %s done", snapshot.slug)
self.snapshots_obj[snapshot.slug] = snapshot
return True
finally:
self._scheduler.suspend = False
self.lock.release()
@ -229,7 +231,7 @@ class SnapshotsManager(CoreSysAttributes):
if addon:
tasks.append(addon.uninstall())
else:
_LOGGER.warning("Can't remove addon %s", slug)
_LOGGER.warning("Can't remove addon %s", snapshot.slug)
for slug in restore_addons:
addon = self._addons.get(slug)
@ -249,13 +251,14 @@ class SnapshotsManager(CoreSysAttributes):
await task_hass
await self._homeassistant.run()
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Full-Restore %s error: %s", snapshot.slug, err)
return False
else:
_LOGGER.info("Full-Restore %s done", snapshot.slug)
return True
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Full-Restore %s error: %s", slug, err)
return False
finally:
self._scheduler.suspend = False
self.lock.release()
@ -298,7 +301,8 @@ class SnapshotsManager(CoreSysAttributes):
if addon:
tasks.append(snapshot.export_addon(addon))
else:
_LOGGER.warning("Can't restore addon %s", slug)
_LOGGER.warning("Can't restore addon %s",
snapshot.slug)
if tasks:
_LOGGER.info("Partial-Restore %s run %d tasks",
@ -308,13 +312,14 @@ class SnapshotsManager(CoreSysAttributes):
# make sure homeassistant run agen
await self._homeassistant.run()
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Partial-Restore %s error: %s", snapshot.slug, err)
return False
else:
_LOGGER.info("Partial-Restore %s done", snapshot.slug)
return True
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Partial-Restore %s error: %s", slug, err)
return False
finally:
self._scheduler.suspend = False
self.lock.release()

View File

@ -244,12 +244,13 @@ class Snapshot(CoreSysAttributes):
with tarfile.open(self.tar_file, "w:") as tar:
tar.add(self._tmp.name, arcname=".")
if write_json_file(Path(self._tmp.name, "snapshot.json"), self._data):
try:
write_json_file(Path(self._tmp.name, "snapshot.json"), self._data)
await self._loop.run_in_executor(None, _create_snapshot)
else:
_LOGGER.error("Can't write snapshot.json")
self._tmp.cleanup()
except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Can't write snapshot: %s", err)
finally:
self._tmp.cleanup()
async def import_addon(self, addon):
"""Add a addon into snapshot."""
@ -280,7 +281,7 @@ class Snapshot(CoreSysAttributes):
async def store_folders(self, folder_list=None):
"""Backup hassio data into snapshot."""
folder_list = folder_list or ALL_FOLDERS
folder_list = set(folder_list or ALL_FOLDERS)
def _folder_save(name):
"""Intenal function to snapshot a folder."""
@ -293,8 +294,8 @@ class Snapshot(CoreSysAttributes):
with tarfile.open(snapshot_tar, "w:gz",
compresslevel=1) as tar_file:
tar_file.add(origin_dir, arcname=".")
_LOGGER.info("Snapshot folder %s done", name)
_LOGGER.info("Snapshot folder %s done", name)
self._data[ATTR_FOLDERS].append(name)
except (tarfile.TarError, OSError) as err:
_LOGGER.warning("Can't snapshot folder %s: %s", name, err)
@ -307,7 +308,7 @@ class Snapshot(CoreSysAttributes):
async def restore_folders(self, folder_list=None):
"""Backup hassio data into snapshot."""
folder_list = folder_list or ALL_FOLDERS
folder_list = set(folder_list or self.folders)
def _folder_restore(name):
"""Intenal function to restore a folder."""
@ -323,7 +324,7 @@ class Snapshot(CoreSysAttributes):
_LOGGER.info("Restore folder %s", name)
with tarfile.open(snapshot_tar, "r:gz") as tar_file:
tar_file.extractall(path=origin_dir)
_LOGGER.info("Restore folder %s done", name)
_LOGGER.info("Restore folder %s done", name)
except (tarfile.TarError, OSError) as err:
_LOGGER.warning("Can't restore folder %s: %s", name, err)
@ -365,7 +366,7 @@ class Snapshot(CoreSysAttributes):
self._homeassistant.api_password = self.homeassistant_password
# save
self._homeassistant.save()
self._homeassistant.save_data()
def store_repositories(self):
"""Store repository list into snapshot."""

View File

@ -9,31 +9,42 @@ from ..const import (
ATTR_LAST_VERSION,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
from ..validate import NETWORK_PORT
from ..validate import NETWORK_PORT, REPOSITORIES, DOCKER_IMAGE
ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL]
def unique_addons(addons_list):
"""Validate that a add-on is unique."""
single = set([addon[ATTR_SLUG] for addon in addons_list])
if len(single) != len(addons_list):
raise vol.Invalid("Invalid addon list on snapshot!")
return addons_list
# pylint: disable=no-value-for-parameter
SCHEMA_SNAPSHOT = vol.Schema({
vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_TYPE): vol.In([SNAPSHOT_FULL, SNAPSHOT_PARTIAL]),
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_DATE): vol.Coerce(str),
vol.Optional(ATTR_HOMEASSISTANT, default={}): vol.Schema({
vol.Optional(ATTR_HOMEASSISTANT, default=dict): vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_IMAGE): vol.Coerce(str),
vol.Optional(ATTR_LAST_VERSION): vol.Coerce(str),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
}, extra=vol.REMOVE_EXTRA),
vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({
vol.Optional(ATTR_FOLDERS, default=list):
vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
vol.Optional(ATTR_ADDONS, default=list): vol.All([vol.Schema({
vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_VERSION): vol.Coerce(str),
}, extra=vol.REMOVE_EXTRA)],
vol.Optional(ATTR_REPOSITORIES, default=[]): [vol.Url()],
}, extra=vol.REMOVE_EXTRA)], unique_addons),
vol.Optional(ATTR_REPOSITORIES, default=list): REPOSITORIES,
}, extra=vol.ALLOW_EXTRA)

View File

@ -89,4 +89,4 @@ class Updater(JsonConfig, CoreSysAttributes):
# update versions
self._data[ATTR_HOMEASSISTANT] = data.get('homeassistant')
self._data[ATTR_HASSIO] = data.get('hassio')
self.save()
self.save_data()

View File

@ -10,14 +10,9 @@ _LOGGER = logging.getLogger(__name__)
def write_json_file(jsonfile, data):
"""Write a json file."""
try:
json_str = json.dumps(data, indent=2)
with jsonfile.open('w') as conf_file:
conf_file.write(json_str)
except (OSError, json.JSONDecodeError):
return False
return True
json_str = json.dumps(data, indent=2)
with jsonfile.open('w') as conf_file:
conf_file.write(json_str)
def read_json_file(jsonfile):
@ -35,7 +30,10 @@ class JsonConfig(object):
self._schema = schema
self._data = {}
# init or load data
self.read_data()
def read_data(self):
"""Read json file & validate."""
if self._file.is_file():
try:
self._data = read_json_file(self._file)
@ -43,27 +41,33 @@ class JsonConfig(object):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate
# Validate
try:
self._data = self._schema(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse %s: %s",
self._file, humanize_error(self._data, ex))
# reset data to default
# Reset data to default
_LOGGER.warning("Reset %s to default", self._file)
self._data = self._schema({})
def save(self):
def save_data(self):
"""Store data to config file."""
# validate
# Validate
try:
self._data = self._schema(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse data: %s",
humanize_error(self._data, ex))
return False
# Load last valid data
_LOGGER.warning("Reset %s to last version", self._file)
self.save_data()
return
# write
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
try:
write_json_file(self._file, self._data)
except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Can't store config in %s: %s", self._file, err)

View File

@ -14,6 +14,10 @@ from .const import (
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
ALSA_CHANNEL = vol.Match(r"\d+,\d+")
WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
# pylint: disable=no-value-for-parameter
REPOSITORIES = vol.All([vol.Url()], vol.Unique())
def validate_timezone(timezone):
@ -62,7 +66,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
vol.Match(r"^[0-9a-f]{32}$"),
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
@ -85,7 +89,7 @@ SCHEMA_HASSIO_CONFIG = vol.Schema({
vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str),
vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[
"https://github.com/hassio-addons/repository",
]): [vol.Url()],
]): REPOSITORIES,
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT,

View File

@ -1,5 +1,5 @@
{
"hassio": "0.82",
"hassio": "0.83",
"homeassistant": "0.61.1",
"resinos": "1.1",
"resinhup": "0.3",