2024-01-19 15:56:56 +00:00
|
|
|
"""Validate integration icon translation files."""
|
2024-03-08 15:36:11 +00:00
|
|
|
|
2024-01-19 15:56:56 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
import orjson
|
|
|
|
import voluptuous as vol
|
|
|
|
from voluptuous.humanize import humanize_error
|
|
|
|
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
|
|
|
|
from .model import Config, Integration
|
|
|
|
from .translations import translation_key_validator
|
|
|
|
|
|
|
|
|
|
|
|
def icon_value_validator(value: Any) -> str:
|
|
|
|
"""Validate that the icon is a valid icon."""
|
|
|
|
value = cv.string_with_no_html(value)
|
|
|
|
if not value.startswith("mdi:"):
|
|
|
|
raise vol.Invalid(
|
|
|
|
"The icon needs to be a valid icon from Material Design Icons and start with `mdi:`"
|
|
|
|
)
|
|
|
|
return str(value)
|
|
|
|
|
|
|
|
|
|
|
|
def require_default_icon_validator(value: dict) -> dict:
|
|
|
|
"""Validate that a default icon is set."""
|
|
|
|
if "_" not in value:
|
|
|
|
raise vol.Invalid(
|
|
|
|
"An entity component needs to have a default icon defined with `_`"
|
|
|
|
)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2024-01-21 11:02:15 +00:00
|
|
|
def ensure_not_same_as_default(value: dict) -> dict:
|
|
|
|
"""Validate an icon isn't the same as its default icon."""
|
|
|
|
for translation_key, section in value.items():
|
|
|
|
if (default := section.get("default")) and (states := section.get("state")):
|
|
|
|
for state, icon in states.items():
|
|
|
|
if icon == default:
|
|
|
|
raise vol.Invalid(
|
|
|
|
f"The icon for state `{translation_key}.{state}` is the"
|
|
|
|
" same as the default icon and thus can be removed"
|
|
|
|
)
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2024-01-19 15:56:56 +00:00
|
|
|
def icon_schema(integration_type: str) -> vol.Schema:
|
|
|
|
"""Create a icon schema."""
|
|
|
|
|
|
|
|
state_validator = cv.schema_with_slug_keys(
|
|
|
|
icon_value_validator,
|
|
|
|
slug_validator=translation_key_validator,
|
|
|
|
)
|
|
|
|
|
|
|
|
def icon_schema_slug(marker: type[vol.Marker]) -> dict[vol.Marker, Any]:
|
|
|
|
return {
|
|
|
|
marker("default"): icon_value_validator,
|
|
|
|
vol.Optional("state"): state_validator,
|
2024-01-21 11:02:15 +00:00
|
|
|
vol.Optional("state_attributes"): vol.All(
|
|
|
|
cv.schema_with_slug_keys(
|
|
|
|
{
|
|
|
|
marker("default"): icon_value_validator,
|
|
|
|
marker("state"): state_validator,
|
|
|
|
},
|
|
|
|
slug_validator=translation_key_validator,
|
|
|
|
),
|
|
|
|
ensure_not_same_as_default,
|
2024-01-19 15:56:56 +00:00
|
|
|
),
|
|
|
|
}
|
|
|
|
|
2024-01-29 18:26:55 +00:00
|
|
|
schema = vol.Schema(
|
2024-01-19 15:56:56 +00:00
|
|
|
{
|
|
|
|
vol.Optional("services"): state_validator,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2024-01-29 18:26:55 +00:00
|
|
|
if integration_type in ("entity", "helper", "system"):
|
2024-02-04 21:57:11 +00:00
|
|
|
if integration_type != "entity":
|
|
|
|
field = vol.Optional("entity_component")
|
|
|
|
else:
|
|
|
|
field = vol.Required("entity_component")
|
2024-01-29 18:26:55 +00:00
|
|
|
schema = schema.extend(
|
2024-01-19 15:56:56 +00:00
|
|
|
{
|
2024-02-04 21:57:11 +00:00
|
|
|
field: vol.All(
|
2024-01-19 15:56:56 +00:00
|
|
|
cv.schema_with_slug_keys(
|
|
|
|
icon_schema_slug(vol.Required),
|
|
|
|
slug_validator=vol.Any("_", cv.slug),
|
|
|
|
),
|
|
|
|
require_default_icon_validator,
|
2024-01-21 11:02:15 +00:00
|
|
|
ensure_not_same_as_default,
|
2024-01-19 15:56:56 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
2024-01-29 18:26:55 +00:00
|
|
|
if integration_type not in ("entity", "system"):
|
|
|
|
schema = schema.extend(
|
|
|
|
{
|
|
|
|
vol.Optional("entity"): vol.All(
|
2024-01-21 11:02:15 +00:00
|
|
|
cv.schema_with_slug_keys(
|
2024-01-29 18:26:55 +00:00
|
|
|
cv.schema_with_slug_keys(
|
|
|
|
icon_schema_slug(vol.Optional),
|
|
|
|
slug_validator=translation_key_validator,
|
|
|
|
),
|
|
|
|
slug_validator=cv.slug,
|
2024-01-21 11:02:15 +00:00
|
|
|
),
|
2024-01-29 18:26:55 +00:00
|
|
|
ensure_not_same_as_default,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
return schema
|
2024-01-19 15:56:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
def validate_icon_file(config: Config, integration: Integration) -> None: # noqa: C901
|
|
|
|
"""Validate icon file for integration."""
|
|
|
|
icons_file = integration.path / "icons.json"
|
|
|
|
if not icons_file.is_file():
|
|
|
|
return
|
|
|
|
|
|
|
|
name = str(icons_file.relative_to(integration.path))
|
|
|
|
|
|
|
|
try:
|
|
|
|
icons = orjson.loads(icons_file.read_text())
|
|
|
|
except ValueError as err:
|
|
|
|
integration.add_error("icons", f"Invalid JSON in {name}: {err}")
|
|
|
|
return
|
|
|
|
|
|
|
|
schema = icon_schema(integration.integration_type)
|
|
|
|
|
|
|
|
try:
|
|
|
|
schema(icons)
|
|
|
|
except vol.Invalid as err:
|
|
|
|
integration.add_error("icons", f"Invalid {name}: {humanize_error(icons, err)}")
|
|
|
|
|
|
|
|
|
|
|
|
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
|
|
|
"""Handle JSON files inside integrations."""
|
|
|
|
for integration in integrations.values():
|
|
|
|
validate_icon_file(config, integration)
|