Merge pull request #97 from home-assistant/dev

Release 0.43
This commit is contained in:
Pascal Vizeli 2017-07-12 02:31:19 +02:00 committed by GitHub
commit bfa7443ae2
20 changed files with 428 additions and 211 deletions

92
API.md
View File

@ -51,48 +51,13 @@ The addons from `addons` are only installed one.
], ],
"addons_repositories": [ "addons_repositories": [
"REPO_URL" "REPO_URL"
],
"snapshots": [
{
"slug": "SLUG",
"data": "ISO",
"name": "Custom name"
}
] ]
} }
``` ```
- GET `/supervisor/addons` - GET `/supervisor/addons`
Get all available addons Get all available addons. Will be delete soon. Look to `/addons`
```json
{
"addons": [
{
"name": "xy bla",
"slug": "xy",
"description": "description",
"arch": ["armhf", "aarch64", "i386", "amd64"],
"repository": "core|local|REP_ID",
"version": "LAST_VERSION",
"installed": "none|INSTALL_VERSION",
"detached": "bool",
"build": "bool",
"url": "null|url"
}
],
"repositories": [
{
"slug": "12345678",
"name": "Repitory Name|unknown",
"source": "URL_OF_REPOSITORY",
"url": "WEBSITE|REPOSITORY",
"maintainer": "BLA BLU <fla@dld.ch>|unknown"
}
]
}
```
- POST `/supervisor/update` - POST `/supervisor/update`
Optional: Optional:
@ -157,6 +122,21 @@ Return QR-Code
### Backup/Snapshot ### Backup/Snapshot
- GET `/snapshots`
```json
{
"snapshots": [
{
"slug": "SLUG",
"date": "ISO",
"name": "Custom name"
}
]
}
```
- POST `/snapshots/reload`
- POST `/snapshots/new/full` - POST `/snapshots/new/full`
```json ```json
{ {
@ -269,7 +249,9 @@ Optional:
{ {
"version": "INSTALL_VERSION", "version": "INSTALL_VERSION",
"last_version": "LAST_VERSION", "last_version": "LAST_VERSION",
"devices": [] "devices": [""],
"image": "str",
"custom": "bool -> if custom image"
} }
``` ```
@ -291,11 +273,47 @@ Output the raw docker log
```json ```json
{ {
"devices": [], "devices": [],
"image": "Optional|null",
"last_version": "Optional for custom image|null"
} }
``` ```
Image with `null` and last_version with `null` reset this options.
### REST API addons ### REST API addons
- GET `/addons`
Get all available addons
```json
{
"addons": [
{
"name": "xy bla",
"slug": "xy",
"description": "description",
"arch": ["armhf", "aarch64", "i386", "amd64"],
"repository": "core|local|REP_ID",
"version": "LAST_VERSION",
"installed": "none|INSTALL_VERSION",
"detached": "bool",
"build": "bool",
"url": "null|url"
}
],
"repositories": [
{
"slug": "12345678",
"name": "Repitory Name|unknown",
"source": "URL_OF_REPOSITORY",
"url": "WEBSITE|REPOSITORY",
"maintainer": "BLA BLU <fla@dld.ch>|unknown"
}
]
}
```
- POST `/addons/reload` - POST `/addons/reload`
- GET `/addons/{addon}/info` - GET `/addons/{addon}/info`

View File

@ -15,53 +15,22 @@ from .validate import (
from ..const import ( from ..const import (
FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON, FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON,
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM) REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM)
from ..tools import read_json_file, write_json_file from ..tools import JsonConfig, read_json_file
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(MAP_VOLUME) RE_VOLUME = re.compile(MAP_VOLUME)
class Data(object): class Data(JsonConfig):
"""Hold data for addons inside HassIO.""" """Hold data for addons inside HassIO."""
def __init__(self, config): def __init__(self, config):
"""Initialize data holder.""" """Initialize data holder."""
self._file = FILE_HASSIO_ADDONS super().__init__(FILE_HASSIO_ADDONS, SCHEMA_ADDON_FILE)
self._data = {}
self.config = config self.config = config
self._cache = {}
self._repositories = {} self._repositories = {}
self._cache = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate
try:
self._data = SCHEMA_ADDON_FILE(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse addons.json -> %s",
humanize_error(self._data, ex))
def save(self):
"""Store data to config file."""
# validate
try:
self._data = SCHEMA_ADDON_FILE(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse addons data -> %s",
humanize_error(self._data, ex))
return False
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
@property @property
def user(self): def user(self):

View File

@ -77,6 +77,9 @@ class RestAPI(object):
"""Register homeassistant function.""" """Register homeassistant function."""
api_addons = APIAddons(self.config, self.loop, addons) api_addons = APIAddons(self.config, self.loop, addons)
self.webapp.router.add_get('/addons', api_addons.list)
self.webapp.router.add_post('/addons/reload', api_addons.reload)
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info) self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
self.webapp.router.add_post( self.webapp.router.add_post(
'/addons/{addon}/install', api_addons.install) '/addons/{addon}/install', api_addons.install)
@ -105,6 +108,9 @@ class RestAPI(object):
"""Register snapshots function.""" """Register snapshots function."""
api_snapshots = APISnapshots(self.config, self.loop, snapshots) api_snapshots = APISnapshots(self.config, self.loop, snapshots)
self.webapp.router.add_get('/snapshots', api_snapshots.list)
self.webapp.router.add_post('/snapshots/reload', api_snapshots.reload)
self.webapp.router.add_post( self.webapp.router.add_post(
'/snapshots/new/full', api_snapshots.snapshot_full) '/snapshots/new/full', api_snapshots.snapshot_full)
self.webapp.router.add_post( self.webapp.router.add_post(

View File

@ -9,8 +9,9 @@ from .util import api_process, api_process_raw, api_validate
from ..const import ( from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY, ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY,
ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK, ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK, ATTR_SLUG,
BOOT_AUTO, BOOT_MANUAL) ATTR_SOURCE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_ARCH, ATTR_MAINTAINER,
ATTR_INSTALLED, BOOT_AUTO, BOOT_MANUAL)
from ..validate import DOCKER_PORTS from ..validate import DOCKER_PORTS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -47,6 +48,44 @@ class APIAddons(object):
return addon return addon
@api_process
async def list(self, request):
"""Return all addons / repositories ."""
data_addons = []
for addon in self.addons.list_addons:
data_addons.append({
ATTR_NAME: addon.name,
ATTR_SLUG: addon.slug,
ATTR_DESCRIPTON: addon.description,
ATTR_VERSION: addon.last_version,
ATTR_INSTALLED: addon.version_installed,
ATTR_ARCH: addon.supported_arch,
ATTR_DETACHED: addon.is_detached,
ATTR_REPOSITORY: addon.repository,
ATTR_BUILD: addon.need_build,
ATTR_URL: addon.url,
})
data_repositories = []
for repository in self.addons.list_repositories:
data_repositories.append({
ATTR_SLUG: repository.slug,
ATTR_NAME: repository.name,
ATTR_SOURCE: repository.source,
ATTR_URL: repository.url,
ATTR_MAINTAINER: repository.maintainer,
})
return {
ATTR_ADDONS: data_addons,
ATTR_REPOSITORIES: data_repositories,
}
@api_process
def reload(self, request):
"""Reload all addons data."""
return self.addons.reload()
@api_process @api_process
async def info(self, request): async def info(self, request):
"""Return addon information.""" """Return addon information."""

View File

@ -5,7 +5,8 @@ import logging
import voluptuous as vol import voluptuous as vol
from .util import api_process, api_process_raw, api_validate from .util import api_process, api_process_raw, api_validate
from ..const import ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM)
from ..validate import HASS_DEVICES from ..validate import HASS_DEVICES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -13,6 +14,9 @@ _LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({ SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_DEVICES): HASS_DEVICES, vol.Optional(ATTR_DEVICES): HASS_DEVICES,
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)),
}) })
SCHEMA_VERSION = vol.Schema({ SCHEMA_VERSION = vol.Schema({
@ -34,8 +38,10 @@ class APIHomeAssistant(object):
"""Return host information.""" """Return host information."""
return { return {
ATTR_VERSION: self.homeassistant.version, ATTR_VERSION: self.homeassistant.version,
ATTR_LAST_VERSION: self.config.last_homeassistant, ATTR_LAST_VERSION: self.homeassistant.last_version,
ATTR_DEVICES: self.config.homeassistant_devices, ATTR_IMAGE: self.homeassistant.image,
ATTR_DEVICES: self.homeassistant.devices,
ATTR_CUSTOM: self.homeassistant.is_custom_image,
} }
@api_process @api_process
@ -44,7 +50,11 @@ class APIHomeAssistant(object):
body = await api_validate(SCHEMA_OPTIONS, request) body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_DEVICES in body: if ATTR_DEVICES in body:
self.config.homeassistant_devices = body[ATTR_DEVICES] self.homeassistant.devices = body[ATTR_DEVICES]
if ATTR_IMAGE in body:
self.homeassistant.set_custom(
body[ATTR_IMAGE], body[ATTR_LAST_VERSION])
return True return True
@ -57,9 +67,6 @@ class APIHomeAssistant(object):
if self.homeassistant.in_progress: if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress") raise RuntimeError("Other task is in progress")
if version == self.homeassistant.version:
raise RuntimeError("Version is already in use")
return await asyncio.shield( return await asyncio.shield(
self.homeassistant.update(version), loop=self.loop) self.homeassistant.update(version), loop=self.loop)

View File

@ -9,7 +9,7 @@ from ..snapshots.validate import ALL_FOLDERS
from ..const import ( from ..const import (
ATTR_NAME, ATTR_SLUG, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES, ATTR_NAME, ATTR_SLUG, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES,
ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE, ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE,
ATTR_DEVICES) ATTR_DEVICES, ATTR_SNAPSHOTS)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -47,23 +47,39 @@ class APISnapshots(object):
raise RuntimeError("Snapshot not exists") raise RuntimeError("Snapshot not exists")
return snapshot return snapshot
@staticmethod @api_process
def _addons_list(snapshot): async def list(self, request):
"""Generate a list with addons data.""" """Return snapshot list."""
data = [] data_snapshots = []
for addon_data in snapshot.addons: for snapshot in self.snapshots.list_snapshots:
data.append({ data_snapshots.append({
ATTR_SLUG: addon_data[ATTR_SLUG], ATTR_SLUG: snapshot.slug,
ATTR_NAME: addon_data[ATTR_NAME], ATTR_NAME: snapshot.name,
ATTR_VERSION: addon_data[ATTR_VERSION], ATTR_DATE: snapshot.date,
}) })
return data
return {
ATTR_SNAPSHOTS: data_snapshots,
}
@api_process
def reload(self, request):
"""Reload snapshot list."""
return asyncio.shield(self.snapshots.reload(), loop=self.loop)
@api_process @api_process
async def info(self, request): async def info(self, request):
"""Return snapshot info.""" """Return snapshot info."""
snapshot = self._extract_snapshot(request) snapshot = self._extract_snapshot(request)
data_addons = []
for addon_data in snapshot.addons:
data_addons.append({
ATTR_SLUG: addon_data[ATTR_SLUG],
ATTR_NAME: addon_data[ATTR_NAME],
ATTR_VERSION: addon_data[ATTR_VERSION],
})
return { return {
ATTR_SLUG: snapshot.slug, ATTR_SLUG: snapshot.slug,
ATTR_TYPE: snapshot.sys_type, ATTR_TYPE: snapshot.sys_type,
@ -74,7 +90,7 @@ class APISnapshots(object):
ATTR_VERSION: snapshot.homeassistant_version, ATTR_VERSION: snapshot.homeassistant_version,
ATTR_DEVICES: snapshot.homeassistant_devices, ATTR_DEVICES: snapshot.homeassistant_devices,
}, },
ATTR_ADDONS: self._addons_list(snapshot), ATTR_ADDONS: data_addons,
ATTR_REPOSITORIES: snapshot.repositories, ATTR_REPOSITORIES: snapshot.repositories,
ATTR_FOLDERS: snapshot.folders, ATTR_FOLDERS: snapshot.folders,
} }

View File

@ -10,7 +10,7 @@ from ..const import (
HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_REPOSITORIES, HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_REPOSITORIES,
ATTR_REPOSITORY, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_REPOSITORY, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED,
ATTR_DETACHED, ATTR_SOURCE, ATTR_MAINTAINER, ATTR_URL, ATTR_ARCH, ATTR_DETACHED, ATTR_SOURCE, ATTR_MAINTAINER, ATTR_URL, ATTR_ARCH,
ATTR_BUILD, ATTR_TIMEZONE, ATTR_DATE, ATTR_SNAPSHOTS) ATTR_BUILD, ATTR_TIMEZONE)
from ..tools import validate_timezone from ..tools import validate_timezone
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -77,18 +77,6 @@ class APISupervisor(object):
return data return data
def _snapshots_list(self):
"""Return a list of available snapshots."""
data = []
for snapshot in self.snapshots.list_snapshots:
data.append({
ATTR_SLUG: snapshot.slug,
ATTR_NAME: snapshot.name,
ATTR_DATE: snapshot.date,
})
return data
@api_process @api_process
async def ping(self, request): async def ping(self, request):
"""Return ok for signal that the api is ready.""" """Return ok for signal that the api is ready."""
@ -105,7 +93,6 @@ class APISupervisor(object):
ATTR_TIMEZONE: self.config.timezone, ATTR_TIMEZONE: self.config.timezone,
ATTR_ADDONS: self._addons_list(only_installed=True), ATTR_ADDONS: self._addons_list(only_installed=True),
ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories, ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories,
ATTR_SNAPSHOTS: self._snapshots_list(),
} }
@api_process @api_process

