2019-04-13 20:17:01 +00:00
|
|
|
"""Manifest validation."""
|
2021-03-18 21:58:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-04-29 09:43:23 +00:00
|
|
|
from pathlib import Path
|
2020-01-27 09:42:26 +00:00
|
|
|
from urllib.parse import urlparse
|
2019-04-13 20:17:01 +00:00
|
|
|
|
2021-05-17 13:48:41 +00:00
|
|
|
from awesomeversion import (
|
|
|
|
AwesomeVersion,
|
|
|
|
AwesomeVersionException,
|
|
|
|
AwesomeVersionStrategy,
|
|
|
|
)
|
2019-04-13 20:17:01 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
from voluptuous.humanize import humanize_error
|
|
|
|
|
2022-03-16 10:21:51 +00:00
|
|
|
from homeassistant.const import Platform
|
2021-12-19 08:09:21 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv
|
|
|
|
|
2021-04-29 09:43:23 +00:00
|
|
|
from .model import Config, Integration
|
2019-04-13 20:17:01 +00:00
|
|
|
|
2020-01-27 09:42:26 +00:00
|
|
|
DOCUMENTATION_URL_SCHEMA = "https"
|
|
|
|
DOCUMENTATION_URL_HOST = "www.home-assistant.io"
|
|
|
|
DOCUMENTATION_URL_PATH_PREFIX = "/integrations/"
|
2020-04-16 16:00:04 +00:00
|
|
|
DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"}
|
2020-01-27 09:42:26 +00:00
|
|
|
|
2020-04-05 15:48:55 +00:00
|
|
|
SUPPORTED_QUALITY_SCALES = ["gold", "internal", "platinum", "silver"]
|
2021-04-15 08:21:38 +00:00
|
|
|
SUPPORTED_IOT_CLASSES = [
|
|
|
|
"assumed_state",
|
|
|
|
"calculated",
|
|
|
|
"cloud_polling",
|
|
|
|
"cloud_push",
|
|
|
|
"local_polling",
|
|
|
|
"local_push",
|
|
|
|
]
|
|
|
|
|
|
|
|
# List of integrations that are supposed to have no IoT class
|
|
|
|
NO_IOT_CLASS = [
|
2022-03-16 10:21:51 +00:00
|
|
|
*{platform.value for platform in Platform},
|
2021-04-15 08:21:38 +00:00
|
|
|
"api",
|
2022-04-30 15:06:43 +00:00
|
|
|
"application_credentials",
|
2021-04-15 08:21:38 +00:00
|
|
|
"auth",
|
|
|
|
"automation",
|
|
|
|
"blueprint",
|
|
|
|
"color_extractor",
|
|
|
|
"config",
|
|
|
|
"configurator",
|
|
|
|
"counter",
|
|
|
|
"default_config",
|
|
|
|
"device_automation",
|
|
|
|
"device_tracker",
|
2022-01-18 04:42:18 +00:00
|
|
|
"diagnostics",
|
2021-04-15 08:21:38 +00:00
|
|
|
"discovery",
|
|
|
|
"downloader",
|
|
|
|
"ffmpeg",
|
|
|
|
"frontend",
|
2022-05-26 20:15:44 +00:00
|
|
|
"hardkernel",
|
2022-05-25 18:39:15 +00:00
|
|
|
"hardware",
|
2021-04-15 08:21:38 +00:00
|
|
|
"history",
|
|
|
|
"homeassistant",
|
2022-06-14 06:25:11 +00:00
|
|
|
"homeassistant_yellow",
|
2021-04-15 08:21:38 +00:00
|
|
|
"image",
|
|
|
|
"input_boolean",
|
2021-12-20 15:18:58 +00:00
|
|
|
"input_button",
|
2021-04-15 08:21:38 +00:00
|
|
|
"input_datetime",
|
|
|
|
"input_number",
|
|
|
|
"input_select",
|
|
|
|
"input_text",
|
|
|
|
"intent_script",
|
|
|
|
"intent",
|
|
|
|
"logbook",
|
|
|
|
"logger",
|
|
|
|
"lovelace",
|
|
|
|
"map",
|
|
|
|
"media_source",
|
|
|
|
"my",
|
|
|
|
"onboarding",
|
|
|
|
"panel_custom",
|
|
|
|
"panel_iframe",
|
|
|
|
"plant",
|
|
|
|
"profiler",
|
|
|
|
"proxy",
|
|
|
|
"python_script",
|
2022-05-25 18:39:15 +00:00
|
|
|
"raspberry_pi",
|
2022-07-20 10:06:52 +00:00
|
|
|
"repairs",
|
2021-04-15 08:21:38 +00:00
|
|
|
"safe_mode",
|
|
|
|
"script",
|
|
|
|
"search",
|
|
|
|
"system_health",
|
|
|
|
"system_log",
|
|
|
|
"tag",
|
|
|
|
"timer",
|
|
|
|
"trace",
|
|
|
|
"webhook",
|
|
|
|
"websocket_api",
|
|
|
|
"zone",
|
|
|
|
]
|
2020-01-07 16:21:56 +00:00
|
|
|
|
2020-01-27 09:42:26 +00:00
|
|
|
|
|
|
|
def documentation_url(value: str) -> str:
|
|
|
|
"""Validate that a documentation url has the correct path and domain."""
|
|
|
|
if value in DOCUMENTATION_URL_EXCEPTIONS:
|
|
|
|
return value
|
|
|
|
|
|
|
|
parsed_url = urlparse(value)
|
2020-05-05 18:00:00 +00:00
|
|
|
if parsed_url.scheme != DOCUMENTATION_URL_SCHEMA:
|
2020-01-27 09:42:26 +00:00
|
|
|
raise vol.Invalid("Documentation url is not prefixed with https")
|
2020-04-16 16:00:04 +00:00
|
|
|
if parsed_url.netloc == DOCUMENTATION_URL_HOST and not parsed_url.path.startswith(
|
|
|
|
DOCUMENTATION_URL_PATH_PREFIX
|
|
|
|
):
|
2020-01-27 09:42:26 +00:00
|
|
|
raise vol.Invalid(
|
|
|
|
"Documentation url does not begin with www.home-assistant.io/integrations"
|
|
|
|
)
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2020-12-31 00:06:26 +00:00
|
|
|
def verify_lowercase(value: str):
|
|
|
|
"""Verify a value is lowercase."""
|
|
|
|
if value.lower() != value:
|
|
|
|
raise vol.Invalid("Value needs to be lowercase")
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def verify_uppercase(value: str):
|
|
|
|
"""Verify a value is uppercase."""
|
|
|
|
if value.upper() != value:
|
|
|
|
raise vol.Invalid("Value needs to be uppercase")
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2021-01-25 12:31:14 +00:00
|
|
|
def verify_version(value: str):
|
|
|
|
"""Verify the version."""
|
2021-05-17 13:48:41 +00:00
|
|
|
try:
|
|
|
|
AwesomeVersion(
|
|
|
|
value,
|
|
|
|
[
|
|
|
|
AwesomeVersionStrategy.CALVER,
|
|
|
|
AwesomeVersionStrategy.SEMVER,
|
|
|
|
AwesomeVersionStrategy.SIMPLEVER,
|
|
|
|
AwesomeVersionStrategy.BUILDVER,
|
|
|
|
AwesomeVersionStrategy.PEP440,
|
|
|
|
],
|
2021-01-25 12:31:14 +00:00
|
|
|
)
|
2021-05-17 13:48:41 +00:00
|
|
|
except AwesomeVersionException:
|
|
|
|
raise vol.Invalid(f"'{value}' is not a valid version.")
|
2021-01-25 12:31:14 +00:00
|
|
|
return value
|
|
|
|
|
|
|
|
|
2021-04-16 23:32:12 +00:00
|
|
|
def verify_wildcard(value: str):
|
|
|
|
"""Verify the matcher contains a wildcard."""
|
|
|
|
if "*" not in value:
|
|
|
|
raise vol.Invalid(f"'{value}' needs to contain a wildcard matcher")
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
MANIFEST_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required("domain"): str,
|
|
|
|
vol.Required("name"): str,
|
2022-05-25 18:39:15 +00:00
|
|
|
vol.Optional("integration_type"): vol.In(["hardware", "helper"]),
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional("config_flow"): bool,
|
2020-10-07 16:30:51 +00:00
|
|
|
vol.Optional("mqtt"): [str],
|
2020-09-11 10:19:21 +00:00
|
|
|
vol.Optional("zeroconf"): [
|
|
|
|
vol.Any(
|
|
|
|
str,
|
2021-12-19 08:09:21 +00:00
|
|
|
vol.All(
|
|
|
|
cv.deprecated("macaddress"),
|
|
|
|
cv.deprecated("model"),
|
|
|
|
cv.deprecated("manufacturer"),
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Required("type"): str,
|
|
|
|
vol.Optional("macaddress"): vol.All(
|
|
|
|
str, verify_uppercase, verify_wildcard
|
|
|
|
),
|
|
|
|
vol.Optional("manufacturer"): vol.All(
|
|
|
|
str, verify_lowercase
|
|
|
|
),
|
|
|
|
vol.Optional("model"): vol.All(str, verify_lowercase),
|
|
|
|
vol.Optional("name"): vol.All(str, verify_lowercase),
|
|
|
|
vol.Optional("properties"): vol.Schema(
|
|
|
|
{str: verify_lowercase}
|
|
|
|
),
|
|
|
|
}
|
|
|
|
),
|
2020-09-11 10:19:21 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional("ssdp"): vol.Schema(
|
2019-11-02 19:30:09 +00:00
|
|
|
vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))])
|
2019-07-31 19:25:30 +00:00
|
|
|
),
|
2022-07-08 23:55:31 +00:00
|
|
|
vol.Optional("bluetooth"): [
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional("service_uuid"): vol.All(str, verify_lowercase),
|
2022-07-24 21:39:53 +00:00
|
|
|
vol.Optional("service_data_uuid"): vol.All(str, verify_lowercase),
|
2022-07-08 23:55:31 +00:00
|
|
|
vol.Optional("local_name"): vol.All(str),
|
|
|
|
vol.Optional("manufacturer_id"): int,
|
2022-07-17 22:25:45 +00:00
|
|
|
vol.Optional("manufacturer_data_start"): [int],
|
2022-07-08 23:55:31 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
],
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}),
|
2021-01-14 08:09:08 +00:00
|
|
|
vol.Optional("dhcp"): [
|
|
|
|
vol.Schema(
|
|
|
|
{
|
2021-04-16 23:32:12 +00:00
|
|
|
vol.Optional("macaddress"): vol.All(
|
|
|
|
str, verify_uppercase, verify_wildcard
|
|
|
|
),
|
2021-01-14 08:09:08 +00:00
|
|
|
vol.Optional("hostname"): vol.All(str, verify_lowercase),
|
2022-02-15 17:02:52 +00:00
|
|
|
vol.Optional("registered_devices"): cv.boolean,
|
2021-01-14 08:09:08 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
],
|
2021-08-20 19:04:18 +00:00
|
|
|
vol.Optional("usb"): [
|
|
|
|
vol.Schema(
|
|
|
|
{
|
|
|
|
vol.Optional("vid"): vol.All(str, verify_uppercase),
|
|
|
|
vol.Optional("pid"): vol.All(str, verify_uppercase),
|
2021-08-26 13:59:02 +00:00
|
|
|
vol.Optional("serial_number"): vol.All(str, verify_lowercase),
|
|
|
|
vol.Optional("manufacturer"): vol.All(str, verify_lowercase),
|
|
|
|
vol.Optional("description"): vol.All(str, verify_lowercase),
|
2021-08-21 19:56:49 +00:00
|
|
|
vol.Optional("known_devices"): [str],
|
2021-08-20 19:04:18 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
],
|
2020-01-27 09:42:26 +00:00
|
|
|
vol.Required("documentation"): vol.All(
|
|
|
|
vol.Url(), documentation_url # pylint: disable=no-value-for-parameter
|
|
|
|
),
|
2020-05-05 18:00:00 +00:00
|
|
|
vol.Optional(
|
|
|
|
"issue_tracker"
|
|
|
|
): vol.Url(), # pylint: disable=no-value-for-parameter
|
2020-01-07 16:21:56 +00:00
|
|
|
vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES),
|
2020-03-16 21:47:44 +00:00
|
|
|
vol.Optional("requirements"): [str],
|
|
|
|
vol.Optional("dependencies"): [str],
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional("after_dependencies"): [str],
|
|
|
|
vol.Required("codeowners"): [str],
|
2022-01-28 21:37:53 +00:00
|
|
|
vol.Optional("loggers"): [str],
|
2020-08-26 08:20:14 +00:00
|
|
|
vol.Optional("disabled"): str,
|
2021-04-15 08:21:38 +00:00
|
|
|
vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES),
|
2022-02-22 07:42:57 +00:00
|
|
|
vol.Optional("supported_brands"): vol.Schema({str: str}),
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
)
|
2019-04-13 20:17:01 +00:00
|
|
|
|
2021-01-25 12:31:14 +00:00
|
|
|
CUSTOM_INTEGRATION_MANIFEST_SCHEMA = MANIFEST_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Optional("version"): vol.All(str, verify_version),
|
2022-02-22 07:42:57 +00:00
|
|
|
vol.Remove("supported_brands"): dict,
|
2021-01-25 12:31:14 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def validate_version(integration: Integration):
|
|
|
|
"""
|
|
|
|
Validate the version of the integration.
|
|
|
|
|
|
|
|
Will be removed when the version key is no longer optional for custom integrations.
|
|
|
|
"""
|
|
|
|
if not integration.manifest.get("version"):
|
2021-05-17 13:48:41 +00:00
|
|
|
integration.add_error("manifest", "No 'version' key in the manifest file.")
|
2021-01-25 12:31:14 +00:00
|
|
|
return
|
|
|
|
|
2019-04-13 20:17:01 +00:00
|
|
|
|
2021-04-29 09:43:23 +00:00
|
|
|
def validate_manifest(integration: Integration, core_components_dir: Path) -> None:
|
2019-04-13 20:17:01 +00:00
|
|
|
"""Validate manifest."""
|
2021-04-15 08:21:38 +00:00
|
|
|
if not integration.manifest:
|
|
|
|
return
|
|
|
|
|
2019-04-13 20:17:01 +00:00
|
|
|
try:
|
2021-01-25 12:31:14 +00:00
|
|
|
if integration.core:
|
|
|
|
MANIFEST_SCHEMA(integration.manifest)
|
|
|
|
else:
|
|
|
|
CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest)
|
2019-04-13 20:17:01 +00:00
|
|
|
except vol.Invalid as err:
|
|
|
|
integration.add_error(
|
2020-04-05 15:48:55 +00:00
|
|
|
"manifest", f"Invalid manifest: {humanize_error(integration.manifest, err)}"
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-04-13 20:17:01 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if integration.manifest["domain"] != integration.path.name:
|
|
|
|
integration.add_error("manifest", "Domain does not match dir name")
|
2019-04-13 20:17:01 +00:00
|
|
|
|
2021-04-29 09:43:23 +00:00
|
|
|
if (
|
|
|
|
not integration.core
|
|
|
|
and (core_components_dir / integration.manifest["domain"]).exists()
|
|
|
|
):
|
|
|
|
integration.add_warning(
|
|
|
|
"manifest", "Domain collides with built-in core integration"
|
|
|
|
)
|
|
|
|
|
2021-04-15 08:21:38 +00:00
|
|
|
if (
|
|
|
|
integration.manifest["domain"] in NO_IOT_CLASS
|
|
|
|
and "iot_class" in integration.manifest
|
|
|
|
):
|
|
|
|
integration.add_error("manifest", "Domain should not have an IoT Class")
|
|
|
|
|
|
|
|
if (
|
|
|
|
integration.manifest["domain"] not in NO_IOT_CLASS
|
|
|
|
and "iot_class" not in integration.manifest
|
|
|
|
):
|
|
|
|
integration.add_error("manifest", "Domain is missing an IoT Class")
|
|
|
|
|
2022-02-22 07:42:57 +00:00
|
|
|
for domain, _name in integration.manifest.get("supported_brands", {}).items():
|
|
|
|
if (core_components_dir / domain).exists():
|
|
|
|
integration.add_warning(
|
|
|
|
"manifest",
|
|
|
|
f"Supported brand domain {domain} collides with built-in core integration",
|
|
|
|
)
|
|
|
|
|
2021-01-25 12:31:14 +00:00
|
|
|
if not integration.core:
|
|
|
|
validate_version(integration)
|
|
|
|
|
2019-04-13 20:17:01 +00:00
|
|
|
|
2021-04-29 09:43:23 +00:00
|
|
|
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
2019-04-13 20:17:01 +00:00
|
|
|
"""Handle all integrations manifests."""
|
2021-04-29 09:43:23 +00:00
|
|
|
core_components_dir = config.root / "homeassistant/components"
|
2019-04-13 20:17:01 +00:00
|
|
|
for integration in integrations.values():
|
2021-04-29 09:43:23 +00:00
|
|
|
validate_manifest(integration, core_components_dir)
|