diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 958f54a77ff..ae5af2f3a99 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .model import Config, Integration +from .model import Config, Integration, IntegrationType BASE = """ # This file is generated by script/hassfest/codeowners.py @@ -65,7 +65,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config) for domain in sorted(integrations): integration = integrations[domain] - if integration.integration_type == "virtual": + if integration.integration_type == IntegrationType.VIRTUAL: continue codeowners = integration.manifest["codeowners"] diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 1f8b7d1139b..54ca4230001 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -6,7 +6,7 @@ import json from typing import Any from .brand import validate as validate_brands -from .model import Brand, Config, Integration +from .model import Brand, Config, Integration, IntegrationType from .serializer import format_python_namespace UNIQUE_ID_IGNORE = {"huawei_lte", "mqtt", "adguard"} @@ -75,7 +75,7 @@ def _generate_and_validate(integrations: dict[str, Integration], config: Config) _validate_integration(config, integration) - if integration.integration_type == "helper": + if integration.integration_type == IntegrationType.HELPER: domains["helper"].append(domain) else: domains["integration"].append(domain) @@ -94,8 +94,8 @@ def _populate_brand_integrations( for domain in sub_integrations: integration = integrations.get(domain) if not integration or integration.integration_type in ( - "entity", - "system", + IntegrationType.ENTITY, + IntegrationType.SYSTEM, ): continue metadata: dict[str, Any] = { @@ -170,7 +170,10 @@ def _generate_integrations( result["integration"][domain] = metadata else: # integration integration = integrations[domain] - if integration.integration_type in ("entity", "system"): + if integration.integration_type in ( + IntegrationType.ENTITY, + IntegrationType.SYSTEM, + ): continue if integration.translated_name: @@ -180,7 +183,7 @@ def _generate_integrations( metadata["integration_type"] = integration.integration_type - if integration.integration_type == "virtual": + if integration.integration_type == IntegrationType.VIRTUAL: if integration.supported_by: metadata["supported_by"] = integration.supported_by if integration.iot_standards: @@ -195,7 +198,7 @@ def _generate_integrations( ): metadata["single_config_entry"] = single_config_entry - if integration.integration_type == "helper": + if integration.integration_type == IntegrationType.HELPER: result["helper"][domain] = metadata else: result["integration"][domain] = metadata diff --git a/script/hassfest/core_files.py b/script/hassfest/core_files.py index ac480de11a3..24e9315a02b 100644 --- a/script/hassfest/core_files.py +++ b/script/hassfest/core_files.py @@ -4,7 +4,7 @@ import re from homeassistant.util.yaml import load_yaml_dict -from .model import Config, Integration +from .model import Config, Integration, IntegrationType # Non-entity-platform components that belong in base_platforms EXTRA_BASE_PLATFORMS = {"diagnostics"} @@ -29,7 +29,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: entity_platforms = { integration.domain for integration in integrations.values() - if integration.manifest.get("integration_type") == "entity" + if integration.integration_type == IntegrationType.ENTITY and integration.domain != "tag" } diff --git a/script/hassfest/icons.py b/script/hassfest/icons.py index 6d2187e3fe6..83efb6d6764 100644 --- a/script/hassfest/icons.py +++ b/script/hassfest/icons.py @@ -11,7 +11,7 @@ from voluptuous.humanize import humanize_error import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import convert_shorthand_service_icon -from .model import Config, Integration +from .model import Config, Integration, IntegrationType from .translations import translation_key_validator @@ -141,7 +141,7 @@ TRIGGER_ICONS_SCHEMA = cv.schema_with_slug_keys( def icon_schema( - core_integration: bool, integration_type: str, no_entity_platform: bool + core_integration: bool, integration_type: IntegrationType, no_entity_platform: bool ) -> vol.Schema: """Create an icon schema.""" @@ -189,8 +189,12 @@ def icon_schema( } ) - if integration_type in ("entity", "helper", "system"): - if integration_type != "entity" or no_entity_platform: + if integration_type in ( + IntegrationType.ENTITY, + IntegrationType.HELPER, + IntegrationType.SYSTEM, + ): + if integration_type != IntegrationType.ENTITY or no_entity_platform: field = vol.Optional("entity_component") else: field = vol.Required("entity_component") @@ -207,7 +211,7 @@ def icon_schema( ) } ) - if integration_type not in ("entity", "system"): + if integration_type not in (IntegrationType.ENTITY, IntegrationType.SYSTEM): schema = schema.extend( { vol.Optional("entity"): vol.All( diff --git a/script/hassfest/integration_info.py b/script/hassfest/integration_info.py index 8747e256be7..85a327e147d 100644 --- a/script/hassfest/integration_info.py +++ b/script/hassfest/integration_info.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .model import Config, Integration +from .model import Config, Integration, IntegrationType from .serializer import format_python @@ -12,12 +12,12 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: if config.specific_integrations: return - int_type = "entity" + int_type = IntegrationType.ENTITY domains = [ integration.domain for integration in integrations.values() - if integration.manifest.get("integration_type") == int_type + if integration.integration_type == int_type # Tag is type "entity" but has no entity platform and integration.domain != "tag" ] @@ -36,7 +36,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: def generate(integrations: dict[str, Integration], config: Config) -> None: """Generate integration file.""" - int_type = "entity" + int_type = IntegrationType.ENTITY filename = "entity_platforms" platform_path = config.root / f"homeassistant/generated/{filename}.py" platform_path.write_text(config.cache[f"integrations_{int_type}"]) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 6e6a5e2d84d..dacc34d3dea 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -21,7 +21,7 @@ from homeassistant.const import Platform from homeassistant.helpers import config_validation as cv from script.util import sort_manifest as util_sort_manifest -from .model import Config, Integration, ScaledQualityScaleTiers +from .model import Config, Integration, IntegrationType, ScaledQualityScaleTiers DOCUMENTATION_URL_SCHEMA = "https" DOCUMENTATION_URL_HOST = "www.home-assistant.io" @@ -206,15 +206,7 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema( vol.Required("domain"): str, vol.Required("name"): str, vol.Optional("integration_type", default="hub"): vol.In( - [ - "device", - "entity", - "hardware", - "helper", - "hub", - "service", - "system", - ] + [t.value for t in IntegrationType if t != IntegrationType.VIRTUAL] ), vol.Optional("config_flow"): bool, vol.Optional("mqtt"): [str], @@ -311,7 +303,7 @@ VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, vol.Required("name"): str, - vol.Required("integration_type"): "virtual", + vol.Required("integration_type"): IntegrationType.VIRTUAL.value, vol.Exclusive("iot_standards", "virtual_integration"): [ vol.Any("homekit", "zigbee", "zwave") ], @@ -322,7 +314,7 @@ VIRTUAL_INTEGRATION_MANIFEST_SCHEMA = vol.Schema( def manifest_schema(value: dict[str, Any]) -> vol.Schema: """Validate integration manifest.""" - if value.get("integration_type") == "virtual": + if value.get("integration_type") == IntegrationType.VIRTUAL: return VIRTUAL_INTEGRATION_MANIFEST_SCHEMA(value) return INTEGRATION_MANIFEST_SCHEMA(value) @@ -373,12 +365,12 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No if ( domain not in NO_IOT_CLASS and "iot_class" not in integration.manifest - and integration.manifest.get("integration_type") != "virtual" + and integration.integration_type != IntegrationType.VIRTUAL ): integration.add_error("manifest", "Domain is missing an IoT Class") if ( - integration.manifest.get("integration_type") == "virtual" + integration.integration_type == IntegrationType.VIRTUAL and (supported_by := integration.manifest.get("supported_by")) and not (core_components_dir / supported_by).exists() ): diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 06e1df4caf3..494d727fccc 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from enum import IntEnum +from enum import IntEnum, StrEnum import json import pathlib from typing import Any, Literal @@ -200,9 +200,15 @@ class Integration: return self.manifest.get("supported_by", {}) @property - def integration_type(self) -> str: + def integration_type(self) -> IntegrationType: """Get integration_type.""" - return self.manifest.get("integration_type", "hub") + integration_type = self.manifest.get("integration_type", "hub") + try: + return IntegrationType(integration_type) + except ValueError: + # The manifest validation will catch this as an error, so we can default to + # a valid value here to avoid ValueErrors in other plugins + return IntegrationType.HUB @property def iot_class(self) -> str | None: @@ -248,6 +254,19 @@ class Integration: self.manifest_path = manifest_path +class IntegrationType(StrEnum): + """Supported integration types.""" + + DEVICE = "device" + ENTITY = "entity" + HARDWARE = "hardware" + HELPER = "helper" + HUB = "hub" + SERVICE = "service" + SYSTEM = "system" + VIRTUAL = "virtual" + + class ScaledQualityScaleTiers(IntEnum): """Supported manifest quality scales.""" diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 94b23e32b55..3e5b852d5b4 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -11,7 +11,7 @@ from homeassistant.const import Platform from homeassistant.exceptions import HomeAssistantError from homeassistant.util.yaml import load_yaml_dict -from .model import Config, Integration, ScaledQualityScaleTiers +from .model import Config, Integration, IntegrationType, ScaledQualityScaleTiers from .quality_scale_validation import ( RuleValidationProtocol, action_setup, @@ -2200,7 +2200,7 @@ def validate_iqs_file(config: Config, integration: Integration) -> None: if ( integration.domain not in INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE and integration.domain not in NO_QUALITY_SCALE - and integration.integration_type != "virtual" + and integration.integration_type != IntegrationType.VIRTUAL ): integration.add_error( "quality_scale", @@ -2218,7 +2218,7 @@ def validate_iqs_file(config: Config, integration: Integration) -> None: ) return return - if integration.integration_type == "virtual": + if integration.integration_type == IntegrationType.VIRTUAL: integration.add_error( "quality_scale", "Virtual integrations are not allowed to have a quality scale file.", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 5e55b9a1ca8..cd24b42879d 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -14,7 +14,7 @@ from voluptuous.humanize import humanize_error import homeassistant.helpers.config_validation as cv from script.translations import upload -from .model import Config, Integration +from .model import Config, Integration, IntegrationType UNDEFINED = 0 REQUIRED = 1 @@ -345,7 +345,9 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: flow_title=REMOVED, require_step_title=False, mandatory_description=( - "user" if integration.integration_type == "helper" else None + "user" + if integration.integration_type == IntegrationType.HELPER + else None ), ), vol.Optional("config_subentries"): cv.schema_with_slug_keys(