View File

@ -1,17 +1,13 @@
"""Bootstrap HassIO.""" """Bootstrap HassIO."""
from datetime import datetime from datetime import datetime
import logging import logging
import json
import os import os
from pathlib import Path, PurePath from pathlib import Path, PurePath
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error
from .const import FILE_HASSIO_CONFIG, HASSIO_DATA from .const import FILE_HASSIO_CONFIG, HASSIO_DATA
from .tools import ( from .tools import fetch_last_versions, JsonConfig, validate_timezone
fetch_last_versions, write_json_file, read_json_file, validate_timezone)
from .validate import HASS_DEVICES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -19,7 +15,6 @@ DATETIME_FORMAT = "%Y%m%d %H:%M:%S"
HOMEASSISTANT_CONFIG = PurePath("homeassistant") HOMEASSISTANT_CONFIG = PurePath("homeassistant")
HOMEASSISTANT_LAST = 'homeassistant_last' HOMEASSISTANT_LAST = 'homeassistant_last'
HOMEASSISTANT_DEVICES = 'homeassistant_devices'
HASSIO_SSL = PurePath("ssl") HASSIO_SSL = PurePath("ssl")
HASSIO_LAST = 'hassio_last' HASSIO_LAST = 'hassio_last'
@ -50,7 +45,6 @@ SCHEMA_CONFIG = vol.Schema({
vol.Optional(API_ENDPOINT): vol.Coerce(str), vol.Optional(API_ENDPOINT): vol.Coerce(str),
vol.Optional(TIMEZONE, default='UTC'): validate_timezone, vol.Optional(TIMEZONE, default='UTC'): validate_timezone,
vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str), vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str),
vol.Optional(HOMEASSISTANT_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(HASSIO_LAST): vol.Coerce(str), vol.Optional(HASSIO_LAST): vol.Coerce(str),
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()], vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(), vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(),
@ -61,48 +55,13 @@ SCHEMA_CONFIG = vol.Schema({
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)
class CoreConfig(object): class CoreConfig(JsonConfig):
"""Hold all core config data.""" """Hold all core config data."""
def __init__(self): def __init__(self):
"""Initialize config object.""" """Initialize config object."""
super().__init__(FILE_HASSIO_CONFIG, SCHEMA_CONFIG)
self.arch = None self.arch = None
self._file = FILE_HASSIO_CONFIG
self._data = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate data
if not self._validate_config():
self._data = SCHEMA_CONFIG({})
def _validate_config(self):
"""Validate config and return True or False."""
# validate data
try:
self._data = SCHEMA_CONFIG(self._data)
except vol.Invalid as ex:
_LOGGER.warning(
"Invalid config %s", humanize_error(self._data, ex))
return False
return True
def save(self):
"""Store data to config file."""
if not self._validate_config():
return False
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
async def fetch_update_infos(self, websession): async def fetch_update_infos(self, websession):
"""Read current versions from web.""" """Read current versions from web."""
@ -150,22 +109,6 @@ class CoreConfig(object):
self._data[TIMEZONE] = value self._data[TIMEZONE] = value
self.save() self.save()
@property
def homeassistant_devices(self):
"""Return list of special device to map into homeassistant."""
return self._data[HOMEASSISTANT_DEVICES]
@homeassistant_devices.setter
def homeassistant_devices(self, value):
"""Set list of special device."""
self._data[HOMEASSISTANT_DEVICES] = value
self.save()
@property
def homeassistant_image(self):
"""Return docker homeassistant repository."""
return os.environ['HOMEASSISTANT_REPOSITORY']
@property @property
def last_homeassistant(self): def last_homeassistant(self):
"""Actual version of homeassistant.""" """Actual version of homeassistant."""

View File

@ -1,7 +1,7 @@
"""Const file for HassIO.""" """Const file for HassIO."""
from pathlib import Path from pathlib import Path
HASSIO_VERSION = '0.42' HASSIO_VERSION = '0.43'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/master/version.json') 'hassio/master/version.json')
@ -24,6 +24,7 @@ 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")
SOCKET_DOCKER = Path("/var/run/docker.sock") SOCKET_DOCKER = Path("/var/run/docker.sock")
SOCKET_HC = Path("/var/run/hassio-hc.sock") SOCKET_HC = Path("/var/run/hassio-hc.sock")
@ -94,6 +95,7 @@ ATTR_SIZE = 'size'
ATTR_TYPE = 'type' ATTR_TYPE = 'type'
ATTR_TIMEOUT = 'timeout' ATTR_TIMEOUT = 'timeout'
ATTR_AUTO_UPDATE = 'auto_update' ATTR_AUTO_UPDATE = 'auto_update'
ATTR_CUSTOM = 'custom'
STARTUP_INITIALIZE = 'initialize' STARTUP_INITIALIZE = 'initialize'
STARTUP_BEFORE = 'before' STARTUP_BEFORE = 'before'

View File

@ -13,13 +13,12 @@ from .const import (
RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT, RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT,
RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE, RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE,
STARTUP_INITIALIZE, RUN_RELOAD_SNAPSHOTS_TASKS, RUN_UPDATE_ADDONS_TASKS) STARTUP_INITIALIZE, RUN_RELOAD_SNAPSHOTS_TASKS, RUN_UPDATE_ADDONS_TASKS)
from .homeassistant import HomeAssistant
from .scheduler import Scheduler from .scheduler import Scheduler
from .dock.homeassistant import DockerHomeAssistant
from .dock.supervisor import DockerSupervisor from .dock.supervisor import DockerSupervisor
from .snapshots import SnapshotsManager from .snapshots import SnapshotsManager
from .tasks import ( from .tasks import (
hassio_update, homeassistant_watchdog, homeassistant_setup, hassio_update, homeassistant_watchdog, api_sessions_cleanup, addons_update)
api_sessions_cleanup, addons_update)
from .tools import get_local_ip, fetch_timezone from .tools import get_local_ip, fetch_timezone
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -41,7 +40,10 @@ class HassIO(object):
# init basic docker container # init basic docker container
self.supervisor = DockerSupervisor(config, loop, self.dock, self.stop) self.supervisor = DockerSupervisor(config, loop, self.dock, self.stop)
self.homeassistant = DockerHomeAssistant(config, loop, self.dock)
# init homeassistant
self.homeassistant = HomeAssistant(
config, loop, self.dock, self.websession)
# init HostControl # init HostControl
self.host_control = HostControl(loop) self.host_control = HostControl(loop)
@ -94,13 +96,8 @@ class HassIO(object):
api_sessions_cleanup(self.config), RUN_CLEANUP_API_SESSIONS, api_sessions_cleanup(self.config), RUN_CLEANUP_API_SESSIONS,
now=True) now=True)
# first start of supervisor? # Load homeassistant
if not await self.homeassistant.exists(): await self.homeassistant.prepare()
_LOGGER.info("No HomeAssistant docker found.")
await homeassistant_setup(
self.config, self.loop, self.homeassistant, self.websession)
else:
await self.homeassistant.attach()
# Load addons # Load addons
await self.addons.prepare() await self.addons.prepare()
@ -157,6 +154,10 @@ class HassIO(object):
homeassistant_watchdog(self.loop, self.homeassistant), homeassistant_watchdog(self.loop, self.homeassistant),
RUN_WATCHDOG_HOMEASSISTANT) RUN_WATCHDOG_HOMEASSISTANT)
# If landingpage / run upgrade in background
if self.homeassistant.version == 'landingpage':
self.loop.create_task(self.homeassistant.install())
async def stop(self, exit_code=0): async def stop(self, exit_code=0):
"""Stop a running orchestration.""" """Stop a running orchestration."""
# don't process scheduler anymore # don't process scheduler anymore

