diff --git a/API.md b/API.md index 3fe6c7ad6..65e619a29 100644 --- a/API.md +++ b/API.md @@ -71,10 +71,10 @@ Optional: { "beta_channel": "true|false", "timezone": "TIMEZONE", + "wait_boot": "int", "addons_repositories": [ "REPO_URL" - ], - "wait_boot": "int" + ] } ``` @@ -170,10 +170,7 @@ Return QR-Code "name": "custom snapshot name / description", "date": "ISO", "size": "SIZE_IN_MB", - "homeassistant": { - "version": "INSTALLED_HASS_VERSION", - "devices": [] - }, + "homeassistant": "version", "addons": [ { "slug": "ADDON_SLUG", @@ -286,7 +283,6 @@ Optional: { "version": "INSTALL_VERSION", "last_version": "LAST_VERSION", - "devices": [""], "image": "str", "custom": "bool -> if custom image", "boot": "bool", @@ -319,7 +315,6 @@ Output is the raw Docker log. ```json { - "devices": [], "image": "Optional|null", "last_version": "Optional for custom image|null", "port": "port for access hass", diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 9aeade419..571f6d304 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -703,8 +703,9 @@ class Addon(CoreSysAttributes): # check version / restore image version = data[ATTR_VERSION] - if version != self.instance.version: + if not await self.instance.exists(): _LOGGER.info("Restore image for addon %s", self._id) + image_file = Path(temp, "image.tar") if image_file.is_file(): await self.instance.import_image(image_file, version) diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 3603586d2..becf7588f 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -183,7 +183,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema({ vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM, vol.Required(ATTR_STATE): vol.In([STATE_STARTED, STATE_STOPPED]), vol.Required(ATTR_VERSION): vol.Coerce(str), -}) +}, extra=vol.REMOVE_EXTRA) def validate_options(raw_schema): diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index e27394540..d12a108bc 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -6,20 +6,19 @@ import voluptuous as vol from .utils import api_process, api_process_raw, api_validate from ..const import ( - ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM, - ATTR_BOOT, ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, - CONTENT_TYPE_BINARY) + ATTR_VERSION, ATTR_LAST_VERSION, ATTR_IMAGE, ATTR_CUSTOM, ATTR_BOOT, + ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, CONTENT_TYPE_BINARY) from ..coresys import CoreSysAttributes -from ..validate import HASS_DEVICES, NETWORK_PORT +from ..validate import NETWORK_PORT _LOGGER = logging.getLogger(__name__) # pylint: disable=no-value-for-parameter SCHEMA_OPTIONS = vol.Schema({ - vol.Optional(ATTR_DEVICES): HASS_DEVICES, vol.Optional(ATTR_BOOT): vol.Boolean(), - vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Any(None, vol.Coerce(str)), + 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.Optional(ATTR_PORT): NETWORK_PORT, @@ -43,7 +42,6 @@ class APIHomeAssistant(CoreSysAttributes): ATTR_VERSION: self._homeassistant.version, ATTR_LAST_VERSION: self._homeassistant.last_version, ATTR_IMAGE: self._homeassistant.image, - ATTR_DEVICES: self._homeassistant.devices, ATTR_CUSTOM: self._homeassistant.is_custom_image, ATTR_BOOT: self._homeassistant.boot, ATTR_PORT: self._homeassistant.api_port, @@ -56,12 +54,9 @@ class APIHomeAssistant(CoreSysAttributes): """Set homeassistant options.""" body = await api_validate(SCHEMA_OPTIONS, request) - if ATTR_DEVICES in body: - self._homeassistant.devices = body[ATTR_DEVICES] - - if ATTR_IMAGE in body: - self._homeassistant.set_custom( - body[ATTR_IMAGE], body[ATTR_LAST_VERSION]) + if ATTR_IMAGE in body and ATTR_LAST_VERSION in body: + self._homeassistant.image = body[ATTR_IMAGE] + self._homeassistant.last_version = body[ATTR_LAST_VERSION] if ATTR_BOOT in body: self._homeassistant.boot = body[ATTR_BOOT] @@ -78,6 +73,7 @@ class APIHomeAssistant(CoreSysAttributes): if ATTR_WATCHDOG in body: self._homeassistant.watchdog = body[ATTR_WATCHDOG] + self._homeassistant.save() return True @api_process diff --git a/hassio/api/snapshots.py b/hassio/api/snapshots.py index c293c2a68..804bd0439 100644 --- a/hassio/api/snapshots.py +++ b/hassio/api/snapshots.py @@ -9,7 +9,7 @@ from ..snapshots.validate import ALL_FOLDERS from ..const import ( ATTR_NAME, ATTR_SLUG, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES, ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE, - ATTR_DEVICES, ATTR_SNAPSHOTS) + ATTR_SNAPSHOTS) from ..coresys import CoreSysAttributes _LOGGER = logging.getLogger(__name__) @@ -82,10 +82,7 @@ class APISnapshots(CoreSysAttributes): ATTR_NAME: snapshot.name, ATTR_DATE: snapshot.date, ATTR_SIZE: snapshot.size, - ATTR_HOMEASSISTANT: { - ATTR_VERSION: snapshot.homeassistant_version, - ATTR_DEVICES: snapshot.homeassistant_devices, - }, + ATTR_HOMEASSISTANT: snapshot.homeassistant_version, ATTR_ADDONS: data_addons, ATTR_REPOSITORIES: snapshot.repositories, ATTR_FOLDERS: snapshot.folders, diff --git a/hassio/docker/addon.py b/hassio/docker/addon.py index 0f025c652..760207143 100644 --- a/hassio/docker/addon.py +++ b/hassio/docker/addon.py @@ -100,8 +100,8 @@ class DockerAddon(DockerInterface): # Auto mapping UART devices if self.addon.auto_uart: - for uart_dev in self._hardware.serial_devices: - devices.append("{0}:{0}:rwm".format(uart_dev)) + for device in self._hardware.serial_devices: + devices.append(f"{device}:{device}:rwm") # Return None if no devices is present return devices or None @@ -135,7 +135,7 @@ class DockerAddon(DockerInterface): """Return tmpfs for docker add-on.""" options = self.addon.tmpfs if options: - return {"/tmpfs": "{}".format(options)} + return {"/tmpfs": f"{options}"} return None @property @@ -324,7 +324,7 @@ class DockerAddon(DockerInterface): """ try: with tar_file.open("rb") as read_tar: - self._docker.api.load_image(read_tar) + self._docker.api.load_image(read_tar, quiet=True) image = self._docker.images.get(self.image) image.tag(self.image, tag=tag) diff --git a/hassio/docker/homeassistant.py b/hassio/docker/homeassistant.py index 2fb53dca2..148e9c78e 100644 --- a/hassio/docker/homeassistant.py +++ b/hassio/docker/homeassistant.py @@ -26,14 +26,10 @@ class DockerHomeAssistant(DockerInterface): @property def devices(self): """Create list of special device to map into docker.""" - if not self._homeassistant.devices: - return None - devices = [] - for device in self._homeassistant.devices: - devices.append("/dev/{0}:/dev/{0}:rwm".format(device)) - - return devices + for device in self._hardware.serial_devices: + devices.append(f"{device}:{device}:rwm") + return devices or None def _run(self): """Run docker image. diff --git a/hassio/docker/interface.py b/hassio/docker/interface.py index 46432854a..fd44e943a 100644 --- a/hassio/docker/interface.py +++ b/hassio/docker/interface.py @@ -69,7 +69,7 @@ class DockerInterface(CoreSysAttributes): """ try: _LOGGER.info("Pull image %s tag %s.", self.image, tag) - image = self._docker.images.pull("{}:{}".format(self.image, tag)) + image = self._docker.images.pull(f"{self.image}:{tag}") image.tag(self.image, tag='latest') self._meta = image.attrs @@ -90,8 +90,9 @@ class DockerInterface(CoreSysAttributes): Need run inside executor. """ try: - self._docker.images.get(self.image) - except docker.errors.DockerException: + image = self._docker.images.get(self.image) + assert f"{self.image}:{self.version}" in image.tags + except (docker.errors.DockerException, AssertionError): return False return True @@ -204,11 +205,11 @@ class DockerInterface(CoreSysAttributes): try: with suppress(docker.errors.ImageNotFound): self._docker.images.remove( - image="{}:latest".format(self.image), force=True) + image=f"{self.image}:latest", force=True) with suppress(docker.errors.ImageNotFound): self._docker.images.remove( - image="{}:{}".format(self.image, self.version), force=True) + image=f"{self.image}:{self.version}", force=True) except docker.errors.DockerException as err: _LOGGER.warning("Can't remove image %s: %s", self.image, err) diff --git a/hassio/homeassistant.py b/hassio/homeassistant.py index fb6fdc70f..29ec28001 100644 --- a/hassio/homeassistant.py +++ b/hassio/homeassistant.py @@ -8,8 +8,8 @@ import aiohttp from aiohttp.hdrs import CONTENT_TYPE from .const import ( - FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION, - ATTR_VERSION, ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, + FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, + ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, HEADER_HA_ACCESS, CONTENT_TYPE_JSON) from .coresys import CoreSysAttributes from .docker.homeassistant import DockerHomeAssistant @@ -33,14 +33,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): async def load(self): """Prepare HomeAssistant object.""" - if not await self.instance.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.instance.attach() + if await self.instance.attach(): + return + + _LOGGER.info("No HomeAssistant docker %s found.", self.image) + await self.install_landingpage() @property def api_ip(self): @@ -67,7 +64,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): def api_password(self, value): """Set password for home-assistant instance.""" self._data[ATTR_PASSWORD] = value - self.save() @property def api_ssl(self): @@ -78,7 +74,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): def api_ssl(self, value): """Set SSL for home-assistant instance.""" self._data[ATTR_SSL] = value - self.save() @property def api_url(self): @@ -96,7 +91,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): def watchdog(self, value): """Return True if the watchdog should protect Home-Assistant.""" self._data[ATTR_WATCHDOG] = value - self.save() @property def version(self): @@ -110,28 +104,34 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): return self._data.get(ATTR_LAST_VERSION) return self._updater.version_homeassistant + @last_version.setter + def last_version(self, value): + """Set last available version of homeassistant.""" + if value: + self._data[ATTR_LAST_VERSION] = value + else: + self._data.pop(ATTR_LAST_VERSION, None) + @property def image(self): """Return image name of hass containter.""" - if ATTR_IMAGE in self._data: + if self._data.get(ATTR_IMAGE): return self._data[ATTR_IMAGE] return os.environ['HOMEASSISTANT_REPOSITORY'] + @image.setter + def image(self, value): + """Set image name of hass containter.""" + if value: + self._data[ATTR_IMAGE] = value + else: + self._data.pop(ATTR_IMAGE, None) + @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() + return all(attr in self._data for attr in + (ATTR_IMAGE, ATTR_LAST_VERSION)) @property def boot(self): @@ -142,23 +142,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): def boot(self, value): """Set home-assistant boot options.""" self._data[ATTR_BOOT] = 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.instance.image = self.image - else: - if image: - self._data[ATTR_IMAGE] = image - self.instance.image = image - if version: - self._data[ATTR_VERSION] = version - self.save() async def install_landingpage(self): """Install a landingpage.""" @@ -196,8 +179,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): """Update HomeAssistant version.""" version = version or self.last_version running = await self.instance.is_running() + exists = await self.instance.exists() - if version == self.instance.version: + if exists and version == self.instance.version: _LOGGER.info("Version %s is already installed", version) return False diff --git a/hassio/snapshots/snapshot.py b/hassio/snapshots/snapshot.py index d5e54444b..c3fe641f6 100644 --- a/hassio/snapshots/snapshot.py +++ b/hassio/snapshots/snapshot.py @@ -13,8 +13,9 @@ from .validate import SCHEMA_SNAPSHOT, ALL_FOLDERS from .utils import remove_folder from ..const import ( ATTR_SLUG, ATTR_NAME, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES, - ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_DEVICES, - ATTR_IMAGE, ATTR_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT) + ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_IMAGE, + ATTR_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT, + ATTR_LAST_VERSION) from ..coresys import CoreSysAttributes from ..utils.json import write_json_file @@ -82,14 +83,14 @@ class Snapshot(CoreSysAttributes): self._data[ATTR_HOMEASSISTANT][ATTR_VERSION] = value @property - def homeassistant_devices(self): - """Return snapshot homeassistant devices.""" - return self._data[ATTR_HOMEASSISTANT].get(ATTR_DEVICES) + def homeassistant_last_version(self): + """Return snapshot homeassistant last version (custom).""" + return self._data[ATTR_HOMEASSISTANT].get(ATTR_LAST_VERSION) - @homeassistant_devices.setter - def homeassistant_devices(self, value): - """Set snapshot homeassistant devices.""" - self._data[ATTR_HOMEASSISTANT][ATTR_DEVICES] = value + @homeassistant_last_version.setter + def homeassistant_last_version(self, value): + """Set snapshot homeassistant last version (custom).""" + self._data[ATTR_HOMEASSISTANT][ATTR_LAST_VERSION] = value @property def homeassistant_image(self): @@ -335,13 +336,13 @@ class Snapshot(CoreSysAttributes): def store_homeassistant(self): """Read all data from homeassistant object.""" self.homeassistant_version = self._homeassistant.version - self.homeassistant_devices = self._homeassistant.devices self.homeassistant_watchdog = self._homeassistant.watchdog self.homeassistant_boot = self._homeassistant.boot # custom image if self._homeassistant.is_custom_image: self.homeassistant_image = self._homeassistant.image + self.homeassistant_last_version = self._homeassistant.last_version # api self.homeassistant_port = self._homeassistant.api_port @@ -350,20 +351,22 @@ class Snapshot(CoreSysAttributes): def restore_homeassistant(self): """Write all data to homeassistant object.""" - self._homeassistant.devices = self.homeassistant_devices self._homeassistant.watchdog = self.homeassistant_watchdog self._homeassistant.boot = self.homeassistant_boot # custom image if self.homeassistant_image: - self._homeassistant.set_custom( - self.homeassistant_image, self.homeassistant_version) + self._homeassistant.image = self.homeassistant_image + self._homeassistant.last_version = self.homeassistant_last_version # api self._homeassistant.api_port = self.homeassistant_port self._homeassistant.api_ssl = self.homeassistant_ssl self._homeassistant.api_password = self.homeassistant_password + # save + self._homeassistant.save() + def store_repositories(self): """Store repository list into snapshot.""" self.repositories = self._config.addons_repositories diff --git a/hassio/snapshots/validate.py b/hassio/snapshots/validate.py index 9e0fcec04..5007179a8 100644 --- a/hassio/snapshots/validate.py +++ b/hassio/snapshots/validate.py @@ -4,11 +4,12 @@ import voluptuous as vol from ..const import ( ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_NAME, ATTR_SLUG, ATTR_DATE, - ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_DEVICES, - ATTR_IMAGE, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, + ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_IMAGE, + ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, + ATTR_LAST_VERSION, FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL, SNAPSHOT_FULL, SNAPSHOT_PARTIAL) -from ..validate import HASS_DEVICES, NETWORK_PORT +from ..validate import NETWORK_PORT ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL] @@ -20,19 +21,19 @@ SCHEMA_SNAPSHOT = vol.Schema({ vol.Required(ATTR_DATE): vol.Coerce(str), vol.Optional(ATTR_HOMEASSISTANT, default={}): vol.Schema({ vol.Required(ATTR_VERSION): vol.Coerce(str), - vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES, vol.Optional(ATTR_IMAGE): vol.Coerce(str), + vol.Optional(ATTR_LAST_VERSION): 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.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.ALLOW_EXTRA) diff --git a/hassio/validate.py b/hassio/validate.py index a77cb92da..fb3fe5c57 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -4,15 +4,14 @@ import voluptuous as vol import pytz from .const import ( - ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_SESSIONS, ATTR_PASSWORD, - ATTR_TOTP, ATTR_SECURITY, ATTR_BETA_CHANNEL, ATTR_TIMEZONE, - ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, - ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, - ATTR_PORT, ATTR_WATCHDOG, ATTR_WAIT_BOOT) + ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_SESSIONS, ATTR_PASSWORD, ATTR_TOTP, + ATTR_SECURITY, ATTR_BETA_CHANNEL, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST, + ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, ATTR_HOMEASSISTANT, ATTR_HASSIO, + ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, + ATTR_WAIT_BOOT) NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) -HASS_DEVICES = [vol.Match(r"^[^/]*$")] ALSA_CHANNEL = vol.Match(r"\d+,\d+") WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60)) @@ -60,7 +59,6 @@ DOCKER_PORTS = vol.Schema({ # pylint: disable=no-value-for-parameter SCHEMA_HASS_CONFIG = vol.Schema({ - vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES, vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str), vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),