Files
supervisor/supervisor/apps/validate.py
T
Stefan AgnerandClaude Opus 4.8 74fa31f886 Reduce app config schema warning noise for regular users (#7071)
* Lower app config advisory log level for non-local store apps

App config deprecation and misconfiguration advisories were logged at
warning level for every app in every store repository on each store
reload. Because store data is validated for all available apps, not just
installed ones, regular users saw "Please report this to the maintainer"
warnings for apps they had never installed and could not fix, which is
confusing and backwards for that audience.

Route these advisories through an injected logging callable so the store
data layer can pick the level that matches who can act on them. Apps from
the local repository (authored by the user) and any app on the dev channel
(a developer testing store apps) still log at warning level; everything
else logs at debug, keeping the messages available for troubleshooting
without spamming regular users' logs. Installed apps re-validated from
apps.json on every boot now also log at debug, since they were already
migrated and their advisories logged when first read from the store.

Reword the messages to state the deprecation or misconfiguration as a fact
instead of asking the reader to report it to the maintainer, since the
remaining warning-level audiences are the ones who can address it directly.
The hard rejection for a dynamic ingress port conflict keeps its existing
wording, as it aborts loading regardless of channel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Also warn for installed apps from custom repositories

The initial change kept app config advisories at warning level only for
the local repository and the dev channel, silencing them for every store
app on stable/beta. Review feedback pointed out that this also hides the
warning for installed apps whose maintainer has moved on - often thin
wrapper apps in custom repositories where the warning is the user's only
heads-up before the app breaks.

Warn for installed apps from custom (non-built-in) repositories as well.
Apps from the curated built-in stores (Core, Community, ESPHome, Music
Assistant) stay quiet even when installed, since deprecations there are
fixed via PRs and are not actionable for the user. Uninstalled apps
remain quiet everywhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 10:22:26 +02:00

668 lines
23 KiB
Python

"""Validate apps options schema."""
from collections.abc import Callable
import logging
import re
import secrets
from typing import Any
import uuid
import voluptuous as vol
from ..const import (
ARCH_ALL_COMPAT,
ARCH_DEPRECATED,
ATTR_ACCESS_TOKEN,
ATTR_ADVANCED,
ATTR_APPARMOR,
ATTR_ARCH,
ATTR_ARGS,
ATTR_AUDIO,
ATTR_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT,
ATTR_AUTH_API,
ATTR_AUTO_UPDATE,
ATTR_BACKUP_EXCLUDE,
ATTR_BACKUP_POST,
ATTR_BACKUP_PRE,
ATTR_BOOT,
ATTR_BUILD_FROM,
ATTR_CONFIGURATION,
ATTR_DESCRIPTON,
ATTR_DEVICES,
ATTR_DEVICETREE,
ATTR_DISCOVERY,
ATTR_DOCKER_API,
ATTR_ENVIRONMENT,
ATTR_FIELDS,
ATTR_FULL_ACCESS,
ATTR_GPIO,
ATTR_HASSIO_API,
ATTR_HASSIO_ROLE,
ATTR_HOMEASSISTANT,
ATTR_HOMEASSISTANT_API,
ATTR_HOST_DBUS,
ATTR_HOST_IPC,
ATTR_HOST_NETWORK,
ATTR_HOST_PID,
ATTR_HOST_UTS,
ATTR_IMAGE,
ATTR_INGRESS,
ATTR_INGRESS_ENTRY,
ATTR_INGRESS_PANEL,
ATTR_INGRESS_PORT,
ATTR_INGRESS_STREAM,
ATTR_INGRESS_TOKEN,
ATTR_INIT,
ATTR_JOURNALD,
ATTR_KERNEL_MODULES,
ATTR_LABELS,
ATTR_LEGACY,
ATTR_MACHINE,
ATTR_MAP,
ATTR_NAME,
ATTR_NETWORK,
ATTR_OPTIONS,
ATTR_PANEL_ADMIN,
ATTR_PANEL_ICON,
ATTR_PANEL_TITLE,
ATTR_PORTS,
ATTR_PORTS_DESCRIPTION,
ATTR_PRIVILEGED,
ATTR_PROTECTED,
ATTR_REALTIME,
ATTR_REPOSITORY,
ATTR_SCHEMA,
ATTR_SERVICES,
ATTR_SLUG,
ATTR_SQUASH,
ATTR_STAGE,
ATTR_STARTUP,
ATTR_STATE,
ATTR_STDIN,
ATTR_SYSTEM,
ATTR_SYSTEM_MANAGED,
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY,
ATTR_TIMEOUT,
ATTR_TMPFS,
ATTR_TRANSLATIONS,
ATTR_TYPE,
ATTR_UART,
ATTR_UDEV,
ATTR_ULIMITS,
ATTR_URL,
ATTR_USB,
ATTR_USER,
ATTR_UUID,
ATTR_VERSION,
ATTR_VIDEO,
ATTR_WATCHDOG,
ATTR_WEBUI,
INGRESS_DYNAMIC_PORT_MAX,
INGRESS_DYNAMIC_PORT_MIN,
MACHINE_DEPRECATED,
ROLE_ALL,
ROLE_DEFAULT,
AppBoot,
AppBootConfig,
AppStage,
AppStartup,
AppState,
)
from ..docker.const import Capabilities
from ..validate import (
docker_image,
docker_ports,
docker_ports_description,
network_port,
token,
uuid_match,
version_tag,
)
from .const import (
ATTR_BACKUP,
ATTR_BREAKING_VERSIONS,
ATTR_CODENOTARY,
ATTR_PATH,
ATTR_READ_ONLY,
RE_SLUG,
AppBackupMode,
MappingType,
)
from .options import RE_SCHEMA_ELEMENT
_LOGGER: logging.Logger = logging.getLogger(__name__)
RE_VOLUME = re.compile(
r"^(data|config|ssl|local_apps|addons|backup|share|media|homeassistant_config|all_app_configs|all_addon_configs|app_config|addon_config)(?::(rw|ro))?$"
)
RE_SERVICE = re.compile(r"^(?P<service>mqtt|mysql):(?P<rights>provide|want|need)$")
RE_DOCKER_IMAGE_BUILD = re.compile(
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$"
)
SCHEMA_ELEMENT = vol.Schema(
vol.Any(
vol.Match(RE_SCHEMA_ELEMENT),
[
# A list may not directly contain another list
vol.Any(
vol.Match(RE_SCHEMA_ELEMENT),
{str: vol.Self},
)
],
{str: vol.Self},
)
)
RE_MACHINE = re.compile(
r"^!?(?:"
r"|intel-nuc"
r"|khadas-vim3"
r"|generic-aarch64"
r"|generic-x86-64"
r"|odroid-c2"
r"|odroid-c4"
r"|odroid-m1"
r"|odroid-n2"
r"|odroid-xu"
r"|qemuarm-64"
r"|qemuarm"
r"|qemux86-64"
r"|qemux86"
r"|raspberrypi"
r"|raspberrypi2"
r"|raspberrypi3-64"
r"|raspberrypi3"
r"|raspberrypi4-64"
r"|raspberrypi4"
r"|raspberrypi5-64"
r"|yellow"
r"|green"
r"|tinker"
r")$"
)
RE_SLUG_FIELD = re.compile(r"^" + RE_SLUG + r"$")
def _warn_app_config(log: Callable[..., None]):
"""Build a validator that flags questionable app configs.
Deprecation and misconfiguration advisories are emitted through ``log`` so
the caller can pick the level appropriate for the audience (see
SCHEMA_APP_CONFIG vs. SCHEMA_APP_CONFIG_QUIET). The messages are addressed at
whoever can act on them (a local app's author or a developer on the dev
channel), so they intentionally do not ask the reader to contact a
maintainer.
"""
def _warn(config: dict[str, Any]):
name = config.get(ATTR_NAME)
if not name:
raise vol.Invalid("Invalid app config!")
if ATTR_ADVANCED in config:
# Deprecated since Supervisor 2026.03.0; this field is ignored and
# the advisory can be removed once that version is the minimum
# supported.
log(
"App '%s' uses the deprecated 'advanced' config field, which is ignored.",
name,
)
if config.get(ATTR_FULL_ACCESS, False) and (
config.get(ATTR_DEVICES)
or config.get(ATTR_UART)
or config.get(ATTR_USB)
or config.get(ATTR_GPIO)
):
log(
"App '%s' has full device access; its selective device access configuration is redundant and ignored.",
name,
)
if config.get(ATTR_BACKUP, AppBackupMode.HOT) == AppBackupMode.COLD and (
config.get(ATTR_BACKUP_POST) or config.get(ATTR_BACKUP_PRE)
):
log(
"App '%s' only supports COLD backups but configures pre/post backup commands, which are ignored.",
name,
)
if deprecated_arches := [
arch for arch in config.get(ATTR_ARCH, []) if arch in ARCH_DEPRECATED
]:
log(
"App '%s' uses deprecated 'arch' values: %s",
name,
deprecated_arches,
)
if deprecated_machines := [
machine
for machine in config.get(ATTR_MACHINE, [])
if machine.lstrip("!") in MACHINE_DEPRECATED
]:
log(
"App '%s' uses deprecated 'machine' values: %s",
name,
deprecated_machines,
)
if ATTR_CODENOTARY in config:
log(
"App '%s' uses the deprecated 'codenotary' config field, which is no longer used and ignored.",
name,
)
# Deprecated map types, deprecated as of 2026.07
_LEGACY_MAP_TYPES = {
MappingType.ADDONS: MappingType.LOCAL_APPS,
MappingType.ALL_ADDON_CONFIGS: MappingType.ALL_APP_CONFIGS,
MappingType.ADDON_CONFIG: MappingType.APP_CONFIG,
}
for volume in config[ATTR_MAP]:
if (volume_type := volume[ATTR_TYPE]) in _LEGACY_MAP_TYPES:
log(
"App '%s' uses legacy map type '%s'; use '%s' instead.",
name,
volume_type,
_LEGACY_MAP_TYPES[volume_type],
)
# Dynamic ingress port selection (ingress_port: 0) picks a random port
# from the INGRESS_DYNAMIC_PORT_MIN-INGRESS_DYNAMIC_PORT_MAX range. An
# app must not map a port from that range to the host itself, as the
# dynamically chosen ingress port could then coincide with it, exposing
# the ingress endpoint on the host and bypassing ingress authentication.
if config.get(ATTR_INGRESS) and config.get(ATTR_INGRESS_PORT) == 0:
for container_port in config.get(ATTR_PORTS, {}):
port_number = str(container_port).partition("/")[0]
if (
port_number.isdigit()
and INGRESS_DYNAMIC_PORT_MIN
<= int(port_number)
<= INGRESS_DYNAMIC_PORT_MAX
):
raise vol.Invalid(
f"App '{name}' uses dynamic ingress port selection (ingress_port: 0) "
f"but maps port {port_number} which is reserved for dynamic ingress "
f"ports ({INGRESS_DYNAMIC_PORT_MIN}-{INGRESS_DYNAMIC_PORT_MAX}). "
"Please report this to the maintainer of the app."
)
return config
return _warn
def _migrate_app_config(log: Callable[..., None]):
"""Migrate app config.
Deprecated-format advisories are emitted through ``log`` so the caller can
pick the level appropriate for the audience (see SCHEMA_APP_CONFIG vs.
SCHEMA_APP_CONFIG_QUIET). The migration itself always happens regardless of
the level.
"""
def _migrate(config: dict[str, Any]):
if not isinstance(config, dict):
raise vol.Invalid("App config must be a dictionary!")
name = config.get(ATTR_NAME)
if not name:
raise vol.Invalid("Invalid app config!")
# Startup 2018-03-30
if config.get(ATTR_STARTUP) in ("before", "after"):
value = config[ATTR_STARTUP]
log(
"App '%s' uses deprecated 'startup' value '%s'.",
name,
value,
)
if value == "before":
config[ATTR_STARTUP] = AppStartup.SERVICES
elif value == "after":
config[ATTR_STARTUP] = AppStartup.APPLICATION
# UART 2021-01-20
if "auto_uart" in config:
log(
"App '%s' uses deprecated 'auto_uart'; use 'uart' instead.",
name,
)
config[ATTR_UART] = config.pop("auto_uart")
# Device 2021-01-20
if ATTR_DEVICES in config and any(":" in line for line in config[ATTR_DEVICES]):
log(
"App '%s' uses a deprecated 'devices' format; use a list of paths only.",
name,
)
config[ATTR_DEVICES] = [line.split(":")[0] for line in config[ATTR_DEVICES]]
# TMPFS 2021-02-01
if ATTR_TMPFS in config and not isinstance(config[ATTR_TMPFS], bool):
log(
"App '%s' uses a deprecated 'tmpfs' format; use a boolean instead.",
name,
)
config[ATTR_TMPFS] = True
# 2021-06 "snapshot" renamed to "backup"
for entry in (
"snapshot_exclude",
"snapshot_post",
"snapshot_pre",
"snapshot",
):
if entry in config:
new_entry = entry.replace("snapshot", "backup")
config[new_entry] = config.pop(entry)
log(
"App '%s' uses deprecated config '%s'; use '%s' instead.",
name,
entry,
new_entry,
)
# 2023-11 "map" entries can also be dict to allow path configuration
volumes = []
for entry in config.get(ATTR_MAP, []):
if isinstance(entry, dict):
# Validate that dict entries have required 'type' field
if ATTR_TYPE not in entry:
log(
"App '%s' has an invalid map entry missing the 'type' field, skipping: %s",
name,
entry,
)
continue
volumes.append(entry)
if isinstance(entry, str):
result = RE_VOLUME.match(entry)
if not result:
log(
"App '%s' has an invalid map entry, skipping: %s",
name,
entry,
)
continue
volumes.append(
{
ATTR_TYPE: result.group(1),
ATTR_READ_ONLY: result.group(2) != "rw",
}
)
# Always update config to clear potentially malformed ones
config[ATTR_MAP] = volumes
# 2023-10 "config" became "homeassistant" so /config can be used for app's public config
if any(volume[ATTR_TYPE] == MappingType.CONFIG for volume in volumes):
if any(
volume
and volume[ATTR_TYPE]
in {
MappingType.APP_CONFIG,
MappingType.ADDON_CONFIG,
MappingType.HOMEASSISTANT_CONFIG,
}
for volume in volumes
):
log(
"App '%s' uses incompatible map options; '%s', '%s', and '%s' are ignored when '%s' is included.",
name,
MappingType.APP_CONFIG,
MappingType.ADDON_CONFIG,
MappingType.HOMEASSISTANT_CONFIG,
MappingType.CONFIG,
)
else:
log(
"App '%s' uses deprecated map option '%s'; use '%s' instead.",
name,
MappingType.CONFIG,
MappingType.HOMEASSISTANT_CONFIG,
)
# 2026-07 addon-based map options replaced by app-based options.
volume_types = {volume[ATTR_TYPE] for volume in volumes}
for app_mapping_type, legacy_mapping_type in (
(MappingType.LOCAL_APPS, MappingType.ADDONS),
(MappingType.ALL_APP_CONFIGS, MappingType.ALL_ADDON_CONFIGS),
(MappingType.APP_CONFIG, MappingType.ADDON_CONFIG),
):
if {app_mapping_type, legacy_mapping_type} <= volume_types:
log(
"App '%s' uses incompatible map options '%s' and '%s'; legacy option '%s' will be ignored.",
name,
app_mapping_type,
legacy_mapping_type,
legacy_mapping_type,
)
return config
return _migrate
# pylint: disable=no-value-for-parameter
_SCHEMA_APP_CONFIG = vol.Schema(
{
vol.Required(ATTR_NAME): str,
vol.Required(ATTR_VERSION): version_tag,
vol.Required(ATTR_SLUG): vol.Match(RE_SLUG_FIELD),
vol.Required(ATTR_DESCRIPTON): str,
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL_COMPAT)],
vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()),
vol.Optional(ATTR_URL): vol.Url(),
vol.Optional(ATTR_STARTUP, default=AppStartup.APPLICATION): vol.Coerce(
AppStartup
),
vol.Optional(ATTR_BOOT, default=AppBootConfig.AUTO): vol.Coerce(AppBootConfig),
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
vol.Optional(ATTR_STAGE, default=AppStage.STABLE): vol.Coerce(AppStage),
vol.Optional(ATTR_PORTS): docker_ports,
vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
vol.Optional(ATTR_WATCHDOG): vol.Match(
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:(\[PORT:\d+\]|\d+).*$"
),
vol.Optional(ATTR_WEBUI): vol.Match(
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
),
vol.Optional(ATTR_INGRESS, default=False): vol.Boolean(),
vol.Optional(ATTR_INGRESS_PORT, default=8099): vol.Any(
network_port, vol.Equal(0)
),
vol.Optional(ATTR_INGRESS_ENTRY): str,
vol.Optional(ATTR_INGRESS_STREAM, default=False): vol.Boolean(),
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): str,
vol.Optional(ATTR_PANEL_TITLE): str,
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(),
vol.Optional(ATTR_HOMEASSISTANT): version_tag,
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_UTS, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
vol.Optional(ATTR_DEVICES): [str],
vol.Optional(ATTR_UDEV, default=False): vol.Boolean(),
vol.Optional(ATTR_TMPFS, default=False): vol.Boolean(),
vol.Optional(ATTR_MAP, default=list): [
vol.Schema(
{
vol.Required(ATTR_TYPE): vol.Coerce(MappingType),
vol.Optional(ATTR_READ_ONLY, default=True): bool,
vol.Optional(ATTR_PATH): str,
}
)
],
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): str},
vol.Optional(ATTR_PRIVILEGED): [vol.Coerce(Capabilities)],
vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(),
vol.Optional(ATTR_FULL_ACCESS, default=False): vol.Boolean(),
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(),
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
vol.Optional(ATTR_USB, default=False): vol.Boolean(),
vol.Optional(ATTR_UART, default=False): vol.Boolean(),
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
vol.Optional(ATTR_REALTIME, default=False): vol.Boolean(),
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL),
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
vol.Optional(ATTR_DISCOVERY): [str],
vol.Optional(ATTR_BACKUP_EXCLUDE): [str],
vol.Optional(ATTR_BACKUP_PRE): str,
vol.Optional(ATTR_BACKUP_POST): str,
vol.Optional(ATTR_BACKUP, default=AppBackupMode.HOT): vol.Coerce(AppBackupMode),
vol.Optional(ATTR_OPTIONS, default={}): dict,
vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
vol.Schema({str: SCHEMA_ELEMENT}),
False,
),
vol.Optional(ATTR_IMAGE): docker_image,
vol.Optional(ATTR_ULIMITS, default=dict): vol.Any(
{str: vol.Coerce(int)}, # Simple format: {name: limit}
{
str: vol.Any(
vol.Coerce(int), # Simple format for individual entries
vol.Schema(
{ # Detailed format for individual entries
vol.Required("soft"): vol.Coerce(int),
vol.Required("hard"): vol.Coerce(int),
}
),
)
},
),
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
vol.Coerce(int), vol.Range(min=10, max=300)
),
vol.Optional(ATTR_JOURNALD, default=False): vol.Boolean(),
vol.Optional(ATTR_BREAKING_VERSIONS, default=list): [version_tag],
},
extra=vol.REMOVE_EXTRA,
)
def _build_app_config_schema(log: Callable[..., None]):
"""Build the store app config schema, routing advisories through log."""
return vol.All(_migrate_app_config(log), _warn_app_config(log), _SCHEMA_APP_CONFIG)
# Store app configs are validated on every store reload for every app in every
# repository, including ones the user has not installed. The advisories they
# raise are only actionable for a local app's author or a developer testing on
# the dev channel, so the store data layer validates with the verbose schema
# only in those cases and uses the quiet (debug-level) schema otherwise, to keep
# them out of regular users' logs.
SCHEMA_APP_CONFIG = _build_app_config_schema(_LOGGER.warning)
SCHEMA_APP_CONFIG_QUIET = _build_app_config_schema(_LOGGER.debug)
# pylint: disable=no-value-for-parameter
SCHEMA_BUILD_CONFIG = vol.Schema(
{
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Any(
vol.Match(RE_DOCKER_IMAGE_BUILD),
vol.Schema({vol.In(ARCH_ALL_COMPAT): vol.Match(RE_DOCKER_IMAGE_BUILD)}),
),
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({str: str}),
vol.Optional(ATTR_LABELS, default=dict): vol.Schema({str: str}),
},
extra=vol.REMOVE_EXTRA,
)
SCHEMA_TRANSLATION_CONFIGURATION = vol.Schema(
{
vol.Required(ATTR_NAME): str,
vol.Optional(ATTR_DESCRIPTON): vol.Maybe(str),
vol.Optional(ATTR_FIELDS): {str: vol.Self},
},
extra=vol.REMOVE_EXTRA,
)
SCHEMA_APP_TRANSLATIONS = vol.Schema(
{
vol.Optional(ATTR_CONFIGURATION): {str: SCHEMA_TRANSLATION_CONFIGURATION},
vol.Optional(ATTR_NETWORK): {str: str},
},
extra=vol.REMOVE_EXTRA,
)
# pylint: disable=no-value-for-parameter
SCHEMA_APP_USER = vol.Schema(
{
vol.Required(ATTR_VERSION): version_tag,
vol.Optional(ATTR_IMAGE): docker_image,
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): uuid_match,
vol.Optional(ATTR_ACCESS_TOKEN): token,
vol.Optional(ATTR_INGRESS_TOKEN, default=secrets.token_urlsafe): str,
vol.Optional(ATTR_OPTIONS, default=dict): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT): vol.Coerce(AppBoot),
vol.Optional(ATTR_NETWORK): docker_ports,
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str),
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str),
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
vol.Optional(ATTR_SYSTEM_MANAGED, default=False): vol.Boolean(),
vol.Optional(ATTR_SYSTEM_MANAGED_CONFIG_ENTRY, default=None): vol.Maybe(str),
},
extra=vol.REMOVE_EXTRA,
)
SCHEMA_APP_SYSTEM = vol.All(
# Installed app config is re-validated from apps.json on every startup. It
# was already migrated (and any advisory logged) when read from the store,
# so keep these at debug level to avoid repeating them on each boot.
_migrate_app_config(_LOGGER.debug),
_SCHEMA_APP_CONFIG.extend(
{
# The source location is owned by the store and resolved at runtime
# (see App.path_location); it is intentionally not persisted here.
# REMOVE_EXTRA drops any stale value left in older apps.json files.
vol.Required(ATTR_REPOSITORY): str,
vol.Required(ATTR_TRANSLATIONS, default=dict): {
str: SCHEMA_APP_TRANSLATIONS
},
}
),
)
SCHEMA_APPS_FILE = vol.Schema(
{
vol.Optional(ATTR_USER, default=dict): {str: SCHEMA_APP_USER},
vol.Optional(ATTR_SYSTEM, default=dict): {str: SCHEMA_APP_SYSTEM},
},
extra=vol.REMOVE_EXTRA,
)
SCHEMA_APP_BACKUP = vol.Schema(
{
vol.Required(ATTR_USER): SCHEMA_APP_USER,
vol.Required(ATTR_SYSTEM): SCHEMA_APP_SYSTEM,
vol.Required(ATTR_STATE): vol.Coerce(AppState),
vol.Required(ATTR_VERSION): version_tag,
},
extra=vol.REMOVE_EXTRA,
)