View File

@ -260,9 +260,13 @@ class DockerBase(object):
if not self._install(tag): if not self._install(tag):
return False return False
# cleanup old stuff # run or cleanup container
if was_running: if was_running:
self._run() self._run()
else:
self._stop()
# cleanup images
self._cleanup() self._cleanup()
return True return True

View File

@ -13,9 +13,10 @@ HASS_DOCKER_NAME = 'homeassistant'
class DockerHomeAssistant(DockerBase): class DockerHomeAssistant(DockerBase):
"""Docker hassio wrapper for HomeAssistant.""" """Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock): def __init__(self, config, loop, dock, data):
"""Initialize docker homeassistant wrapper.""" """Initialize docker homeassistant wrapper."""
super().__init__(config, loop, dock, image=config.homeassistant_image) super().__init__(config, loop, dock, image=data.image)
self.data = data
@property @property
def name(self): def name(self):
@ -25,11 +26,11 @@ class DockerHomeAssistant(DockerBase):
@property @property
def devices(self): def devices(self):
"""Create list of special device to map into docker.""" """Create list of special device to map into docker."""
if not self.config.homeassistant_devices: if not self.data.devices:
return return
devices = [] devices = []
for device in self.config.homeassistant_devices: for device in self.data.devices:
devices.append("/dev/{0}:/dev/{0}:rwm".format(device)) devices.append("/dev/{0}:/dev/{0}:rwm".format(device))
return devices return devices

162
hassio/homeassistant.py Normal file
View File

@ -0,0 +1,162 @@
"""HomeAssistant control object."""
import asyncio
import logging
import os
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_VERSION)
from .dock.homeassistant import DockerHomeAssistant
from .tools import JsonConfig
from .validate import SCHEMA_HASS_CONFIG
_LOGGER = logging.getLogger(__name__)
class HomeAssistant(JsonConfig):
"""Hass core object for handle it."""
def __init__(self, config, loop, dock, websession):
"""Initialize hass object."""
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
self.config = config
self.loop = loop
self.websession = websession
self.docker = DockerHomeAssistant(config, loop, dock, self)
async def prepare(self):
"""Prepare HomeAssistant object."""
if not await self.docker.exists():
_LOGGER.info("No HomeAssistant docker %s found.", self.image)
if self.is_custom_image:
await self.install()
else:
await self.install_landingpage()
else:
await self.docker.attach()
@property
def version(self):
"""Return version of running homeassistant."""
return self.docker.version
@property
def last_version(self):
"""Return last available version of homeassistant."""
if self.is_custom_image:
return self._data.get(ATTR_LAST_VERSION)
return self.config.last_homeassistant
@property
def image(self):
"""Return image name of hass containter."""
if ATTR_IMAGE in self._data:
return self._data[ATTR_IMAGE]
return os.environ['HOMEASSISTANT_REPOSITORY']
@property
def is_custom_image(self):
"""Return True if a custom image is used."""
return ATTR_IMAGE in self._data
@property
def devices(self):
"""Return extend device mapping."""
return self._data[ATTR_DEVICES]
@devices.setter
def devices(self, value):
"""Set extend device mapping."""
self._data[ATTR_DEVICES] = value
self.save()
def set_custom(self, image, version):
"""Set a custom image for homeassistant."""
# reset
if image is None and version is None:
self._data.pop(ATTR_IMAGE, None)
self._data.pop(ATTR_VERSION, None)
self.docker.image = self.image
else:
if image:
self._data[ATTR_IMAGE] = image
self.docker.image = image
if version:
self._data[ATTR_VERSION] = version
self.save()
async def install_landingpage(self):
"""Install a landingpage."""
_LOGGER.info("Setup HomeAssistant landingpage")
while True:
if await self.docker.install('landingpage'):
break
_LOGGER.warning("Fails install landingpage, retry after 60sec")
await asyncio.sleep(60, loop=self.loop)
async def install(self):
"""Install a landingpage."""
_LOGGER.info("Setup HomeAssistant")
while True:
# read homeassistant tag and install it
if not self.last_version:
await self.config.fetch_update_infos(self.websession)
tag = self.last_version
if tag and await self.docker.install(tag):
break
_LOGGER.warning("Error on install HomeAssistant. Retry in 60sec")
await asyncio.sleep(60, loop=self.loop)
# store version
_LOGGER.info("HomeAssistant docker now installed")
await self.docker.cleanup()
def update(self, version=None):
"""Update HomeAssistant version.
Return a coroutine.
"""
version = version or self.last_version
return self.docker.update(version)
def run(self):
"""Run HomeAssistant docker.
Return a coroutine.
"""
return self.docker.run()
def stop(self):
"""Stop HomeAssistant docker.
Return a coroutine.
"""
return self.docker.stop()
def restart(self):
"""Restart HomeAssistant docker.
Return a coroutine.
"""
return self.docker.restart()
def logs(self):
"""Get HomeAssistant docker logs.
Return a coroutine.
"""
return self.docker.logs()
def is_running(self):
"""Return True if docker container is running.
Return a coroutine.
"""
return self.docker.is_running()
@property
def in_progress(self):
"""Return True if a task is in progress."""
return self.docker.in_progress

View File

@ -37,7 +37,7 @@ class SnapshotsManager(object):
def _create_snapshot(self, name, sys_type): def _create_snapshot(self, name, sys_type):
"""Initialize a new snapshot object from name.""" """Initialize a new snapshot object from name."""
date_str = str(datetime.utcnow()) date_str = datetime.utcnow().isoformat()
slug = create_slug(name, date_str) slug = create_slug(name, date_str)
tar_file = Path(self.config.path_backup, "{}.tar".format(slug)) tar_file = Path(self.config.path_backup, "{}.tar".format(slug))
@ -46,8 +46,7 @@ class SnapshotsManager(object):
snapshot.create(slug, name, date_str, sys_type) snapshot.create(slug, name, date_str, sys_type)
# set general data # set general data
snapshot.homeassistant_version = self.homeassistant.version snapshot.snapshot_homeassistant(self.homeassistant)
snapshot.homeassistant_devices = self.config.homeassistant_devices
snapshot.repositories = self.config.addons_repositories snapshot.repositories = self.config.addons_repositories
return snapshot return snapshot
@ -198,8 +197,7 @@ class SnapshotsManager(object):
await snapshot.restore_folders() await snapshot.restore_folders()
# start homeassistant restore # start homeassistant restore
self.config.homeassistant_devices = \ snapshot.restore_homeassistant(self.homeassistant)
snapshot.homeassistant_devices
task_hass = self.loop.create_task( task_hass = self.loop.create_task(
self.homeassistant.update(snapshot.homeassistant_version)) self.homeassistant.update(snapshot.homeassistant_version))
@ -281,8 +279,7 @@ class SnapshotsManager(object):
await snapshot.restore_folders(folders) await snapshot.restore_folders(folders)
if homeassistant: if homeassistant:
self.config.homeassistant_devices = \ snapshot.restore_homeassistant(self.homeassistant)
snapshot.homeassistant_devices
tasks.append(self.homeassistant.update( tasks.append(self.homeassistant.update(
snapshot.homeassistant_version)) snapshot.homeassistant_version))

View File

@ -13,7 +13,8 @@ from .validate import SCHEMA_SNAPSHOT, ALL_FOLDERS
from .util import remove_folder from .util import remove_folder
from ..const import ( from ..const import (
ATTR_SLUG, ATTR_NAME, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES, ATTR_SLUG, ATTR_NAME, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES,
ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_DEVICES) ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_DEVICES,
ATTR_IMAGE)
from ..tools import write_json_file from ..tools import write_json_file
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -90,6 +91,16 @@ class Snapshot(object):
"""Set snapshot homeassistant devices.""" """Set snapshot homeassistant devices."""
self._data[ATTR_HOMEASSISTANT][ATTR_DEVICES] = value self._data[ATTR_HOMEASSISTANT][ATTR_DEVICES] = value
@property
def homeassistant_image(self):
"""Return snapshot homeassistant custom image."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_IMAGE)
@homeassistant_image.setter
def homeassistant_image(self, value):
"""Set snapshot homeassistant custom image."""
self._data[ATTR_HOMEASSISTANT][ATTR_IMAGE] = value
@property @property
def size(self): def size(self):
"""Return snapshot size.""" """Return snapshot size."""
@ -111,6 +122,24 @@ class Snapshot(object):
self._data[ATTR_REPOSITORIES] = [] self._data[ATTR_REPOSITORIES] = []
self._data[ATTR_FOLDERS] = [] self._data[ATTR_FOLDERS] = []
def snapshot_homeassistant(self, homeassistant):
"""Read all data from homeassistant object."""
self.homeassistant_version = homeassistant.version
self.homeassistant_devices = homeassistant.devices
# custom image
if homeassistant.is_custom_image:
self.homeassistant_image = homeassistant.image
def restore_homeassistant(self, homeassistant):
"""Write all data to homeassistant object."""
homeassistant.devices = self.homeassistant_devices
# custom image
if self.homeassistant_image:
homeassistant.set_custom(
self.homeassistant_image, self.homeassistant_version)
async def load(self): async def load(self):
"""Read snapshot.json from tar file.""" """Read snapshot.json from tar file."""
if not self.tar_file.is_file(): if not self.tar_file.is_file():

View File

@ -5,7 +5,7 @@ import voluptuous as vol
from ..const import ( from ..const import (
ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_NAME, ATTR_SLUG, ATTR_DATE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_NAME, ATTR_SLUG, ATTR_DATE,
ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_DEVICES, ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_DEVICES,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL, ATTR_IMAGE, FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL) SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
from ..validate import HASS_DEVICES from ..validate import HASS_DEVICES
@ -20,6 +20,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Required(ATTR_HOMEASSISTANT): vol.Schema({ vol.Required(ATTR_HOMEASSISTANT): vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str), vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES, vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(ATTR_IMAGE): vol.Coerce(str),
}), }),
vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)], vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({ vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({

View File

@ -66,20 +66,3 @@ def homeassistant_watchdog(loop, homeassistant):
loop.create_task(homeassistant.run()) loop.create_task(homeassistant.run())
return _homeassistant_watchdog return _homeassistant_watchdog
async def homeassistant_setup(config, loop, homeassistant, websession):
"""Install a homeassistant docker container."""
while True:
# read homeassistant tag and install it
if not config.last_homeassistant:
await config.fetch_update_infos(websession)
tag = config.last_homeassistant
if tag and await homeassistant.install(tag):
break
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
await asyncio.sleep(60, loop=loop)
# store version
_LOGGER.info("HomeAssistant docker now installed.")

View File

@ -9,6 +9,7 @@ import aiohttp
import async_timeout import async_timeout
import pytz import pytz
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error
from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA
@ -98,3 +99,44 @@ async def fetch_timezone(websession):
data = await request.json() data = await request.json()
return data.get('time_zone', 'UTC') return data.get('time_zone', 'UTC')
class JsonConfig(object):
"""Hass core object for handle it."""
def __init__(self, json_file, schema):
"""Initialize hass object."""
self._file = json_file
self._schema = schema
self._data = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# 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))
def save(self):
"""Store data to config file."""
# 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
# write
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True

View File

@ -1,6 +1,9 @@
"""Validate functions.""" """Validate functions."""
import voluptuous as vol import voluptuous as vol
from .const import ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
HASS_DEVICES = [vol.Match(r"^[^/]*$")] HASS_DEVICES = [vol.Match(r"^[^/]*$")]
@ -30,3 +33,10 @@ DOCKER_PORTS = vol.Schema({
vol.All(vol.Coerce(str), vol.Match(r"^\d+(?:/tcp|/udp)?$")): vol.All(vol.Coerce(str), vol.Match(r"^\d+(?:/tcp|/udp)?$")):
convert_to_docker_ports, convert_to_docker_ports,
}) })
SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
})

View File

@ -1,5 +1,5 @@
{ {
"hassio": "0.42", "hassio": "0.43",
"homeassistant": "0.48.1", "homeassistant": "0.48.1",
"resinos": "0.8", "resinos": "0.8",
"resinhup": "0.1", "resinhup": "0.1",