"""Config flow for MQTT."""

import asyncio
from collections import OrderedDict
from collections.abc import Callable, Mapping
from copy import deepcopy
from dataclasses import dataclass
from enum import IntEnum
import json
import logging
from ssl import PROTOCOL_TLS_CLIENT, SSLContext, SSLError
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast, override
from uuid import uuid4

from cryptography.hazmat.primitives.serialization import (
    Encoding,
    NoEncryption,
    PrivateFormat,
    load_der_private_key,
    load_pem_private_key,
)
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
import voluptuous as vol
import yaml

from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.button import ButtonDeviceClass
from homeassistant.components.climate import (
    DEFAULT_MAX_HUMIDITY,
    DEFAULT_MAX_TEMP,
    DEFAULT_MIN_HUMIDITY,
    DEFAULT_MIN_TEMP,
    PRESET_NONE,
)
from homeassistant.components.cover import CoverDeviceClass
from homeassistant.components.file_upload import process_uploaded_file
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
from homeassistant.components.image import DEFAULT_CONTENT_TYPE
from homeassistant.components.light import (
    DEFAULT_MAX_KELVIN,
    DEFAULT_MIN_KELVIN,
    VALID_COLOR_MODES,
    valid_supported_color_modes,
)
from homeassistant.components.number import (
    DEFAULT_MAX_VALUE,
    DEFAULT_MIN_VALUE,
    DEFAULT_STEP,
    DEVICE_CLASS_UNITS as NUMBER_DEVICE_CLASS_UNITS,
    NumberDeviceClass,
    NumberMode,
)
from homeassistant.components.sensor import (
    CONF_STATE_CLASS,
    DEVICE_CLASS_UNITS,
    STATE_CLASS_UNITS,
    SensorDeviceClass,
    SensorStateClass,
)
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.components.valve import ValveDeviceClass, ValveState
from homeassistant.config_entries import (
    SOURCE_RECONFIGURE,
    ConfigEntry,
    ConfigFlow,
    ConfigFlowResult,
    ConfigSubentryFlow,
    OptionsFlow,
    SubentryFlowResult,
)
from homeassistant.const import (
    CONF_BRIGHTNESS,
    CONF_CLIENT_ID,
    CONF_CODE,
    CONF_DEVICE,
    CONF_DEVICE_CLASS,
    CONF_DISCOVERY,
    CONF_EFFECT,
    CONF_ENTITY_CATEGORY,
    CONF_HOST,
    CONF_MODE,
    CONF_MODEL,
    CONF_MODEL_ID,
    CONF_NAME,
    CONF_OPTIMISTIC,
    CONF_OPTIONS,
    CONF_PASSWORD,
    CONF_PAYLOAD,
    CONF_PAYLOAD_OFF,
    CONF_PAYLOAD_ON,
    CONF_PLATFORM,
    CONF_PORT,
    CONF_PROTOCOL,
    CONF_STATE_TEMPLATE,
    CONF_TEMPERATURE_UNIT,
    CONF_UNIQUE_ID,
    CONF_UNIT_OF_MEASUREMENT,
    CONF_USERNAME,
    CONF_VALUE_TEMPLATE,
    STATE_CLOSED,
    STATE_CLOSING,
    STATE_OPEN,
    STATE_OPENING,
    EntityCategory,
    Platform,
    UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, async_get_hass, callback
from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.selector import (
    BooleanSelector,
    DurationSelector,
    DurationSelectorConfig,
    FileSelector,
    FileSelectorConfig,
    NumberSelector,
    NumberSelectorConfig,
    NumberSelectorMode,
    SelectOptionDict,
    Selector,
    SelectSelector,
    SelectSelectorConfig,
    SelectSelectorMode,
    TemplateSelector,
    TemplateSelectorConfig,
    TextSelector,
    TextSelectorConfig,
    TextSelectorType,
)
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from homeassistant.util.unit_conversion import TemperatureConverter

from .addon import get_addon_manager
from .client import try_connection
from .const import (
    ALARM_CONTROL_PANEL_SUPPORTED_FEATURES,
    ATTR_PAYLOAD,
    ATTR_QOS,
    ATTR_RETAIN,
    ATTR_TOPIC,
    CONF_ACTION_TEMPLATE,
    CONF_ACTION_TOPIC,
    CONF_AVAILABILITY_TEMPLATE,
    CONF_AVAILABILITY_TOPIC,
    CONF_AVAILABLE_TONES,
    CONF_BIRTH_MESSAGE,
    CONF_BLUE_TEMPLATE,
    CONF_BRIGHTNESS_COMMAND_TEMPLATE,
    CONF_BRIGHTNESS_COMMAND_TOPIC,
    CONF_BRIGHTNESS_SCALE,
    CONF_BRIGHTNESS_STATE_TOPIC,
    CONF_BRIGHTNESS_TEMPLATE,
    CONF_BRIGHTNESS_VALUE_TEMPLATE,
    CONF_BROKER,
    CONF_CERTIFICATE,
    CONF_CLIENT_CERT,
    CONF_CLIENT_KEY,
    CONF_CODE_ARM_REQUIRED,
    CONF_CODE_DISARM_REQUIRED,
    CONF_CODE_FORMAT,
    CONF_CODE_TRIGGER_REQUIRED,
    CONF_COLOR_MODE_STATE_TOPIC,
    CONF_COLOR_MODE_VALUE_TEMPLATE,
    CONF_COLOR_TEMP_COMMAND_TEMPLATE,
    CONF_COLOR_TEMP_COMMAND_TOPIC,
    CONF_COLOR_TEMP_KELVIN,
    CONF_COLOR_TEMP_STATE_TOPIC,
    CONF_COLOR_TEMP_TEMPLATE,
    CONF_COLOR_TEMP_VALUE_TEMPLATE,
    CONF_COMMAND_OFF_TEMPLATE,
    CONF_COMMAND_ON_TEMPLATE,
    CONF_COMMAND_TEMPLATE,
    CONF_COMMAND_TOPIC,
    CONF_CONFIGURATION_URL,
    CONF_CONTENT_TYPE,
    CONF_CURRENT_HUMIDITY_TEMPLATE,
    CONF_CURRENT_HUMIDITY_TOPIC,
    CONF_CURRENT_TEMP_TEMPLATE,
    CONF_CURRENT_TEMP_TOPIC,
    CONF_DIRECTION_COMMAND_TEMPLATE,
    CONF_DIRECTION_COMMAND_TOPIC,
    CONF_DIRECTION_STATE_TOPIC,
    CONF_DIRECTION_VALUE_TEMPLATE,
    CONF_DISCOVERY_PREFIX,
    CONF_DISCOVERY_QOS,
    CONF_EFFECT_COMMAND_TEMPLATE,
    CONF_EFFECT_COMMAND_TOPIC,
    CONF_EFFECT_LIST,
    CONF_EFFECT_STATE_TOPIC,
    CONF_EFFECT_TEMPLATE,
    CONF_EFFECT_VALUE_TEMPLATE,
    CONF_ENTITY_PICTURE,
    CONF_EXPIRE_AFTER,
    CONF_FAN_MODE_COMMAND_TEMPLATE,
    CONF_FAN_MODE_COMMAND_TOPIC,
    CONF_FAN_MODE_LIST,
    CONF_FAN_MODE_STATE_TEMPLATE,
    CONF_FAN_MODE_STATE_TOPIC,
    CONF_FLASH,
    CONF_FLASH_TIME_LONG,
    CONF_FLASH_TIME_SHORT,
    CONF_GET_POSITION_TEMPLATE,
    CONF_GET_POSITION_TOPIC,
    CONF_GREEN_TEMPLATE,
    CONF_HS_COMMAND_TEMPLATE,
    CONF_HS_COMMAND_TOPIC,
    CONF_HS_STATE_TOPIC,
    CONF_HS_VALUE_TEMPLATE,
    CONF_HUMIDITY_COMMAND_TEMPLATE,
    CONF_HUMIDITY_COMMAND_TOPIC,
    CONF_HUMIDITY_MAX,
    CONF_HUMIDITY_MIN,
    CONF_HUMIDITY_STATE_TEMPLATE,
    CONF_HUMIDITY_STATE_TOPIC,
    CONF_HW_VERSION,
    CONF_IMAGE_ENCODING,
    CONF_IMAGE_TOPIC,
    CONF_KEEPALIVE,
    CONF_LAST_RESET_VALUE_TEMPLATE,
    CONF_MANUFACTURER,
    CONF_MAX,
    CONF_MAX_KELVIN,
    CONF_MESSAGE_EXPIRY_INTERVAL,
    CONF_MIN,
    CONF_MIN_KELVIN,
    CONF_MODE_COMMAND_TEMPLATE,
    CONF_MODE_COMMAND_TOPIC,
    CONF_MODE_LIST,
    CONF_MODE_STATE_TEMPLATE,
    CONF_MODE_STATE_TOPIC,
    CONF_OFF_DELAY,
    CONF_ON_COMMAND_TYPE,
    CONF_OSCILLATION_COMMAND_TEMPLATE,
    CONF_OSCILLATION_COMMAND_TOPIC,
    CONF_OSCILLATION_STATE_TOPIC,
    CONF_OSCILLATION_VALUE_TEMPLATE,
    CONF_PATTERN,
    CONF_PAYLOAD_ARM_AWAY,
    CONF_PAYLOAD_ARM_CUSTOM_BYPASS,
    CONF_PAYLOAD_ARM_HOME,
    CONF_PAYLOAD_ARM_NIGHT,
    CONF_PAYLOAD_ARM_VACATION,
    CONF_PAYLOAD_AVAILABLE,
    CONF_PAYLOAD_CLOSE,
    CONF_PAYLOAD_LOCK,
    CONF_PAYLOAD_NOT_AVAILABLE,
    CONF_PAYLOAD_OPEN,
    CONF_PAYLOAD_OSCILLATION_OFF,
    CONF_PAYLOAD_OSCILLATION_ON,
    CONF_PAYLOAD_PRESS,
    CONF_PAYLOAD_RESET,
    CONF_PAYLOAD_RESET_PERCENTAGE,
    CONF_PAYLOAD_RESET_PRESET_MODE,
    CONF_PAYLOAD_STOP,
    CONF_PAYLOAD_STOP_TILT,
    CONF_PAYLOAD_TRIGGER,
    CONF_PAYLOAD_UNLOCK,
    CONF_PERCENTAGE_COMMAND_TEMPLATE,
    CONF_PERCENTAGE_COMMAND_TOPIC,
    CONF_PERCENTAGE_STATE_TOPIC,
    CONF_PERCENTAGE_VALUE_TEMPLATE,
    CONF_POSITION_CLOSED,
    CONF_POSITION_OPEN,
    CONF_POWER_COMMAND_TEMPLATE,
    CONF_POWER_COMMAND_TOPIC,
    CONF_PRECISION,
    CONF_PRESET_MODE_COMMAND_TEMPLATE,
    CONF_PRESET_MODE_COMMAND_TOPIC,
    CONF_PRESET_MODE_STATE_TOPIC,
    CONF_PRESET_MODE_VALUE_TEMPLATE,
    CONF_PRESET_MODES_LIST,
    CONF_QOS,
    CONF_RED_TEMPLATE,
    CONF_REPORTS_POSITION,
    CONF_RETAIN,
    CONF_RGB_COMMAND_TEMPLATE,
    CONF_RGB_COMMAND_TOPIC,
    CONF_RGB_STATE_TOPIC,
    CONF_RGB_VALUE_TEMPLATE,
    CONF_RGBW_COMMAND_TEMPLATE,
    CONF_RGBW_COMMAND_TOPIC,
    CONF_RGBW_STATE_TOPIC,
    CONF_RGBW_VALUE_TEMPLATE,
    CONF_RGBWW_COMMAND_TEMPLATE,
    CONF_RGBWW_COMMAND_TOPIC,
    CONF_RGBWW_STATE_TOPIC,
    CONF_RGBWW_VALUE_TEMPLATE,
    CONF_SCHEMA,
    CONF_SET_POSITION_TEMPLATE,
    CONF_SET_POSITION_TOPIC,
    CONF_SPEED_RANGE_MAX,
    CONF_SPEED_RANGE_MIN,
    CONF_STATE_CLOSED,
    CONF_STATE_CLOSING,
    CONF_STATE_JAMMED,
    CONF_STATE_LOCKED,
    CONF_STATE_LOCKING,
    CONF_STATE_OFF,
    CONF_STATE_ON,
    CONF_STATE_OPEN,
    CONF_STATE_OPENING,
    CONF_STATE_STOPPED,
    CONF_STATE_TOPIC,
    CONF_STATE_UNLOCKED,
    CONF_STATE_UNLOCKING,
    CONF_STATE_VALUE_TEMPLATE,
    CONF_STEP,
    CONF_SUGGESTED_DISPLAY_PRECISION,
    CONF_SUPPORT_DURATION,
    CONF_SUPPORT_VOLUME_SET,
    CONF_SUPPORTED_COLOR_MODES,
    CONF_SUPPORTED_FEATURES,
    CONF_SW_VERSION,
    CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE,
    CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC,
    CONF_SWING_HORIZONTAL_MODE_LIST,
    CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE,
    CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC,
    CONF_SWING_MODE_COMMAND_TEMPLATE,
    CONF_SWING_MODE_COMMAND_TOPIC,
    CONF_SWING_MODE_LIST,
    CONF_SWING_MODE_STATE_TEMPLATE,
    CONF_SWING_MODE_STATE_TOPIC,
    CONF_TEMP_COMMAND_TEMPLATE,
    CONF_TEMP_COMMAND_TOPIC,
    CONF_TEMP_HIGH_COMMAND_TEMPLATE,
    CONF_TEMP_HIGH_COMMAND_TOPIC,
    CONF_TEMP_HIGH_STATE_TEMPLATE,
    CONF_TEMP_HIGH_STATE_TOPIC,
    CONF_TEMP_INITIAL,
    CONF_TEMP_LOW_COMMAND_TEMPLATE,
    CONF_TEMP_LOW_COMMAND_TOPIC,
    CONF_TEMP_LOW_STATE_TEMPLATE,
    CONF_TEMP_LOW_STATE_TOPIC,
    CONF_TEMP_MAX,
    CONF_TEMP_MIN,
    CONF_TEMP_STATE_TEMPLATE,
    CONF_TEMP_STATE_TOPIC,
    CONF_TEMP_STEP,
    CONF_TILT_CLOSED_POSITION,
    CONF_TILT_COMMAND_TEMPLATE,
    CONF_TILT_COMMAND_TOPIC,
    CONF_TILT_MAX,
    CONF_TILT_MIN,
    CONF_TILT_OPEN_POSITION,
    CONF_TILT_STATE_OPTIMISTIC,
    CONF_TILT_STATUS_TEMPLATE,
    CONF_TILT_STATUS_TOPIC,
    CONF_TIMEZONE,
    CONF_TLS_INSECURE,
    CONF_TRANSITION,
    CONF_TRANSPORT,
    CONF_URL_TEMPLATE,
    CONF_URL_TOPIC,
    CONF_WHITE_COMMAND_TOPIC,
    CONF_WHITE_SCALE,
    CONF_WILL_MESSAGE,
    CONF_WS_HEADERS,
    CONF_WS_PATH,
    CONF_XY_COMMAND_TEMPLATE,
    CONF_XY_COMMAND_TOPIC,
    CONF_XY_STATE_TOPIC,
    CONF_XY_VALUE_TEMPLATE,
    CONFIG_ENTRY_MINOR_VERSION,
    CONFIG_ENTRY_VERSION,
    DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE,
    DEFAULT_BIRTH,
    DEFAULT_CLIMATE_INITIAL_TEMPERATURE,
    DEFAULT_DISCOVERY,
    DEFAULT_ENCODING,
    DEFAULT_ON_COMMAND_TYPE,
    DEFAULT_PAYLOAD_ARM_AWAY,
    DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS,
    DEFAULT_PAYLOAD_ARM_HOME,
    DEFAULT_PAYLOAD_ARM_NIGHT,
    DEFAULT_PAYLOAD_ARM_VACATION,
    DEFAULT_PAYLOAD_AVAILABLE,
    DEFAULT_PAYLOAD_CLOSE,
    DEFAULT_PAYLOAD_LOCK,
    DEFAULT_PAYLOAD_NOT_AVAILABLE,
    DEFAULT_PAYLOAD_OFF,
    DEFAULT_PAYLOAD_ON,
    DEFAULT_PAYLOAD_OPEN,
    DEFAULT_PAYLOAD_OSCILLATE_OFF,
    DEFAULT_PAYLOAD_OSCILLATE_ON,
    DEFAULT_PAYLOAD_PRESS,
    DEFAULT_PAYLOAD_RESET,
    DEFAULT_PAYLOAD_STOP,
    DEFAULT_PAYLOAD_TRIGGER,
    DEFAULT_PAYLOAD_UNLOCK,
    DEFAULT_PORT,
    DEFAULT_POSITION_CLOSED,
    DEFAULT_POSITION_OPEN,
    DEFAULT_PREFIX,
    DEFAULT_PROTOCOL,
    DEFAULT_QOS,
    DEFAULT_SPEED_RANGE_MAX,
    DEFAULT_SPEED_RANGE_MIN,
    DEFAULT_STATE_JAMMED,
    DEFAULT_STATE_LOCKED,
    DEFAULT_STATE_LOCKING,
    DEFAULT_STATE_STOPPED,
    DEFAULT_STATE_UNLOCKED,
    DEFAULT_STATE_UNLOCKING,
    DEFAULT_TILT_CLOSED_POSITION,
    DEFAULT_TILT_MAX,
    DEFAULT_TILT_MIN,
    DEFAULT_TILT_OPEN_POSITION,
    DEFAULT_TRANSPORT,
    DEFAULT_WILL,
    DOMAIN,
    REMOTE_CODE,
    REMOTE_CODE_TEXT,
    SUPPORTED_PROTOCOLS,
    TRANSPORT_TCP,
    TRANSPORT_WEBSOCKETS,
    VALUES_ON_COMMAND_TYPE,
)
from .models import MqttAvailabilityData, MqttDeviceData, MqttSubentryData
from .util import (
    async_create_certificate_temp_files,
    get_file_path,
    learn_more_url,
    valid_birth_will,
    valid_publish_topic,
    valid_subscribe_topic,
    valid_subscribe_topic_template,
)

_LOGGER = logging.getLogger(__name__)

ADDON_SETUP_TIMEOUT = 5
ADDON_SETUP_TIMEOUT_ROUNDS = 5

CONF_CLIENT_KEY_PASSWORD = "client_key_password"

OTHER_SETTINGS = "other_settings"
SET_CA_CERT = "set_ca_cert"
SET_CLIENT_CERT = "set_client_cert"

CA_VERIFICATION_MODES = [
    "off",
    "auto",
    "custom",
]

SUBENTRY_PLATFORMS = [
    Platform.ALARM_CONTROL_PANEL,
    Platform.BINARY_SENSOR,
    Platform.BUTTON,
    Platform.CLIMATE,
    Platform.COVER,
    Platform.DATE,
    Platform.DATETIME,
    Platform.FAN,
    Platform.IMAGE,
    Platform.LIGHT,
    Platform.LOCK,
    Platform.NOTIFY,
    Platform.NUMBER,
    Platform.SELECT,
    Platform.SENSOR,
    Platform.SIREN,
    Platform.SWITCH,
    Platform.TEXT,
    Platform.TIME,
    Platform.VALVE,
    Platform.WATER_HEATER,
]

_CODE_VALIDATION_MODE = {
    "remote_code": REMOTE_CODE,
    "remote_code_text": REMOTE_CODE_TEXT,
}
EXCLUDE_FROM_CONFIG_IF_NONE = {CONF_ENTITY_CATEGORY, CONF_UNIT_OF_MEASUREMENT}
PWD_NOT_CHANGED = "__**password_not_changed**__"

DEVELOPER_DOCUMENTATION_URL = "https://developers.home-assistant.io/"
USER_DOCUMENTATION_URL = "https://www.home-assistant.io/"
TZ_ZONE_ABBR_URL = (
    "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
    "#Time_zone_abbreviations"
)

INTEGRATION_URL = f"{USER_DOCUMENTATION_URL}integrations/{DOMAIN}/"
TEMPLATING_URL = f"{USER_DOCUMENTATION_URL}docs/configuration/templating/"
COMMAND_TEMPLATING_URL = f"{TEMPLATING_URL}#using-command-templates-with-mqtt"
VALUE_TEMPLATING_URL = f"{TEMPLATING_URL}#using-value-templates-with-mqtt"
AVAILABLE_STATE_CLASSES_URL = (
    f"{DEVELOPER_DOCUMENTATION_URL}docs/core/entity/sensor/#available-state-classes"
)
NAMING_ENTITIES_URL = f"{INTEGRATION_URL}#naming-of-mqtt-entities"
REGISTRY_PROPERTIES_URL = (
    f"{DEVELOPER_DOCUMENTATION_URL}docs/core/entity/#registry-properties"
)

TRANSLATION_DESCRIPTION_PLACEHOLDERS = {
    "command_templating_url": COMMAND_TEMPLATING_URL,
    "value_templating_url": VALUE_TEMPLATING_URL,
    "available_state_classes_url": AVAILABLE_STATE_CLASSES_URL,
    "naming_entities_url": NAMING_ENTITIES_URL,
    "registry_properties_url": REGISTRY_PROPERTIES_URL,
    "tz_abbr_url": TZ_ZONE_ABBR_URL,
}

# Common selectors
BOOLEAN_SELECTOR = BooleanSelector()
TEMPLATE_SELECTOR = TemplateSelector(TemplateSelectorConfig())
TEMPLATE_SELECTOR_READ_ONLY = TemplateSelector(TemplateSelectorConfig(read_only=True))
TEXT_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
TEXT_SELECTOR_READ_ONLY = TextSelector(
    TextSelectorConfig(type=TextSelectorType.TEXT, read_only=True)
)
OPTIONS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[],
        custom_value=True,
        multiple=True,
    )
)
PASSWORD_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD))
QOS_SELECTOR = NumberSelector(
    NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=2)
)
URL_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.URL))

# Config flow specific selectors
BROKER_VERIFICATION_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=CA_VERIFICATION_MODES,
        mode=SelectSelectorMode.DROPDOWN,
        translation_key=SET_CA_CERT,
    )
)
# mime configuration from https://pki-tutorial.readthedocs.io/en/latest/mime.html
CA_CERT_UPLOAD_SELECTOR = FileSelector(
    FileSelectorConfig(accept=".pem,.crt,.cer,.der,application/x-x509-ca-cert")
)
CERT_KEY_UPLOAD_SELECTOR = FileSelector(
    FileSelectorConfig(accept=".pem,.key,.der,.pk8,application/pkcs8")
)
CERT_UPLOAD_SELECTOR = FileSelector(
    FileSelectorConfig(accept=".pem,.crt,.cer,.der,application/x-x509-user-cert")
)
KEEPALIVE_SELECTOR = vol.All(
    NumberSelector(
        NumberSelectorConfig(
            mode=NumberSelectorMode.BOX, min=15, step="any", unit_of_measurement="sec"
        )
    ),
    vol.Coerce(int),
)
PORT_SELECTOR = vol.All(
    NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1, max=65535)),
    vol.Coerce(int),
)
PROTOCOL_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=SUPPORTED_PROTOCOLS,
        mode=SelectSelectorMode.DROPDOWN,
    )
)
PUBLISH_TOPIC_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
SUPPORTED_TRANSPORTS = [
    SelectOptionDict(value=TRANSPORT_TCP, label="TCP"),
    SelectOptionDict(value=TRANSPORT_WEBSOCKETS, label="WebSocket"),
]
TRANSPORT_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=SUPPORTED_TRANSPORTS,
        mode=SelectSelectorMode.DROPDOWN,
    )
)
WS_HEADERS_SELECTOR = TextSelector(
    TextSelectorConfig(type=TextSelectorType.TEXT, multiline=True)
)

# MQTT device subentry selectors
ENTITY_CATEGORY_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[category.value for category in EntityCategory],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key=CONF_ENTITY_CATEGORY,
        sort=True,
    )
)
SUBENTRY_AVAILABILITY_SCHEMA = vol.Schema(
    {
        vol.Optional(CONF_AVAILABILITY_TOPIC): TEXT_SELECTOR,
        vol.Optional(CONF_AVAILABILITY_TEMPLATE): TEMPLATE_SELECTOR,
        vol.Optional(
            CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE
        ): TEXT_SELECTOR,
        vol.Optional(
            CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE
        ): TEXT_SELECTOR,
    }
)
SUBENTRY_PLATFORM_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[platform.value for platform in SUBENTRY_PLATFORMS],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key=CONF_PLATFORM,
    )
)
SUGGESTED_DISPLAY_PRECISION_SELECTOR = NumberSelector(
    NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=9)
)
TIMEOUT_SELECTOR = NumberSelector(
    NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0)
)

# Entity platform specific selectors
ALARM_CONTROL_PANEL_FEATURES_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=list(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES),
        multiple=True,
        translation_key="alarm_control_panel_features",
    )
)
ALARM_CONTROL_PANEL_CODE_MODE = SelectSelector(
    SelectSelectorConfig(
        options=["local_code", "remote_code", "remote_code_text"],
        translation_key="alarm_control_panel_code_mode",
    )
)
BINARY_SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[device_class.value for device_class in BinarySensorDeviceClass],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="device_class_binary_sensor",
        sort=True,
    )
)
BUTTON_DEVICE_CLASS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[device_class.value for device_class in ButtonDeviceClass],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="device_class_button",
        sort=True,
    )
)
CLIMATE_MODE_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=["auto", "off", "cool", "heat", "dry", "fan_only"],
        multiple=True,
        translation_key="climate_modes",
    )
)
COVER_DEVICE_CLASS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[device_class.value for device_class in CoverDeviceClass],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="device_class_cover",
        sort=True,
    )
)
FAN_SPEED_RANGE_MIN_SELECTOR = vol.All(
    NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1)),
    vol.Coerce(int),
)
FAN_SPEED_RANGE_MAX_SELECTOR = vol.All(
    NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=2)),
    vol.Coerce(int),
)
FLASH_TIME_SELECTOR = NumberSelector(
    NumberSelectorConfig(
        mode=NumberSelectorMode.BOX,
        min=1,
    )
)
HUMIDITY_SELECTOR = vol.All(
    NumberSelector(
        NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=100, step=1)
    ),
    vol.Coerce(int),
)
IMAGE_CONTENT_TYPE_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[
            SelectOptionDict(
                value="image/jpeg", label="Joint Photographic Expert Group image (JPEG)"
            ),
            SelectOptionDict(
                value="image/png", label="Portable Network Graphics (PNG)"
            ),
            SelectOptionDict(
                value="image/apng", label="Animated Portable Network Graphics (APNG)"
            ),
            SelectOptionDict(value="image/avif", label="AV1 Image File Format (AVIF)"),
            SelectOptionDict(
                value="image/gif", label="Graphics Interchange Format (GIF)"
            ),
            SelectOptionDict(
                value="image/svg+xml", label="Scalable Vector Graphics (SVG)"
            ),
            SelectOptionDict(value="image/webp", label="Web Picture format (WEBP)"),
        ],
        mode=SelectSelectorMode.DROPDOWN,
    )
)
IMAGE_ENCODING_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=["raw", "b64"],
        translation_key="image_encoding",
        mode=SelectSelectorMode.DROPDOWN,
    )
)
IMAGE_PROCESSING_MODE_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=["image_url", "image_data"],
        translation_key="image_processing_mode",
    )
)
KELVIN_SELECTOR = NumberSelector(
    NumberSelectorConfig(
        mode=NumberSelectorMode.BOX,
        min=1000,
        max=10000,
        step="any",
        unit_of_measurement="K",
    )
)
LIGHT_SCHEMA_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=["basic", "json", "template"],
        translation_key="light_schema",
    )
)
MIN_MAX_SELECTOR = NumberSelector(NumberSelectorConfig(step=1e-3))
NUMBER_DEVICE_CLASS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[device_class.value for device_class in NumberDeviceClass],
        mode=SelectSelectorMode.DROPDOWN,
        # The number device classes are all shared with the sensor device classes
        translation_key="device_class_sensor",
        sort=True,
    )
)
NUMBER_MODE_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[mode.value for mode in NumberMode],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="number_mode",
        sort=True,
    )
)
ON_COMMAND_TYPE_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=VALUES_ON_COMMAND_TYPE,
        mode=SelectSelectorMode.DROPDOWN,
        translation_key=CONF_ON_COMMAND_TYPE,
        sort=True,
    )
)
POSITION_SELECTOR = NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX))
PRECISION_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=["1.0", "0.5", "0.1"],
        mode=SelectSelectorMode.DROPDOWN,
    )
)
PRESET_MODES_SELECTOR = OPTIONS_SELECTOR
SCALE_SELECTOR = NumberSelector(
    NumberSelectorConfig(
        mode=NumberSelectorMode.BOX,
        min=1,
        max=255,
        step=1,
    )
)
SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[device_class.value for device_class in SensorDeviceClass],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="device_class_sensor",
        sort=True,
    )
)
SENSOR_ENTITY_CATEGORY_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[EntityCategory.DIAGNOSTIC.value],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key=CONF_ENTITY_CATEGORY,
        sort=True,
    )
)
SENSOR_STATE_CLASS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[device_class.value for device_class in SensorStateClass],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key=CONF_STATE_CLASS,
    )
)
STEP_SELECTOR = NumberSelector(NumberSelectorConfig(min=1e-3, step=1e-3))
SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[platform.value for platform in VALID_COLOR_MODES],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key=CONF_SUPPORTED_COLOR_MODES,
        multiple=True,
        sort=True,
    )
)
SWITCH_DEVICE_CLASS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[device_class.value for device_class in SwitchDeviceClass],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="device_class_switch",
    )
)
TARGET_TEMPERATURE_FEATURE_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=["single", "high_low", "none"],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="target_temperature_feature",
    )
)
TEMPERATURE_UNIT_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[
            SelectOptionDict(value="C", label="°C"),
            SelectOptionDict(value="F", label="°F"),
        ],
        mode=SelectSelectorMode.DROPDOWN,
    )
)
TEXT_MODE_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[TextSelectorType.TEXT.value, TextSelectorType.PASSWORD.value],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="text_mode",
    )
)
TEXT_SIZE_SELECTOR = NumberSelector(
    NumberSelectorConfig(min=0, max=255, step=1, mode=NumberSelectorMode.BOX)
)
VALVE_DEVICE_CLASS_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[device_class.value for device_class in ValveDeviceClass],
        mode=SelectSelectorMode.DROPDOWN,
        translation_key="device_class_valve",
    )
)
VALVE_POSITION_SELECTOR = NumberSelector(
    NumberSelectorConfig(mode=NumberSelectorMode.BOX, step=1)
)
WATER_HEATER_MODE_SELECTOR = SelectSelector(
    SelectSelectorConfig(
        options=[
            "off",
            "eco",
            "electric",
            "gas",
            "heat_pump",
            "high_demand",
            "performance",
        ],
        multiple=True,
        translation_key="water_heater_modes",
    )
)


@callback
def configured_target_temperature_feature(config: dict[str, Any]) -> str:
    """Calculate current target temperature feature from config."""
    if config == {CONF_PLATFORM: Platform.CLIMATE} or CONF_TEMP_COMMAND_TOPIC in config:
        # default to single on initial set
        return "single"
    if CONF_TEMP_HIGH_COMMAND_TOPIC in config:
        return "high_low"
    return "none"


@callback
def default_alarm_control_panel_code(config: dict[str, Any]) -> str:
    """Return alarm control panel code based on the stored code and code mode."""
    code: str
    if config["alarm_control_panel_code_mode"] in _CODE_VALIDATION_MODE:
        # Return magic value for remote code validation
        return _CODE_VALIDATION_MODE[config["alarm_control_panel_code_mode"]]
    if (code := config.get(CONF_CODE, "")) in _CODE_VALIDATION_MODE.values():
        # Remove magic value for remote code validation
        return ""

    return code


@callback
def default_precision(config: dict[str, Any]) -> str:
    """Return the thermostat precision for system default unit."""

    return str(
        config.get(
            CONF_PRECISION,
            0.1
            if cv.temperature_unit(config[CONF_TEMPERATURE_UNIT])
            is UnitOfTemperature.CELSIUS
            else 1.0,
        )
    )


@callback
def no_empty_list(value: list[Any]) -> list[Any]:
    """Validate a selector returns at least one item."""
    if not value:
        raise vol.Invalid("empty_list_not_allowed")
    return value


@callback
def temperature_default_from_celsius_to_system_default(
    value: float,
) -> Callable[[dict[str, Any]], int]:
    """Return temperature in Celsius in system default unit."""

    def _default(config: dict[str, Any]) -> int:
        return round(
            TemperatureConverter.convert(
                value,
                UnitOfTemperature.CELSIUS,
                cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]),
            )
        )

    return _default


@callback
def temperature_selector(config: dict[str, Any]) -> Selector:
    """Return a temperature selector with configured or system unit."""

    return NumberSelector(
        NumberSelectorConfig(
            mode=NumberSelectorMode.BOX,
            unit_of_measurement=cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]),
        )
    )


@callback
def temperature_step_selector(config: dict[str, Any]) -> Selector:
    """Return a temperature step selector."""

    return NumberSelector(
        NumberSelectorConfig(
            mode=NumberSelectorMode.BOX,
            min=0.1,
            max=10.0,
            step=0.1,
            unit_of_measurement=cv.temperature_unit(config[CONF_TEMPERATURE_UNIT]),
        )
    )


@callback
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
    """Return a context based unit of measurement selector."""

    if (state_class := user_data.get(CONF_STATE_CLASS)) in STATE_CLASS_UNITS:
        return SelectSelector(
            SelectSelectorConfig(
                options=[str(uom) for uom in STATE_CLASS_UNITS[state_class]],
                sort=True,
                custom_value=True,
            )
        )

    if (
        device_class := user_data.get(CONF_DEVICE_CLASS)
    ) is None or device_class not in DEVICE_CLASS_UNITS:
        return TEXT_SELECTOR
    return SelectSelector(
        SelectSelectorConfig(
            options=[str(uom) for uom in DEVICE_CLASS_UNITS[device_class]],
            sort=True,
            custom_value=True,
        )
    )


@callback
def number_unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
    """Return a context based unit of measurement selector for number entities."""

    if (
        device_class := user_data.get(CONF_DEVICE_CLASS)
    ) is None or device_class not in NUMBER_DEVICE_CLASS_UNITS:
        return TEXT_SELECTOR
    return SelectSelector(
        SelectSelectorConfig(
            options=[str(uom) for uom in NUMBER_DEVICE_CLASS_UNITS[device_class]],
            sort=True,
            custom_value=True,
        )
    )


@callback
def validate(validator: Callable[[Any], Any]) -> Callable[[Any], Any]:
    """Run validator, then return the unmodified input."""

    def _validate(value: Any) -> Any:
        validator(value)
        return value

    return _validate


@callback
def validate_field(
    field: str,
    validator: Callable[..., Any],
    user_input: dict[str, Any] | None,
    errors: dict[str, str],
    error: str,
) -> None:
    """Validate a single field."""
    if user_input is None or field not in user_input or validator is None:
        return
    try:
        user_input[field] = validator(user_input[field])
    except ValueError, vol.Error, vol.Invalid:
        errors[field] = error


# Entity platform config validation
@callback
def validate_climate_platform_config(config: dict[str, Any]) -> dict[str, str]:
    """Validate the climate platform options."""
    errors: dict[str, str] = {}
    if (
        CONF_PRESET_MODES_LIST in config
        and PRESET_NONE in config[CONF_PRESET_MODES_LIST]
    ):
        errors["climate_preset_mode_settings"] = "preset_mode_none_not_allowed"
    if (
        CONF_HUMIDITY_MIN in config
        and config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]
    ):
        errors["target_humidity_settings"] = "max_below_min_humidity"
    if CONF_TEMP_MIN in config and config[CONF_TEMP_MIN] >= config[CONF_TEMP_MAX]:
        errors["target_temperature_settings"] = "max_below_min_temperature"

    return errors


@callback
def validate_cover_platform_config(config: dict[str, Any]) -> dict[str, str]:
    """Validate the cover platform options."""
    errors: dict[str, str] = {}

    # If set position topic is set then get position topic is set as well.
    if CONF_SET_POSITION_TOPIC in config and CONF_GET_POSITION_TOPIC not in config:
        errors["cover_position_settings"] = (
            "cover_get_and_set_position_must_be_set_together"
        )

    # if templates are set make sure the topic for the template is also set
    if CONF_VALUE_TEMPLATE in config and CONF_STATE_TOPIC not in config:
        errors[CONF_VALUE_TEMPLATE] = (
            "cover_value_template_must_be_used_with_state_topic"
        )

    if CONF_GET_POSITION_TEMPLATE in config and CONF_GET_POSITION_TOPIC not in config:
        errors["cover_position_settings"] = (
            "cover_get_position_template_must_be_used_with_get_position_topic"
        )

    if CONF_SET_POSITION_TEMPLATE in config and CONF_SET_POSITION_TOPIC not in config:
        errors["cover_position_settings"] = (
            "cover_set_position_template_must_be_used_with_set_position_topic"
        )

    if CONF_TILT_COMMAND_TEMPLATE in config and CONF_TILT_COMMAND_TOPIC not in config:
        errors["cover_tilt_settings"] = (
            "cover_tilt_command_template_must_be_used_with_tilt_command_topic"
        )

    if CONF_TILT_STATUS_TEMPLATE in config and CONF_TILT_STATUS_TOPIC not in config:
        errors["cover_tilt_settings"] = (
            "cover_tilt_status_template_must_be_used_with_tilt_status_topic"
        )

    return errors


@callback
def validate_fan_platform_config(config: dict[str, Any]) -> dict[str, str]:
    """Validate the fan config options."""
    errors: dict[str, str] = {}
    if (
        CONF_SPEED_RANGE_MIN in config
        and CONF_SPEED_RANGE_MAX in config
        and config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]
    ):
        errors["fan_speed_settings"] = (
            "fan_speed_range_max_must_be_greater_than_speed_range_min"
        )
    if (
        CONF_PRESET_MODES_LIST in config
        and config.get(CONF_PAYLOAD_RESET_PRESET_MODE) in config[CONF_PRESET_MODES_LIST]
    ):
        errors["fan_preset_mode_settings"] = (
            "fan_preset_mode_reset_in_preset_modes_list"
        )

    return errors


@callback
def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
    """Validate MQTT light configuration."""
    errors: dict[str, Any] = {}
    if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get(
        CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN
    ):
        errors[OTHER_SETTINGS] = "max_below_min_kelvin"
    return errors


@callback
def validate_number_platform_config(config: dict[str, Any]) -> dict[str, str]:
    """Validate MQTT number configuration."""
    errors: dict[str, Any] = {}
    if (
        CONF_MIN in config
        and CONF_MAX in config
        and config[CONF_MIN] > config[CONF_MAX]
    ):
        errors[CONF_MIN] = "max_below_min"
        errors[CONF_MAX] = "max_below_min"

    if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) == "None":
        unit_of_measurement = None

    if (
        (device_class := config.get(CONF_DEVICE_CLASS)) is not None
        and device_class in NUMBER_DEVICE_CLASS_UNITS
        and unit_of_measurement not in NUMBER_DEVICE_CLASS_UNITS[device_class]
    ):
        errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"

    return errors


@callback
def validate_sensor_platform_config(
    config: dict[str, Any],
) -> dict[str, str]:
    """Validate the sensor options, state and device class config."""
    errors: dict[str, str] = {}
    # Only allow `options` to be set for `enum` sensors
    # to limit the possible sensor values
    if config.get(CONF_OPTIONS) is not None:
        if config.get(CONF_STATE_CLASS) or config.get(CONF_UNIT_OF_MEASUREMENT):
            errors[CONF_OPTIONS] = "options_not_allowed_with_state_class_or_uom"

        if (device_class := config.get(CONF_DEVICE_CLASS)) != SensorDeviceClass.ENUM:
            errors[CONF_DEVICE_CLASS] = "options_device_class_enum"

    if (
        (device_class := config.get(CONF_DEVICE_CLASS)) == SensorDeviceClass.ENUM
        and errors is not None
        and CONF_OPTIONS not in config
    ):
        errors[CONF_OPTIONS] = "options_with_enum_device_class"

    unit_of_measurement: str | None = None
    if (
        device_class in DEVICE_CLASS_UNITS
        and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is None
        and errors is not None
    ):
        # Do not allow an empty unit of measurement in a subentry data flow
        errors[CONF_UNIT_OF_MEASUREMENT] = "uom_required_for_device_class"
        return errors

    if unit_of_measurement == "None":
        unit_of_measurement = None
        config.pop(CONF_UNIT_OF_MEASUREMENT)

    if (
        device_class is not None
        and device_class in DEVICE_CLASS_UNITS
        and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
    ):
        errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"

    if (
        (state_class := config.get(CONF_STATE_CLASS)) is not None
        and state_class in STATE_CLASS_UNITS
        and config.get(CONF_UNIT_OF_MEASUREMENT) not in STATE_CLASS_UNITS[state_class]
    ):
        errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom_for_state_class"

    return errors


@callback
def validate_text_platform_config(
    config: dict[str, Any],
) -> dict[str, str]:
    """Validate the text entity options."""
    errors: dict[str, str] = {}
    if (
        CONF_MIN in config
        and CONF_MAX in config
        and config[CONF_MIN] > config[CONF_MAX]
    ):
        errors["text_other_settings"] = "max_below_min"

    return errors


@callback
def validate_water_heater_platform_config(config: dict[str, Any]) -> dict[str, str]:
    """Validate the water heater platform options."""
    errors: dict[str, str] = {}
    if CONF_TEMP_MIN in config and config[CONF_TEMP_MIN] >= config[CONF_TEMP_MAX]:
        errors["target_temperature_settings"] = "max_below_min_temperature"

    return errors


ENTITY_CONFIG_VALIDATOR: dict[
    str,
    Callable[[dict[str, Any]], dict[str, str]] | None,
] = {
    Platform.ALARM_CONTROL_PANEL: None,
    Platform.BINARY_SENSOR: None,
    Platform.BUTTON: None,
    Platform.CLIMATE: validate_climate_platform_config,
    Platform.COVER: validate_cover_platform_config,
    Platform.DATE: None,
    Platform.DATETIME: None,
    Platform.FAN: validate_fan_platform_config,
    Platform.IMAGE: None,
    Platform.LIGHT: validate_light_platform_config,
    Platform.LOCK: None,
    Platform.NOTIFY: None,
    Platform.NUMBER: validate_number_platform_config,
    Platform.SELECT: None,
    Platform.SENSOR: validate_sensor_platform_config,
    Platform.SIREN: None,
    Platform.SWITCH: None,
    Platform.TEXT: validate_text_platform_config,
    Platform.TIME: None,
    Platform.VALVE: None,
    Platform.WATER_HEATER: validate_water_heater_platform_config,
}


@dataclass(frozen=True, kw_only=True)
class PlatformField:
    """Stores a platform config field schema, required flag and validator."""

    selector: Selector[Any] | Callable[[dict[str, Any]], Selector[Any]]
    required: bool
    validator: Callable[[Any], Any] | None = None
    error: str | None = None
    default: Any | Callable[[dict[str, Any]], Any] | vol.Undefined | None = (
        vol.UNDEFINED
    )
    is_schema_default: bool = False
    include_in_config: bool = False
    exclude_from_reconfig: bool = False
    exclude_from_config: bool = False
    conditions: tuple[dict[str, Any], ...] | None = None
    custom_filtering: bool = False
    section: str | None = None


COMMON_ENTITY_FIELDS: dict[str, PlatformField] = {
    CONF_PLATFORM: PlatformField(
        selector=SUBENTRY_PLATFORM_SELECTOR,
        required=True,
        exclude_from_reconfig=True,
    ),
    CONF_NAME: PlatformField(
        selector=TEXT_SELECTOR,
        required=False,
        exclude_from_reconfig=True,
        default=None,
    ),
    CONF_ENTITY_PICTURE: PlatformField(
        selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url"
    ),
}
SHARED_PLATFORM_ENTITY_FIELDS: dict[str, PlatformField] = {
    CONF_ENTITY_CATEGORY: PlatformField(
        selector=ENTITY_CATEGORY_SELECTOR,
        required=False,
        default=None,
    ),
}
PLATFORM_ENTITY_FIELDS: dict[Platform, dict[str, PlatformField]] = {
    Platform.ALARM_CONTROL_PANEL: {
        CONF_SUPPORTED_FEATURES: PlatformField(
            selector=ALARM_CONTROL_PANEL_FEATURES_SELECTOR,
            required=True,
            default=lambda config: config.get(
                CONF_SUPPORTED_FEATURES, list(ALARM_CONTROL_PANEL_SUPPORTED_FEATURES)
            ),
        ),
        "alarm_control_panel_code_mode": PlatformField(
            selector=ALARM_CONTROL_PANEL_CODE_MODE,
            required=True,
            exclude_from_config=True,
            default=lambda config: (
                config[CONF_CODE].lower()
                if config.get(CONF_CODE) in (REMOTE_CODE, REMOTE_CODE_TEXT)
                else "local_code"
            ),
        ),
    },
    Platform.BINARY_SENSOR: {
        CONF_DEVICE_CLASS: PlatformField(
            selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR,
            required=False,
        ),
        CONF_ENTITY_CATEGORY: PlatformField(
            selector=SENSOR_ENTITY_CATEGORY_SELECTOR,
            required=False,
            default=None,
        ),
    },
    Platform.BUTTON: {
        CONF_DEVICE_CLASS: PlatformField(
            selector=BUTTON_DEVICE_CLASS_SELECTOR,
            required=False,
        ),
    },
    Platform.CLIMATE: {
        CONF_TEMPERATURE_UNIT: PlatformField(
            selector=TEMPERATURE_UNIT_SELECTOR,
            validator=validate(cv.temperature_unit),
            required=True,
            exclude_from_reconfig=True,
            default=lambda _: (
                "C"
                if async_get_hass().config.units.temperature_unit
                is UnitOfTemperature.CELSIUS
                else "F"
            ),
        ),
        "climate_feature_action": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_ACTION_TOPIC)),
        ),
        "climate_feature_target_temperature": PlatformField(
            selector=TARGET_TEMPERATURE_FEATURE_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=configured_target_temperature_feature,
        ),
        "climate_feature_current_temperature": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_CURRENT_TEMP_TOPIC)),
        ),
        "climate_feature_target_humidity": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_HUMIDITY_COMMAND_TOPIC)),
        ),
        "climate_feature_current_humidity": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_HUMIDITY_STATE_TOPIC)),
        ),
        "climate_feature_preset_modes": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_PRESET_MODES_LIST)),
        ),
        "climate_feature_fan_modes": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_FAN_MODE_LIST)),
        ),
        "climate_feature_swing_modes": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_SWING_MODE_LIST)),
        ),
        "climate_feature_swing_horizontal_modes": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_SWING_HORIZONTAL_MODE_LIST)),
        ),
        "climate_feature_power": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_POWER_COMMAND_TOPIC)),
        ),
    },
    Platform.COVER: {
        CONF_DEVICE_CLASS: PlatformField(
            selector=COVER_DEVICE_CLASS_SELECTOR,
            required=False,
        ),
    },
    Platform.DATE: {},
    Platform.DATETIME: {},
    Platform.FAN: {
        "fan_feature_speed": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_PERCENTAGE_COMMAND_TOPIC)),
        ),
        "fan_feature_preset_modes": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_PRESET_MODE_COMMAND_TOPIC)),
        ),
        "fan_feature_oscillation": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_OSCILLATION_COMMAND_TOPIC)),
        ),
        "fan_feature_direction": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_DIRECTION_COMMAND_TOPIC)),
        ),
    },
    Platform.IMAGE: {
        "image_processing_mode": PlatformField(
            selector=IMAGE_PROCESSING_MODE_SELECTOR,
            required=True,
            exclude_from_config=True,
            default=(
                lambda config: (
                    "image_url"
                    if config.get(CONF_IMAGE_TOPIC) is None
                    else "image_data"
                )
            ),
        )
    },
    Platform.LIGHT: {
        CONF_SCHEMA: PlatformField(
            selector=LIGHT_SCHEMA_SELECTOR,
            required=True,
            default="basic",
            exclude_from_reconfig=True,
        ),
        CONF_COLOR_TEMP_KELVIN: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=True,
            default=True,
            is_schema_default=True,
        ),
    },
    Platform.LOCK: {},
    Platform.NOTIFY: {},
    Platform.NUMBER: {
        CONF_DEVICE_CLASS: PlatformField(
            selector=NUMBER_DEVICE_CLASS_SELECTOR,
            required=False,
        ),
        CONF_UNIT_OF_MEASUREMENT: PlatformField(
            selector=number_unit_of_measurement_selector,
            required=False,
            custom_filtering=True,
        ),
    },
    Platform.SELECT: {},
    Platform.SENSOR: {
        CONF_DEVICE_CLASS: PlatformField(
            selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False
        ),
        CONF_STATE_CLASS: PlatformField(
            selector=SENSOR_STATE_CLASS_SELECTOR, required=False
        ),
        CONF_UNIT_OF_MEASUREMENT: PlatformField(
            selector=unit_of_measurement_selector,
            required=False,
            custom_filtering=True,
        ),
        CONF_SUGGESTED_DISPLAY_PRECISION: PlatformField(
            selector=SUGGESTED_DISPLAY_PRECISION_SELECTOR,
            required=False,
            validator=cv.positive_int,
            section=OTHER_SETTINGS,
        ),
        CONF_OPTIONS: PlatformField(
            selector=OPTIONS_SELECTOR,
            required=False,
            conditions=({"device_class": "enum"},),
        ),
        CONF_ENTITY_CATEGORY: PlatformField(
            selector=SENSOR_ENTITY_CATEGORY_SELECTOR,
            required=False,
            default=None,
        ),
    },
    Platform.SIREN: {},
    Platform.SWITCH: {
        CONF_DEVICE_CLASS: PlatformField(
            selector=SWITCH_DEVICE_CLASS_SELECTOR, required=False
        ),
    },
    Platform.TEXT: {},
    Platform.TIME: {},
    Platform.VALVE: {
        CONF_DEVICE_CLASS: PlatformField(
            selector=VALVE_DEVICE_CLASS_SELECTOR, required=False, default=None
        ),
        CONF_REPORTS_POSITION: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=True,
            default=False,
        ),
    },
    Platform.WATER_HEATER: {
        CONF_TEMPERATURE_UNIT: PlatformField(
            selector=TEMPERATURE_UNIT_SELECTOR,
            validator=validate(cv.temperature_unit),
            required=True,
            exclude_from_reconfig=True,
            default=lambda _: (
                "C"
                if async_get_hass().config.units.temperature_unit
                is UnitOfTemperature.CELSIUS
                else "F"
            ),
        ),
        "water_heater_feature_current_temperature": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_CURRENT_TEMP_TOPIC)),
        ),
        "water_heater_feature_power": PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            exclude_from_config=True,
            default=lambda config: bool(config.get(CONF_POWER_COMMAND_TOPIC)),
        ),
    },
}
PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
    Platform.ALARM_CONTROL_PANEL: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            default=DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_CODE: PlatformField(
            selector=PASSWORD_SELECTOR,
            required=True,
            include_in_config=True,
            default=default_alarm_control_panel_code,
            conditions=({"alarm_control_panel_code_mode": "local_code"},),
        ),
        CONF_CODE_ARM_REQUIRED: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=True,
            default=True,
        ),
        CONF_CODE_DISARM_REQUIRED: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=True,
            default=True,
        ),
        CONF_CODE_TRIGGER_REQUIRED: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=True,
            default=True,
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_PAYLOAD_ARM_HOME: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ARM_HOME,
            section="alarm_control_panel_payload_settings",
        ),
        CONF_PAYLOAD_ARM_AWAY: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ARM_AWAY,
            section="alarm_control_panel_payload_settings",
        ),
        CONF_PAYLOAD_ARM_NIGHT: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ARM_NIGHT,
            section="alarm_control_panel_payload_settings",
        ),
        CONF_PAYLOAD_ARM_VACATION: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ARM_VACATION,
            section="alarm_control_panel_payload_settings",
        ),
        CONF_PAYLOAD_ARM_CUSTOM_BYPASS: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS,
            section="alarm_control_panel_payload_settings",
        ),
        CONF_PAYLOAD_TRIGGER: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_TRIGGER,
            section="alarm_control_panel_payload_settings",
        ),
    },
    Platform.BINARY_SENSOR: {
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_PAYLOAD_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OFF,
        ),
        CONF_PAYLOAD_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ON,
        ),
        CONF_EXPIRE_AFTER: PlatformField(
            selector=TIMEOUT_SELECTOR,
            required=False,
            validator=cv.positive_int,
            section=OTHER_SETTINGS,
        ),
        CONF_OFF_DELAY: PlatformField(
            selector=TIMEOUT_SELECTOR,
            required=False,
            validator=cv.positive_int,
            section=OTHER_SETTINGS,
        ),
    },
    Platform.BUTTON: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_PAYLOAD_PRESS: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_PRESS,
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.CLIMATE: {
        # operation mode settings
        CONF_MODE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_MODE_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_MODE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_MODE_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_MODE_LIST: PlatformField(
            selector=CLIMATE_MODE_SELECTOR,
            required=True,
            default=[],
            validator=validate(no_empty_list),
            error="empty_list_not_allowed",
        ),
        CONF_RETAIN: PlatformField(
            selector=BOOLEAN_SELECTOR, required=False, validator=validate(bool)
        ),
        CONF_OPTIMISTIC: PlatformField(
            selector=BOOLEAN_SELECTOR, required=False, validator=validate(bool)
        ),
        # current action settings
        CONF_ACTION_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="climate_action_settings",
            conditions=({"climate_feature_action": True},),
        ),
        CONF_ACTION_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_action_settings",
            conditions=({"climate_feature_action": True},),
        ),
        # target temperature settings
        CONF_TEMP_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "single"},),
        ),
        CONF_TEMP_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "single"},),
        ),
        CONF_TEMP_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "single"},),
        ),
        CONF_TEMP_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "single"},),
        ),
        CONF_TEMP_LOW_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "high_low"},),
        ),
        CONF_TEMP_LOW_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "high_low"},),
        ),
        CONF_TEMP_LOW_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "high_low"},),
        ),
        CONF_TEMP_LOW_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "high_low"},),
        ),
        CONF_TEMP_HIGH_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "high_low"},),
        ),
        CONF_TEMP_HIGH_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "high_low"},),
        ),
        CONF_TEMP_HIGH_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "high_low"},),
        ),
        CONF_TEMP_HIGH_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_temperature_settings",
            conditions=({"climate_feature_target_temperature": "high_low"},),
        ),
        CONF_TEMP_MIN: PlatformField(
            selector=temperature_selector,
            custom_filtering=True,
            required=True,
            default=temperature_default_from_celsius_to_system_default(
                DEFAULT_MIN_TEMP
            ),
            section="target_temperature_settings",
            conditions=(
                {"climate_feature_target_temperature": "high_low"},
                {"climate_feature_target_temperature": "single"},
            ),
        ),
        CONF_TEMP_MAX: PlatformField(
            selector=temperature_selector,
            custom_filtering=True,
            required=True,
            default=temperature_default_from_celsius_to_system_default(
                DEFAULT_MAX_TEMP
            ),
            section="target_temperature_settings",
            conditions=(
                {"climate_feature_target_temperature": "high_low"},
                {"climate_feature_target_temperature": "single"},
            ),
        ),
        CONF_PRECISION: PlatformField(
            selector=PRECISION_SELECTOR,
            required=False,
            default=default_precision,
            section="target_temperature_settings",
            conditions=(
                {"climate_feature_target_temperature": "high_low"},
                {"climate_feature_target_temperature": "single"},
            ),
        ),
        CONF_TEMP_STEP: PlatformField(
            selector=temperature_step_selector,
            custom_filtering=True,
            required=False,
            default=1.0,
            section="target_temperature_settings",
            conditions=(
                {"climate_feature_target_temperature": "high_low"},
                {"climate_feature_target_temperature": "single"},
            ),
        ),
        CONF_TEMP_INITIAL: PlatformField(
            selector=temperature_selector,
            custom_filtering=True,
            required=False,
            default=temperature_default_from_celsius_to_system_default(
                DEFAULT_CLIMATE_INITIAL_TEMPERATURE
            ),
            section="target_temperature_settings",
            conditions=(
                {"climate_feature_target_temperature": "high_low"},
                {"climate_feature_target_temperature": "single"},
            ),
        ),
        # current temperature settings
        CONF_CURRENT_TEMP_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="current_temperature_settings",
            conditions=({"climate_feature_current_temperature": True},),
        ),
        CONF_CURRENT_TEMP_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="current_temperature_settings",
            conditions=({"climate_feature_current_temperature": True},),
        ),
        # target humidity settings
        CONF_HUMIDITY_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="target_humidity_settings",
            conditions=({"climate_feature_target_humidity": True},),
        ),
        CONF_HUMIDITY_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_humidity_settings",
            conditions=({"climate_feature_target_humidity": True},),
        ),
        CONF_HUMIDITY_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="target_humidity_settings",
            conditions=({"climate_feature_target_humidity": True},),
        ),
        CONF_HUMIDITY_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_humidity_settings",
            conditions=({"climate_feature_target_humidity": True},),
        ),
        CONF_HUMIDITY_MIN: PlatformField(
            selector=HUMIDITY_SELECTOR,
            required=True,
            default=DEFAULT_MIN_HUMIDITY,
            section="target_humidity_settings",
            conditions=({"climate_feature_target_humidity": True},),
        ),
        CONF_HUMIDITY_MAX: PlatformField(
            selector=HUMIDITY_SELECTOR,
            required=True,
            default=DEFAULT_MAX_HUMIDITY,
            section="target_humidity_settings",
            conditions=({"climate_feature_target_humidity": True},),
        ),
        # current humidity settings
        CONF_CURRENT_HUMIDITY_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="current_humidity_settings",
            conditions=({"climate_feature_current_humidity": True},),
        ),
        CONF_CURRENT_HUMIDITY_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="current_humidity_settings",
            conditions=({"climate_feature_current_humidity": True},),
        ),
        # power on/off support
        CONF_POWER_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="climate_power_settings",
            conditions=({"climate_feature_power": True},),
        ),
        CONF_POWER_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_power_settings",
            conditions=({"climate_feature_power": True},),
        ),
        CONF_PAYLOAD_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OFF,
            section="climate_power_settings",
            conditions=({"climate_feature_power": True},),
        ),
        CONF_PAYLOAD_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ON,
            section="climate_power_settings",
            conditions=({"climate_feature_power": True},),
        ),
        # preset mode settings
        CONF_PRESET_MODE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="climate_preset_mode_settings",
            conditions=({"climate_feature_preset_modes": True},),
        ),
        CONF_PRESET_MODE_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_preset_mode_settings",
            conditions=({"climate_feature_preset_modes": True},),
        ),
        CONF_PRESET_MODE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="climate_preset_mode_settings",
            conditions=({"climate_feature_preset_modes": True},),
        ),
        CONF_PRESET_MODE_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_preset_mode_settings",
            conditions=({"climate_feature_preset_modes": True},),
        ),
        CONF_PRESET_MODES_LIST: PlatformField(
            selector=PRESET_MODES_SELECTOR,
            required=True,
            validator=validate(no_empty_list),
            error="empty_list_not_allowed",
            section="climate_preset_mode_settings",
            conditions=({"climate_feature_preset_modes": True},),
        ),
        # fan mode settings
        CONF_FAN_MODE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="climate_fan_mode_settings",
            conditions=({"climate_feature_fan_modes": True},),
        ),
        CONF_FAN_MODE_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_fan_mode_settings",
            conditions=({"climate_feature_fan_modes": True},),
        ),
        CONF_FAN_MODE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="climate_fan_mode_settings",
            conditions=({"climate_feature_fan_modes": True},),
        ),
        CONF_FAN_MODE_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_fan_mode_settings",
            conditions=({"climate_feature_fan_modes": True},),
        ),
        CONF_FAN_MODE_LIST: PlatformField(
            selector=PRESET_MODES_SELECTOR,
            required=True,
            validator=validate(no_empty_list),
            error="empty_list_not_allowed",
            section="climate_fan_mode_settings",
            conditions=({"climate_feature_fan_modes": True},),
        ),
        # swing mode settings
        CONF_SWING_MODE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="climate_swing_mode_settings",
            conditions=({"climate_feature_swing_modes": True},),
        ),
        CONF_SWING_MODE_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_swing_mode_settings",
            conditions=({"climate_feature_swing_modes": True},),
        ),
        CONF_SWING_MODE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="climate_swing_mode_settings",
            conditions=({"climate_feature_swing_modes": True},),
        ),
        CONF_SWING_MODE_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_swing_mode_settings",
            conditions=({"climate_feature_swing_modes": True},),
        ),
        CONF_SWING_MODE_LIST: PlatformField(
            selector=PRESET_MODES_SELECTOR,
            required=True,
            validator=validate(no_empty_list),
            error="empty_list_not_allowed",
            section="climate_swing_mode_settings",
            conditions=({"climate_feature_swing_modes": True},),
        ),
        # swing horizontal mode settings
        CONF_SWING_HORIZONTAL_MODE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="climate_swing_horizontal_mode_settings",
            conditions=({"climate_feature_swing_horizontal_modes": True},),
        ),
        CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_swing_horizontal_mode_settings",
            conditions=({"climate_feature_swing_horizontal_modes": True},),
        ),
        CONF_SWING_HORIZONTAL_MODE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="climate_swing_horizontal_mode_settings",
            conditions=({"climate_feature_swing_horizontal_modes": True},),
        ),
        CONF_SWING_HORIZONTAL_MODE_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="climate_swing_horizontal_mode_settings",
            conditions=({"climate_feature_swing_horizontal_modes": True},),
        ),
        CONF_SWING_HORIZONTAL_MODE_LIST: PlatformField(
            selector=PRESET_MODES_SELECTOR,
            required=True,
            validator=validate(no_empty_list),
            error="empty_list_not_allowed",
            section="climate_swing_horizontal_mode_settings",
            conditions=({"climate_feature_swing_horizontal_modes": True},),
        ),
    },
    Platform.COVER: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_PAYLOAD_CLOSE: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_CLOSE,
            section="cover_payload_settings",
        ),
        CONF_PAYLOAD_OPEN: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OPEN,
            section="cover_payload_settings",
        ),
        CONF_PAYLOAD_STOP: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=None,
            section="cover_payload_settings",
        ),
        CONF_PAYLOAD_STOP_TILT: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_STOP,
            section="cover_payload_settings",
        ),
        CONF_STATE_CLOSED: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=STATE_CLOSED,
            section="cover_payload_settings",
        ),
        CONF_STATE_CLOSING: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=STATE_CLOSING,
            section="cover_payload_settings",
        ),
        CONF_STATE_OPEN: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=STATE_OPEN,
            section="cover_payload_settings",
        ),
        CONF_STATE_OPENING: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=STATE_OPENING,
            section="cover_payload_settings",
        ),
        CONF_STATE_STOPPED: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_STATE_STOPPED,
            section="cover_payload_settings",
        ),
        CONF_SET_POSITION_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="cover_position_settings",
        ),
        CONF_SET_POSITION_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="cover_position_settings",
        ),
        CONF_GET_POSITION_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="cover_position_settings",
        ),
        CONF_GET_POSITION_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="cover_position_settings",
        ),
        CONF_POSITION_CLOSED: PlatformField(
            selector=POSITION_SELECTOR,
            required=False,
            validator=int,
            default=DEFAULT_POSITION_CLOSED,
            section="cover_position_settings",
        ),
        CONF_POSITION_OPEN: PlatformField(
            selector=POSITION_SELECTOR,
            required=False,
            validator=int,
            default=DEFAULT_POSITION_OPEN,
            section="cover_position_settings",
        ),
        CONF_TILT_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="cover_tilt_settings",
        ),
        CONF_TILT_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="cover_tilt_settings",
        ),
        CONF_TILT_CLOSED_POSITION: PlatformField(
            selector=POSITION_SELECTOR,
            required=False,
            validator=int,
            default=DEFAULT_TILT_CLOSED_POSITION,
            section="cover_tilt_settings",
        ),
        CONF_TILT_OPEN_POSITION: PlatformField(
            selector=POSITION_SELECTOR,
            required=False,
            validator=int,
            default=DEFAULT_TILT_OPEN_POSITION,
            section="cover_tilt_settings",
        ),
        CONF_TILT_STATUS_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="cover_tilt_settings",
        ),
        CONF_TILT_STATUS_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="cover_tilt_settings",
        ),
        CONF_TILT_MIN: PlatformField(
            selector=POSITION_SELECTOR,
            required=False,
            validator=int,
            default=DEFAULT_TILT_MIN,
            section="cover_tilt_settings",
        ),
        CONF_TILT_MAX: PlatformField(
            selector=POSITION_SELECTOR,
            required=False,
            validator=int,
            default=DEFAULT_TILT_MAX,
            section="cover_tilt_settings",
        ),
        CONF_TILT_STATE_OPTIMISTIC: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            section="cover_tilt_settings",
        ),
    },
    Platform.DATE: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.DATETIME: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_TIMEZONE: PlatformField(selector=TEXT_SELECTOR, required=False),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.FAN: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_STATE_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_PAYLOAD_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OFF,
        ),
        CONF_PAYLOAD_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ON,
        ),
        CONF_RETAIN: PlatformField(
            selector=BOOLEAN_SELECTOR, required=False, validator=bool
        ),
        CONF_OPTIMISTIC: PlatformField(
            selector=BOOLEAN_SELECTOR, required=False, validator=bool
        ),
        CONF_PERCENTAGE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="fan_speed_settings",
            conditions=({"fan_feature_speed": True},),
        ),
        CONF_PERCENTAGE_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="fan_speed_settings",
            conditions=({"fan_feature_speed": True},),
        ),
        CONF_PERCENTAGE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="fan_speed_settings",
            conditions=({"fan_feature_speed": True},),
        ),
        CONF_PERCENTAGE_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="fan_speed_settings",
            conditions=({"fan_feature_speed": True},),
        ),
        CONF_SPEED_RANGE_MIN: PlatformField(
            selector=FAN_SPEED_RANGE_MIN_SELECTOR,
            required=False,
            validator=int,
            default=DEFAULT_SPEED_RANGE_MIN,
            section="fan_speed_settings",
            conditions=({"fan_feature_speed": True},),
        ),
        CONF_SPEED_RANGE_MAX: PlatformField(
            selector=FAN_SPEED_RANGE_MAX_SELECTOR,
            required=False,
            validator=int,
            default=DEFAULT_SPEED_RANGE_MAX,
            section="fan_speed_settings",
            conditions=({"fan_feature_speed": True},),
        ),
        CONF_PAYLOAD_RESET_PERCENTAGE: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_RESET,
            section="fan_speed_settings",
            conditions=({"fan_feature_speed": True},),
        ),
        CONF_PRESET_MODES_LIST: PlatformField(
            selector=PRESET_MODES_SELECTOR,
            required=True,
            section="fan_preset_mode_settings",
            conditions=({"fan_feature_preset_modes": True},),
        ),
        CONF_PRESET_MODE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="fan_preset_mode_settings",
            conditions=({"fan_feature_preset_modes": True},),
        ),
        CONF_PRESET_MODE_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="fan_preset_mode_settings",
            conditions=({"fan_feature_preset_modes": True},),
        ),
        CONF_PRESET_MODE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="fan_preset_mode_settings",
            conditions=({"fan_feature_preset_modes": True},),
        ),
        CONF_PRESET_MODE_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="fan_preset_mode_settings",
            conditions=({"fan_feature_preset_modes": True},),
        ),
        CONF_PAYLOAD_RESET_PRESET_MODE: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_RESET,
            section="fan_preset_mode_settings",
            conditions=({"fan_feature_preset_modes": True},),
        ),
        CONF_OSCILLATION_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="fan_oscillation_settings",
            conditions=({"fan_feature_oscillation": True},),
        ),
        CONF_OSCILLATION_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="fan_oscillation_settings",
            conditions=({"fan_feature_oscillation": True},),
        ),
        CONF_OSCILLATION_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="fan_oscillation_settings",
            conditions=({"fan_feature_oscillation": True},),
        ),
        CONF_OSCILLATION_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="fan_oscillation_settings",
            conditions=({"fan_feature_oscillation": True},),
        ),
        CONF_PAYLOAD_OSCILLATION_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OSCILLATE_OFF,
            section="fan_oscillation_settings",
            conditions=({"fan_feature_oscillation": True},),
        ),
        CONF_PAYLOAD_OSCILLATION_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OSCILLATE_ON,
            section="fan_oscillation_settings",
            conditions=({"fan_feature_oscillation": True},),
        ),
        CONF_DIRECTION_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="fan_direction_settings",
            conditions=({"fan_feature_direction": True},),
        ),
        CONF_DIRECTION_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="fan_direction_settings",
            conditions=({"fan_feature_direction": True},),
        ),
        CONF_DIRECTION_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="fan_direction_settings",
            conditions=({"fan_feature_direction": True},),
        ),
        CONF_DIRECTION_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="fan_direction_settings",
            conditions=({"fan_feature_direction": True},),
        ),
    },
    Platform.IMAGE: {
        CONF_IMAGE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({"image_processing_mode": "image_data"},),
        ),
        CONF_CONTENT_TYPE: PlatformField(
            selector=IMAGE_CONTENT_TYPE_SELECTOR,
            required=True,
            default=DEFAULT_CONTENT_TYPE,
            conditions=({"image_processing_mode": "image_data"},),
        ),
        CONF_IMAGE_ENCODING: PlatformField(
            selector=IMAGE_ENCODING_SELECTOR,
            required=False,
            conditions=({"image_processing_mode": "image_data"},),
            default="raw",
        ),
        CONF_URL_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({"image_processing_mode": "image_url"},),
        ),
        CONF_URL_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
    },
    Platform.LIGHT: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_ON_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=True,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
        ),
        CONF_COMMAND_OFF_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=True,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
        ),
        CONF_ON_COMMAND_TYPE: PlatformField(
            selector=ON_COMMAND_TYPE_SELECTOR,
            required=False,
            default=DEFAULT_ON_COMMAND_TYPE,
            conditions=({CONF_SCHEMA: "basic"},),
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_STATE_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
        ),
        CONF_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
        ),
        CONF_SUPPORTED_COLOR_MODES: PlatformField(
            selector=SUPPORTED_COLOR_MODES_SELECTOR,
            required=False,
            validator=valid_supported_color_modes,
            error="invalid_supported_color_modes",
            conditions=({CONF_SCHEMA: "json"},),
        ),
        CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_RETAIN: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            conditions=({CONF_SCHEMA: "basic"},),
        ),
        CONF_BRIGHTNESS: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            conditions=({CONF_SCHEMA: "json"},),
            section="light_brightness_settings",
        ),
        CONF_BRIGHTNESS_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_brightness_settings",
        ),
        CONF_BRIGHTNESS_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_brightness_settings",
        ),
        CONF_BRIGHTNESS_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_brightness_settings",
        ),
        CONF_PAYLOAD_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OFF,
            conditions=({CONF_SCHEMA: "basic"},),
        ),
        CONF_PAYLOAD_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ON,
            conditions=({CONF_SCHEMA: "basic"},),
        ),
        CONF_BRIGHTNESS_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_brightness_settings",
        ),
        CONF_BRIGHTNESS_SCALE: PlatformField(
            selector=SCALE_SELECTOR,
            required=False,
            validator=cv.positive_int,
            default=255,
            conditions=(
                {CONF_SCHEMA: "basic"},
                {CONF_SCHEMA: "json"},
            ),
            section="light_brightness_settings",
        ),
        CONF_COLOR_MODE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_color_mode_settings",
        ),
        CONF_COLOR_MODE_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_color_mode_settings",
        ),
        CONF_COLOR_TEMP_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_color_temp_settings",
        ),
        CONF_COLOR_TEMP_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_color_temp_settings",
        ),
        CONF_COLOR_TEMP_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_color_temp_settings",
        ),
        CONF_COLOR_TEMP_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_color_temp_settings",
        ),
        CONF_BRIGHTNESS_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
        ),
        CONF_RED_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
        ),
        CONF_GREEN_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
        ),
        CONF_BLUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
        ),
        CONF_COLOR_TEMP_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
        ),
        CONF_HS_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_hs_settings",
        ),
        CONF_HS_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_hs_settings",
        ),
        CONF_HS_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_hs_settings",
        ),
        CONF_HS_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_hs_settings",
        ),
        CONF_RGB_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgb_settings",
        ),
        CONF_RGB_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgb_settings",
        ),
        CONF_RGB_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgb_settings",
        ),
        CONF_RGB_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgb_settings",
        ),
        CONF_RGBW_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgbw_settings",
        ),
        CONF_RGBW_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgbw_settings",
        ),
        CONF_RGBW_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgbw_settings",
        ),
        CONF_RGBW_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgbw_settings",
        ),
        CONF_RGBWW_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgbww_settings",
        ),
        CONF_RGBWW_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgbww_settings",
        ),
        CONF_RGBWW_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgbww_settings",
        ),
        CONF_RGBWW_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_rgbww_settings",
        ),
        CONF_XY_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_xy_settings",
        ),
        CONF_XY_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_xy_settings",
        ),
        CONF_XY_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_xy_settings",
        ),
        CONF_XY_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_xy_settings",
        ),
        CONF_WHITE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_white_settings",
        ),
        CONF_WHITE_SCALE: PlatformField(
            selector=SCALE_SELECTOR,
            required=False,
            validator=cv.positive_int,
            default=255,
            conditions=(
                {CONF_SCHEMA: "basic"},
                {CONF_SCHEMA: "json"},
            ),
            section="light_white_settings",
        ),
        CONF_EFFECT: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            conditions=({CONF_SCHEMA: "json"},),
            section="light_effect_settings",
        ),
        CONF_EFFECT_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_effect_settings",
        ),
        CONF_EFFECT_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_effect_settings",
        ),
        CONF_EFFECT_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_effect_settings",
        ),
        CONF_EFFECT_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "template"},),
            section="light_effect_settings",
        ),
        CONF_EFFECT_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_SCHEMA: "basic"},),
            section="light_effect_settings",
        ),
        CONF_EFFECT_LIST: PlatformField(
            selector=OPTIONS_SELECTOR,
            required=False,
            section="light_effect_settings",
        ),
        CONF_FLASH: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            default=False,
            validator=cv.boolean,
            conditions=({CONF_SCHEMA: "json"},),
            section=OTHER_SETTINGS,
        ),
        CONF_FLASH_TIME_SHORT: PlatformField(
            selector=FLASH_TIME_SELECTOR,
            required=False,
            validator=cv.positive_int,
            default=2,
            conditions=({CONF_SCHEMA: "json"},),
            section=OTHER_SETTINGS,
        ),
        CONF_FLASH_TIME_LONG: PlatformField(
            selector=FLASH_TIME_SELECTOR,
            required=False,
            validator=cv.positive_int,
            default=10,
            conditions=({CONF_SCHEMA: "json"},),
            section=OTHER_SETTINGS,
        ),
        CONF_TRANSITION: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
            default=False,
            validator=cv.boolean,
            conditions=({CONF_SCHEMA: "json"},),
            section=OTHER_SETTINGS,
        ),
        CONF_MAX_KELVIN: PlatformField(
            selector=KELVIN_SELECTOR,
            required=False,
            validator=cv.positive_int,
            default=DEFAULT_MAX_KELVIN,
            section=OTHER_SETTINGS,
        ),
        CONF_MIN_KELVIN: PlatformField(
            selector=KELVIN_SELECTOR,
            required=False,
            validator=cv.positive_int,
            default=DEFAULT_MIN_KELVIN,
            section=OTHER_SETTINGS,
        ),
    },
    Platform.LOCK: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_CODE_FORMAT: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=validate(cv.is_regex),
            error="invalid_regular_expression",
        ),
        CONF_PAYLOAD_LOCK: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_LOCK,
            section="lock_payload_settings",
        ),
        CONF_PAYLOAD_UNLOCK: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_UNLOCK,
            section="lock_payload_settings",
        ),
        CONF_PAYLOAD_OPEN: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            section="lock_payload_settings",
        ),
        CONF_PAYLOAD_RESET: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_RESET,
            section="lock_payload_settings",
        ),
        CONF_STATE_LOCKED: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_STATE_LOCKED,
            section="lock_payload_settings",
        ),
        CONF_STATE_UNLOCKED: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_STATE_UNLOCKED,
            section="lock_payload_settings",
        ),
        CONF_STATE_LOCKING: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_STATE_LOCKING,
            section="lock_payload_settings",
        ),
        CONF_STATE_UNLOCKING: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_STATE_UNLOCKING,
            section="lock_payload_settings",
        ),
        CONF_STATE_JAMMED: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_STATE_JAMMED,
            section="lock_payload_settings",
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.NOTIFY: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.NUMBER: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_MIN: PlatformField(
            selector=MIN_MAX_SELECTOR,
            required=True,
            default=DEFAULT_MIN_VALUE,
        ),
        CONF_MAX: PlatformField(
            selector=MIN_MAX_SELECTOR,
            required=True,
            default=DEFAULT_MAX_VALUE,
        ),
        CONF_STEP: PlatformField(
            selector=STEP_SELECTOR,
            required=True,
            default=DEFAULT_STEP,
        ),
        CONF_MODE: PlatformField(
            selector=NUMBER_MODE_SELECTOR,
            required=True,
            default=NumberMode.AUTO.value,
        ),
        CONF_PAYLOAD_RESET: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_RESET,
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.SELECT: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_OPTIONS: PlatformField(selector=OPTIONS_SELECTOR, required=True),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.SENSOR: {
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_LAST_RESET_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            conditions=({CONF_STATE_CLASS: "total"},),
        ),
        CONF_EXPIRE_AFTER: PlatformField(
            selector=TIMEOUT_SELECTOR,
            required=False,
            validator=cv.positive_int,
            section=OTHER_SETTINGS,
        ),
    },
    Platform.SIREN: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_STATE_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_PAYLOAD_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OFF,
        ),
        CONF_PAYLOAD_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ON,
        ),
        CONF_STATE_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
        ),
        CONF_STATE_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
        ),
        CONF_AVAILABLE_TONES: PlatformField(
            selector=OPTIONS_SELECTOR,
            required=False,
        ),
        CONF_SUPPORT_DURATION: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
        ),
        CONF_SUPPORT_VOLUME_SET: PlatformField(
            selector=BOOLEAN_SELECTOR,
            required=False,
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_COMMAND_OFF_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="siren_other_settings",
        ),
    },
    Platform.SWITCH: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_PAYLOAD_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OFF,
        ),
        CONF_PAYLOAD_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ON,
        ),
        CONF_STATE_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
        ),
        CONF_STATE_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.TEXT: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_MIN: PlatformField(
            selector=TEXT_SIZE_SELECTOR,
            required=True,
            default=0,
            section="text_other_settings",
        ),
        CONF_MAX: PlatformField(
            selector=TEXT_SIZE_SELECTOR,
            required=True,
            default=255,
            section="text_other_settings",
        ),
        CONF_MODE: PlatformField(
            selector=TEXT_MODE_SELECTOR,
            required=True,
            default=TextSelectorType.TEXT.value,
            section="text_other_settings",
        ),
        CONF_PATTERN: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=validate(cv.is_regex),
            error="invalid_regular_expression",
            section="text_other_settings",
        ),
    },
    Platform.TIME: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.VALVE: {
        CONF_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_VALUE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_POSITION_CLOSED: PlatformField(
            selector=VALVE_POSITION_SELECTOR,
            required=True,
            default=DEFAULT_POSITION_CLOSED,
            conditions=({CONF_REPORTS_POSITION: True},),
        ),
        CONF_POSITION_OPEN: PlatformField(
            selector=VALVE_POSITION_SELECTOR,
            required=True,
            default=DEFAULT_POSITION_OPEN,
            conditions=({CONF_REPORTS_POSITION: True},),
        ),
        CONF_PAYLOAD_OPEN: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            default=DEFAULT_PAYLOAD_OPEN,
            conditions=({CONF_REPORTS_POSITION: False},),
            section="valve_payload_settings",
        ),
        CONF_PAYLOAD_CLOSE: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            default=DEFAULT_PAYLOAD_CLOSE,
            conditions=({CONF_REPORTS_POSITION: False},),
            section="valve_payload_settings",
        ),
        CONF_PAYLOAD_STOP: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            section="valve_payload_settings",
        ),
        CONF_STATE_OPEN: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            default=ValveState.OPEN.value,
            conditions=({CONF_REPORTS_POSITION: False},),
            section="valve_payload_settings",
        ),
        CONF_STATE_CLOSED: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            default=ValveState.CLOSED.value,
            conditions=({CONF_REPORTS_POSITION: False},),
            section="valve_payload_settings",
        ),
        CONF_STATE_OPENING: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            default=ValveState.OPENING.value,
            section="valve_payload_settings",
        ),
        CONF_STATE_CLOSING: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            default=ValveState.CLOSING.value,
            section="valve_payload_settings",
        ),
        CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
        CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False),
    },
    Platform.WATER_HEATER: {
        # operation mode settings
        CONF_MODE_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
        ),
        CONF_MODE_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_MODE_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
        ),
        CONF_MODE_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
        ),
        CONF_MODE_LIST: PlatformField(
            selector=WATER_HEATER_MODE_SELECTOR,
            required=True,
            default=[],
            validator=validate(no_empty_list),
            error="empty_list_not_allowed",
        ),
        CONF_RETAIN: PlatformField(
            selector=BOOLEAN_SELECTOR, required=False, validator=validate(bool)
        ),
        CONF_OPTIMISTIC: PlatformField(
            selector=BOOLEAN_SELECTOR, required=False, validator=validate(bool)
        ),
        # target temperature settings
        CONF_TEMP_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=True,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="target_temperature_settings",
        ),
        CONF_TEMP_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_temperature_settings",
        ),
        CONF_TEMP_STATE_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="target_temperature_settings",
        ),
        CONF_TEMP_STATE_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="target_temperature_settings",
        ),
        CONF_TEMP_MIN: PlatformField(
            selector=temperature_selector,
            custom_filtering=True,
            required=True,
            default=temperature_default_from_celsius_to_system_default(43.3),
            section="target_temperature_settings",
        ),
        CONF_TEMP_MAX: PlatformField(
            selector=temperature_selector,
            custom_filtering=True,
            required=True,
            default=temperature_default_from_celsius_to_system_default(60),
            section="target_temperature_settings",
        ),
        CONF_PRECISION: PlatformField(
            selector=PRECISION_SELECTOR,
            required=False,
            default=default_precision,
            section="target_temperature_settings",
        ),
        CONF_TEMP_INITIAL: PlatformField(
            selector=temperature_selector,
            custom_filtering=True,
            required=False,
            default=temperature_default_from_celsius_to_system_default(43.3),
            section="target_temperature_settings",
        ),
        # current temperature settings
        CONF_CURRENT_TEMP_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_subscribe_topic,
            error="invalid_subscribe_topic",
            section="current_temperature_settings",
            conditions=({"water_heater_feature_current_temperature": True},),
        ),
        CONF_CURRENT_TEMP_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="current_temperature_settings",
            conditions=({"water_heater_feature_current_temperature": True},),
        ),
        # power on/off support
        CONF_POWER_COMMAND_TOPIC: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            validator=valid_publish_topic,
            error="invalid_publish_topic",
            section="water_heater_power_settings",
            conditions=({"water_heater_feature_power": True},),
        ),
        CONF_POWER_COMMAND_TEMPLATE: PlatformField(
            selector=TEMPLATE_SELECTOR,
            required=False,
            validator=validate(cv.template),
            error="invalid_template",
            section="water_heater_power_settings",
            conditions=({"water_heater_feature_power": True},),
        ),
        CONF_PAYLOAD_OFF: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_OFF,
            section="water_heater_power_settings",
            conditions=({"water_heater_feature_power": True},),
        ),
        CONF_PAYLOAD_ON: PlatformField(
            selector=TEXT_SELECTOR,
            required=False,
            default=DEFAULT_PAYLOAD_ON,
            section="water_heater_power_settings",
            conditions=({"water_heater_feature_power": True},),
        ),
    },
}
MQTT_DEVICE_PLATFORM_FIELDS = {
    CONF_NAME: PlatformField(selector=TEXT_SELECTOR, required=True),
    CONF_SW_VERSION: PlatformField(
        selector=TEXT_SELECTOR, required=False, section=OTHER_SETTINGS
    ),
    CONF_HW_VERSION: PlatformField(
        selector=TEXT_SELECTOR, required=False, section=OTHER_SETTINGS
    ),
    CONF_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False),
    CONF_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False),
    CONF_MANUFACTURER: PlatformField(selector=TEXT_SELECTOR, required=False),
    CONF_CONFIGURATION_URL: PlatformField(
        selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url"
    ),
    CONF_QOS: PlatformField(
        selector=QOS_SELECTOR,
        required=False,
        validator=int,
        default=DEFAULT_QOS,
        section="mqtt_settings",
    ),
    CONF_MESSAGE_EXPIRY_INTERVAL: PlatformField(
        selector=DurationSelector(DurationSelectorConfig(enable_day=True)),
        required=False,
        section="mqtt_settings",
    ),
}


@callback
def _check_conditions(
    platform_field: PlatformField, component_data: dict[str, Any] | None = None
) -> bool:
    """Only include field if one of conditions match, or no conditions are set."""
    if platform_field.conditions is None or component_data is None:
        return True
    return any(
        all(component_data.get(key) == value for key, value in condition.items())
        for condition in platform_field.conditions
    )


@callback
def calculate_merged_config(
    merged_user_input: dict[str, Any],
    data_schema_fields: dict[str, PlatformField],
    component_data: dict[str, Any],
) -> dict[str, Any]:
    """Calculate merged config."""
    base_schema_fields = {
        key
        for key, platform_field in data_schema_fields.items()
        if _check_conditions(platform_field, component_data)
    } - set(merged_user_input)
    return {
        key: value
        for key, value in component_data.items()
        if key not in base_schema_fields
    } | merged_user_input


@callback
def data_schema_from_fields(
    data_schema_fields: dict[str, PlatformField],
    reconfig: bool,
    component_data: dict[str, Any] | None = None,
    user_input: dict[str, Any] | None = None,
    device_data: MqttDeviceData | None = None,
) -> vol.Schema:
    """Generate custom data schema from platform fields or device data."""

    def get_default(field_details: PlatformField) -> Any:
        if callable(field_details.default):
            if TYPE_CHECKING:
                assert component_data is not None
            return field_details.default(component_data)
        return field_details.default

    if device_data is not None:
        component_data_with_user_input: dict[str, Any] | None = dict(device_data)
        if TYPE_CHECKING:
            assert component_data_with_user_input is not None
        component_data_with_user_input.update(
            component_data_with_user_input.pop("mqtt_settings", {})
        )
    else:
        component_data_with_user_input = deepcopy(component_data)
    if component_data_with_user_input is not None and user_input is not None:
        component_data_with_user_input |= user_input

    sections: dict[str | None, None] = {
        field_details.section: None
        for field_details in data_schema_fields.values()
        if not field_details.is_schema_default
    }
    data_schema: dict[Any, Any] = {}
    all_data_element_options: set[Any] = set()
    no_reconfig_options: set[Any] = set()

    defaults: dict[str, Any] = {}
    for field_name, field_details in data_schema_fields.items():
        default = defaults[field_name] = get_default(field_details)
        if not field_details.include_in_config or component_data is None:
            continue
        component_data[field_name] = default

    for schema_section in sections:
        # Always calculate the default values
        # Getting the default value may update the subentry data,
        # even when and option is filtered out
        data_schema_element = {
            vol.Required(field_name, default=defaults[field_name])
            if field_details.required
            else vol.Optional(
                field_name,
                default=defaults[field_name]
                if field_details.default is not None
                else vol.UNDEFINED,
            ): field_details.selector(component_data_with_user_input or {})
            if callable(field_details.selector) and field_details.custom_filtering
            else field_details.selector
            for field_name, field_details in data_schema_fields.items()
            if not field_details.is_schema_default
            and field_details.section == schema_section
            and (not field_details.exclude_from_reconfig or not reconfig)
            and _check_conditions(field_details, component_data_with_user_input)
        }
        data_element_options = set(data_schema_element)
        all_data_element_options |= data_element_options
        no_reconfig_options |= {
            field_name
            for field_name, field_details in data_schema_fields.items()
            if field_details.section == schema_section
            and field_details.exclude_from_reconfig
        }
        if schema_section is None:
            data_schema.update(data_schema_element)
            continue
        if not data_schema_element:
            # Do not show empty sections
            continue
        # Collapse if no values are changed and no required fields need to be set
        collapsed = (
            not any(
                (default := data_schema_fields[str(option)].default) is vol.UNDEFINED
                or (
                    str(option) in component_data_with_user_input
                    and component_data_with_user_input[str(option)] != default
                )
                for option in data_element_options
                if option in component_data_with_user_input
                or (
                    str(option) in data_schema_fields
                    and data_schema_fields[str(option)].required
                )
            )
            if component_data_with_user_input is not None
            else True
        )
        data_schema[vol.Optional(schema_section)] = section(
            vol.Schema(data_schema_element), SectionConfig({"collapsed": collapsed})
        )

    # Reset all fields from the component_data not in the schema
    # except for options that should stay included
    if component_data:
        filtered_fields = (
            set(data_schema_fields) - all_data_element_options - no_reconfig_options
        )
        for field in filtered_fields:
            if (
                field in component_data
                and not data_schema_fields[field].include_in_config
            ):
                del component_data[field]
    return vol.Schema(data_schema)


@callback
def validate_user_input(
    user_input: dict[str, Any],
    data_schema_fields: dict[str, PlatformField],
    *,
    component_data: dict[str, Any] | None = None,
    config_validator: Callable[[dict[str, Any]], dict[str, str]] | None = None,
) -> tuple[dict[str, Any], dict[str, str]]:
    """Validate user input."""
    errors: dict[str, str] = {}
    # Merge sections
    merged_user_input: dict[str, Any] = {}
    for key, value in user_input.items():
        if isinstance(value, dict):
            merged_user_input.update(value)
        else:
            merged_user_input[key] = value

    for field, value in merged_user_input.items():
        validator = data_schema_fields[field].validator
        try:
            merged_user_input[field] = (
                validator(value) if validator is not None else value
            )
        except ValueError, vol.Error, vol.Invalid:
            data_schema_field = data_schema_fields[field]
            errors[data_schema_field.section or field] = (
                data_schema_field.error or "invalid_input"
            )

    if config_validator is not None:
        if TYPE_CHECKING:
            assert component_data is not None

        errors |= config_validator(
            calculate_merged_config(
                merged_user_input, data_schema_fields, component_data
            ),
        )

    return merged_user_input, errors


@callback
def subentry_schema_default_data_from_fields(
    data_schema_fields: dict[str, PlatformField],
    component_data: dict[str, Any],
) -> dict[str, Any]:
    """Generate custom data schema from platform fields or device data."""
    return {
        key: field.default
        for key, field in data_schema_fields.items()
        if _check_conditions(field, component_data)
        and (
            field.is_schema_default
            or (field.default is not vol.UNDEFINED and key not in component_data)
        )
    }


@callback
def update_password_from_user_input(
    entry_password: str | None, user_input: dict[str, Any]
) -> None:
    """Update the password if the entry has been updated.

    As we want to avoid reflecting the stored password in the UI,
    we replace the suggested value in the UI with a sentitel,
    and we change it back here if it was changed.
    """
    # Take out the password submitted
    user_password: str | None = user_input.pop(CONF_PASSWORD, None)
    # Only add the password if it has changed.
    # If the sentinel password is submitted, we replace that with our current
    # password from the config entry data.
    password_changed = user_password is not None and user_password != PWD_NOT_CHANGED
    password = user_password if password_changed else entry_password
    if password is not None:
        user_input[CONF_PASSWORD] = password


REAUTH_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_USERNAME): TEXT_SELECTOR,
        vol.Required(CONF_PASSWORD): PASSWORD_SELECTOR,
    }
)

OTHER_SETTINGS_SCHEMA = vol.Schema(
    {
        vol.Optional(CONF_CLIENT_ID): TEXT_SELECTOR,
        vol.Optional(CONF_KEEPALIVE): KEEPALIVE_SELECTOR,
        vol.Required(SET_CLIENT_CERT): BOOLEAN_SELECTOR,
        vol.Optional(CONF_CLIENT_CERT): CERT_UPLOAD_SELECTOR,
        vol.Optional(CONF_CLIENT_KEY): CERT_KEY_UPLOAD_SELECTOR,
        vol.Optional(CONF_CLIENT_KEY_PASSWORD): PASSWORD_SELECTOR,
        vol.Required(SET_CA_CERT): BROKER_VERIFICATION_SELECTOR,
        vol.Optional(CONF_CERTIFICATE): CA_CERT_UPLOAD_SELECTOR,
        vol.Optional(CONF_TLS_INSECURE): BOOLEAN_SELECTOR,
        vol.Required(CONF_TRANSPORT, default=DEFAULT_TRANSPORT): TRANSPORT_SELECTOR,
        vol.Optional(CONF_WS_PATH): TEXT_SELECTOR,
        vol.Optional(CONF_WS_HEADERS): WS_HEADERS_SELECTOR,
    }
)
CONFIG_DATAFLOW_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_BROKER): TEXT_SELECTOR,
        vol.Required(CONF_PORT, default=DEFAULT_PORT): PORT_SELECTOR,
        vol.Required(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): PROTOCOL_SELECTOR,
        vol.Optional(CONF_USERNAME): TEXT_SELECTOR,
        vol.Optional(CONF_PASSWORD): PASSWORD_SELECTOR,
        vol.Required(OTHER_SETTINGS): section(
            OTHER_SETTINGS_SCHEMA, SectionConfig({"collapsed": True})
        ),
    }
)


class FlowHandler(ConfigFlow, domain=DOMAIN):
    """Handle a config flow."""

    VERSION = CONFIG_ENTRY_VERSION  # 2
    MINOR_VERSION = CONFIG_ENTRY_MINOR_VERSION  # 1

    _hassio_discovery: dict[str, Any] | None = None
    _addon_manager: AddonManager
    last_uploaded: dict[str, Any]

    def __init__(self) -> None:
        """Set up flow instance."""
        self.install_task: asyncio.Task | None = None
        self.start_task: asyncio.Task | None = None
        self.last_uploaded = {}

    @override
    @classmethod
    @callback
    def async_get_supported_subentry_types(
        cls, config_entry: ConfigEntry
    ) -> dict[str, type[ConfigSubentryFlow]]:
        """Return subentries supported by this handler."""
        return {CONF_DEVICE: MQTTSubentryFlowHandler}

    @override
    @staticmethod
    @callback
    def async_get_options_flow(
        config_entry: ConfigEntry,
    ) -> MQTTOptionsFlowHandler:
        """Get the options flow for this handler."""
        return MQTTOptionsFlowHandler()

    async def _async_install_addon(self) -> None:
        """Install the Mosquitto Mqtt broker add-on."""
        addon_manager: AddonManager = get_addon_manager(self.hass)
        await addon_manager.async_schedule_install_addon()

    async def async_step_install_failed(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Add-on installation failed."""
        return self.async_abort(
            reason="addon_install_failed",
            description_placeholders={"addon": self._addon_manager.addon_name},
        )

    async def async_step_install_addon(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Install Mosquitto Broker add-on."""
        if self.install_task is None:
            self.install_task = self.hass.async_create_task(self._async_install_addon())

        if not self.install_task.done():
            return self.async_show_progress(
                step_id="install_addon",
                progress_action="install_addon",
                progress_task=self.install_task,
            )

        try:
            await self.install_task
        except AddonError as err:
            _LOGGER.error(err)
            return self.async_show_progress_done(next_step_id="install_failed")
        finally:
            self.install_task = None

        return self.async_show_progress_done(next_step_id="start_addon")

    async def async_step_start_failed(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Add-on start failed."""
        return self.async_abort(
            reason="addon_start_failed",
            description_placeholders={"addon": self._addon_manager.addon_name},
        )

    async def async_step_start_addon(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Start Mosquitto Broker add-on."""
        if not self.start_task:
            self.start_task = self.hass.async_create_task(self._async_start_addon())
        if not self.start_task.done():
            return self.async_show_progress(
                step_id="start_addon",
                progress_action="start_addon",
                progress_task=self.start_task,
            )
        try:
            await self.start_task
        except AddonError as err:
            _LOGGER.error(err)
            return self.async_show_progress_done(next_step_id="start_failed")
        finally:
            self.start_task = None

        return self.async_show_progress_done(next_step_id="setup_entry_from_discovery")

    async def _async_get_config_and_try(self) -> dict[str, Any] | None:
        """Get the MQTT add-on discovery info and try the connection."""
        if self._hassio_discovery is not None:
            return self._hassio_discovery
        addon_manager: AddonManager = get_addon_manager(self.hass)
        try:
            addon_discovery_config = (
                await addon_manager.async_get_addon_discovery_info()
            )
            config: dict[str, Any] = {
                CONF_BROKER: addon_discovery_config[CONF_HOST],
                CONF_PORT: addon_discovery_config[CONF_PORT],
                CONF_PROTOCOL: DEFAULT_PROTOCOL,
                CONF_USERNAME: addon_discovery_config.get(CONF_USERNAME),
                CONF_PASSWORD: addon_discovery_config.get(CONF_PASSWORD),
            }
        except AddonError:
            # We do not have discovery information yet
            return None
        if await self.hass.async_add_executor_job(
            try_connection,
            config,
        ):
            self._hassio_discovery = config
            return config
        return None

    async def _async_start_addon(self) -> None:
        """Start the Mosquitto Broker add-on."""
        addon_manager: AddonManager = get_addon_manager(self.hass)
        await addon_manager.async_schedule_start_addon()

        # Sleep some seconds to let the add-on start properly before connecting.
        for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS):
            await asyncio.sleep(ADDON_SETUP_TIMEOUT)
            # Finish setup using discovery info to test the connection
            if await self._async_get_config_and_try():
                break
        else:
            raise AddonError(
                translation_domain=DOMAIN,
                translation_key="addon_start_failed",
                translation_placeholders={"addon": addon_manager.addon_name},
            )

    @override
    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle a flow initialized by the user."""
        if is_hassio(self.hass):
            # Offer to set up broker add-on if supervisor is available
            self._addon_manager = get_addon_manager(self.hass)
            return self.async_show_menu(
                step_id="user",
                menu_options=["addon", "broker"],
                description_placeholders={"addon": self._addon_manager.addon_name},
            )

        # Start up a flow for manual setup
        return await self.async_step_broker()

    async def async_step_setup_entry_from_discovery(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Set up mqtt entry from discovery info."""
        if (config := await self._async_get_config_and_try()) is not None:
            return self.async_create_entry(
                title=self._addon_manager.addon_name,
                data=config,
            )

        raise AbortFlow(
            "addon_connection_failed",
            description_placeholders={"addon": self._addon_manager.addon_name},
        )

    async def async_step_addon(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Install and start MQTT broker add-on."""
        addon_manager = self._addon_manager

        try:
            addon_info = await addon_manager.async_get_addon_info()
        except AddonError as err:
            raise AbortFlow(
                "addon_info_failed",
                description_placeholders={"addon": self._addon_manager.addon_name},
            ) from err

        if addon_info.state is AddonState.RUNNING:
            # Finish setup using discovery info
            return await self.async_step_setup_entry_from_discovery()

        if addon_info.state is AddonState.NOT_RUNNING:
            return await self.async_step_start_addon()

        # Install the add-on and start it
        return await self.async_step_install_addon()

    async def async_step_reauth(
        self, entry_data: Mapping[str, Any]
    ) -> ConfigFlowResult:
        """Handle re-authentication with MQTT broker."""
        if is_hassio(self.hass):
            # Check if entry setup matches the add-on discovery config
            addon_manager = get_addon_manager(self.hass)
            try:
                addon_discovery_config = (
                    await addon_manager.async_get_addon_discovery_info()
                )
            except AddonError:
                # Follow manual flow if we have an error
                pass
            else:
                # Check if the addon secrets need to be renewed.
                # This will repair the config entry,
                # in case the official Mosquitto Broker addon was re-installed.
                if (
                    entry_data[CONF_BROKER] == addon_discovery_config[CONF_HOST]
                    and entry_data[CONF_PORT] == addon_discovery_config[CONF_PORT]
                    and entry_data.get(CONF_USERNAME)
                    == (username := addon_discovery_config.get(CONF_USERNAME))
                    and entry_data.get(CONF_PASSWORD)
                    != (password := addon_discovery_config.get(CONF_PASSWORD))
                ):
                    _LOGGER.info(
                        "Executing autorecovery %s add-on secrets",
                        addon_manager.addon_name,
                    )
                    return await self.async_step_reauth_confirm(
                        user_input={CONF_USERNAME: username, CONF_PASSWORD: password}
                    )

        return await self.async_step_reauth_confirm()

    async def async_step_reauth_confirm(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Confirm re-authentication with MQTT broker."""
        errors: dict[str, str] = {}

        reauth_entry = self._get_reauth_entry()
        if user_input:
            substituted_used_data = deepcopy(user_input)
            update_password_from_user_input(
                reauth_entry.data.get(CONF_PASSWORD), substituted_used_data
            )
            new_entry_data = {**reauth_entry.data, **substituted_used_data}
            if await self.hass.async_add_executor_job(
                try_connection,
                new_entry_data,
            ):
                return self.async_update_and_abort(reauth_entry, data=new_entry_data)

            errors["base"] = "invalid_auth"

        schema = self.add_suggested_values_to_schema(
            REAUTH_SCHEMA,
            {
                CONF_USERNAME: reauth_entry.data.get(CONF_USERNAME),
                CONF_PASSWORD: PWD_NOT_CHANGED,
            },
        )
        return self.async_show_form(
            step_id="reauth_confirm",
            data_schema=schema,
            errors=errors,
        )

    @callback
    def async_get_entry_defaults(self) -> dict[str, Any]:
        """Load the default settings from the entry."""
        data = self._get_reconfigure_entry().data
        other_settings: dict[str, Any] = {
            key.schema: data[key.schema]
            for key in OTHER_SETTINGS_SCHEMA.schema
            if key in data
        }
        other_settings[SET_CLIENT_CERT] = (CONF_CLIENT_CERT in other_settings) and (
            CONF_CLIENT_KEY in other_settings
        )
        other_settings.pop(CONF_CLIENT_CERT, None)
        other_settings.pop(CONF_CLIENT_KEY, None)
        conf_cert = other_settings.pop(CONF_CERTIFICATE, None)
        other_settings[SET_CA_CERT] = (
            "auto"
            if conf_cert == "auto"
            else "custom"
            if conf_cert is not None
            else "off"
        )
        if CONF_WS_HEADERS in other_settings:
            other_settings[CONF_WS_HEADERS] = json_dumps(
                other_settings.pop(CONF_WS_HEADERS)
            )

        settings: dict[str, Any] = {
            key.schema: data[key.schema]
            for key in CONFIG_DATAFLOW_SCHEMA.schema
            if key in data
        }
        settings[OTHER_SETTINGS] = other_settings
        if CONF_PASSWORD in settings:
            # Hide entry password
            settings[CONF_PASSWORD] = PWD_NOT_CHANGED
        return settings

    async def async_step_broker(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Confirm the setup."""
        errors: dict[str, str] = {}
        schema = CONFIG_DATAFLOW_SCHEMA
        entry_config_update: dict[str, Any] = {}
        entry_defaults: dict[str, Any] | None = None
        if is_reconfigure := (self.source == SOURCE_RECONFIGURE):
            reconfigure_entry = self._get_reconfigure_entry()
            entry_defaults = self.async_get_entry_defaults()
        if await async_validate_broker_settings(
            self,
            reconfigure_entry.data if is_reconfigure else None,
            user_input,
            entry_config_update,
            errors,
        ):
            if is_reconfigure:
                return self.async_update_and_abort(
                    reconfigure_entry,
                    data=entry_config_update,
                )
            return self.async_create_entry(
                title=entry_config_update[CONF_BROKER],
                data=entry_config_update,
            )

        schema = self.add_suggested_values_to_schema(
            schema, (entry_defaults or {}) | (user_input or {})
        )
        return self.async_show_form(step_id="broker", data_schema=schema, errors=errors)

    async def async_step_reconfigure(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Handle a reconfiguration flow initialized by the user."""
        return await self.async_step_broker()

    @override
    async def async_step_hassio(
        self, discovery_info: HassioServiceInfo
    ) -> ConfigFlowResult:
        """Receive a Hass.io discovery or process setup after addon install."""
        await self._async_handle_discovery_without_unique_id()

        self._hassio_discovery = discovery_info.config

        return await self.async_step_hassio_confirm()

    async def async_step_hassio_confirm(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Confirm a Hass.io discovery."""
        errors: dict[str, str] = {}
        if TYPE_CHECKING:
            assert self._hassio_discovery

        if user_input is not None:
            data: dict[str, Any] = self._hassio_discovery.copy()
            data[CONF_BROKER] = data.pop(CONF_HOST)
            data[CONF_PROTOCOL] = DEFAULT_PROTOCOL
            can_connect = await self.hass.async_add_executor_job(
                try_connection,
                data,
            )

            if can_connect:
                return self.async_create_entry(
                    title=data["addon"],
                    data={
                        CONF_BROKER: data[CONF_BROKER],
                        CONF_PORT: data[CONF_PORT],
                        CONF_PROTOCOL: DEFAULT_PROTOCOL,
                        CONF_USERNAME: data.get(CONF_USERNAME),
                        CONF_PASSWORD: data.get(CONF_PASSWORD),
                    },
                )

            errors["base"] = "cannot_connect"

        return self.async_show_form(
            step_id="hassio_confirm",
            description_placeholders={"addon": self._hassio_discovery["addon"]},
            errors=errors,
        )


class MQTTOptionsFlowHandler(OptionsFlow):
    """Handle MQTT options."""

    async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
        """Manage the MQTT options."""
        return await self.async_step_options()

    async def async_step_options(
        self, user_input: dict[str, Any] | None = None
    ) -> ConfigFlowResult:
        """Manage the MQTT options."""
        errors = {}

        options_config: dict[str, Any] = dict(self.config_entry.options)
        bad_input: bool = False

        def _birth_will(birt_or_will: str) -> dict[str, Any]:
            """Return the user input for birth or will."""
            if TYPE_CHECKING:
                assert user_input
            return {
                ATTR_TOPIC: user_input[f"{birt_or_will}_topic"],
                ATTR_PAYLOAD: user_input.get(f"{birt_or_will}_payload", ""),
                ATTR_QOS: user_input[f"{birt_or_will}_qos"],
                ATTR_RETAIN: user_input[f"{birt_or_will}_retain"],
            }

        def _validate(
            field: str,
            values: dict[str, Any],
            error_code: str,
            schema: Callable[[Any], Any],
        ) -> None:
            """Validate the user input."""
            nonlocal bad_input
            try:
                option_values = schema(values)
                options_config[field] = option_values
            except vol.Invalid:
                errors["base"] = error_code
                bad_input = True

        if user_input is not None:
            # validate input
            options_config[CONF_DISCOVERY] = user_input[CONF_DISCOVERY]
            _validate(
                CONF_DISCOVERY_PREFIX,
                user_input[CONF_DISCOVERY_PREFIX],
                "bad_discovery_prefix",
                valid_publish_topic,
            )
            options_config[CONF_DISCOVERY_QOS] = int(user_input[CONF_DISCOVERY_QOS])
            if "birth_topic" in user_input:
                _validate(
                    CONF_BIRTH_MESSAGE,
                    _birth_will("birth"),
                    "bad_birth",
                    valid_birth_will,
                )
            if not user_input["birth_enable"]:
                options_config[CONF_BIRTH_MESSAGE] = {}

            if "will_topic" in user_input:
                _validate(
                    CONF_WILL_MESSAGE,
                    _birth_will("will"),
                    "bad_will",
                    valid_birth_will,
                )
            if not user_input["will_enable"]:
                options_config[CONF_WILL_MESSAGE] = {}

            if not bad_input:
                return self.async_create_entry(data=options_config)

        birth = {
            **DEFAULT_BIRTH,
            **options_config.get(CONF_BIRTH_MESSAGE, {}),
        }
        will = {
            **DEFAULT_WILL,
            **options_config.get(CONF_WILL_MESSAGE, {}),
        }
        discovery = options_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
        discovery_prefix = options_config.get(CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX)
        discovery_qos = options_config.get(CONF_DISCOVERY_QOS, DEFAULT_QOS)

        # build form
        fields: OrderedDict[vol.Marker, Any] = OrderedDict()
        fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = BOOLEAN_SELECTOR
        fields[vol.Optional(CONF_DISCOVERY_PREFIX, default=discovery_prefix)] = (
            PUBLISH_TOPIC_SELECTOR
        )
        fields[vol.Optional("discovery_qos", default=discovery_qos)] = QOS_SELECTOR

        # Birth message is disabled if CONF_BIRTH_MESSAGE = {}
        fields[
            vol.Optional(
                "birth_enable",
                default=CONF_BIRTH_MESSAGE not in options_config
                or options_config[CONF_BIRTH_MESSAGE] != {},
            )
        ] = BOOLEAN_SELECTOR
        fields[
            vol.Optional(
                "birth_topic", description={"suggested_value": birth[ATTR_TOPIC]}
            )
        ] = PUBLISH_TOPIC_SELECTOR
        fields[
            vol.Optional(
                "birth_payload", description={"suggested_value": birth[CONF_PAYLOAD]}
            )
        ] = TEXT_SELECTOR
        fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_SELECTOR
        fields[vol.Optional("birth_retain", default=birth[ATTR_RETAIN])] = (
            BOOLEAN_SELECTOR
        )

        # Will message is disabled if CONF_WILL_MESSAGE = {}
        fields[
            vol.Optional(
                "will_enable",
                default=CONF_WILL_MESSAGE not in options_config
                or options_config[CONF_WILL_MESSAGE] != {},
            )
        ] = BOOLEAN_SELECTOR
        fields[
            vol.Optional(
                "will_topic", description={"suggested_value": will[ATTR_TOPIC]}
            )
        ] = PUBLISH_TOPIC_SELECTOR
        fields[
            vol.Optional(
                "will_payload", description={"suggested_value": will[CONF_PAYLOAD]}
            )
        ] = TEXT_SELECTOR
        fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_SELECTOR
        fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = (
            BOOLEAN_SELECTOR
        )

        return self.async_show_form(
            step_id="options",
            data_schema=vol.Schema(fields),
            errors=errors,
            last_step=True,
        )


class MQTTSubentryFlowHandler(ConfigSubentryFlow):
    """Handle MQTT subentry flow."""

    _subentry_data: MqttSubentryData
    _component_id: str | None = None

    @callback
    def update_component_fields(
        self,
        data_schema_fields: dict[str, PlatformField],
        merged_user_input: dict[str, Any],
    ) -> None:
        """Update the componment fields."""
        if TYPE_CHECKING:
            assert self._component_id is not None
        component_data = self._subentry_data["components"][self._component_id]
        # Remove the fields from the component data
        # if they are not in the schema and not in the user input
        config = calculate_merged_config(
            merged_user_input, data_schema_fields, component_data
        )
        for field in (
            field
            for field, platform_field in data_schema_fields.items()
            if field in (set(component_data) - set(config))
            and not platform_field.exclude_from_reconfig
            and not platform_field.include_in_config
        ):
            component_data.pop(field)
        component_data.update(merged_user_input)

    @callback
    def generate_names(self) -> tuple[str, str]:
        """Generate the device and full entity name."""
        if TYPE_CHECKING:
            assert self._component_id is not None
        device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
        if entity_name := self._subentry_data["components"][self._component_id].get(
            CONF_NAME
        ):
            full_entity_name: str = f"{device_name} {entity_name}"
        else:
            full_entity_name = device_name
        return device_name, full_entity_name

    @callback
    def get_suggested_values_from_component(
        self, data_schema: vol.Schema
    ) -> dict[str, Any]:
        """Get suggestions from component data based on the data schema."""
        if TYPE_CHECKING:
            assert self._component_id is not None
        component_data = self._subentry_data["components"][self._component_id]
        return {
            field_key: self.get_suggested_values_from_component(value.schema)
            if isinstance(value, section)
            else component_data.get(field_key)
            for field_key, value in data_schema.schema.items()
        }

    @callback
    def get_suggested_values_from_device_data(
        self, data_schema: vol.Schema
    ) -> dict[str, Any]:
        """Get suggestions from device data based on the data schema."""
        device_data = deepcopy(self._subentry_data["device"])
        device_data.update(device_data.get("mqtt_settings", {}))
        return {
            field_key: self.get_suggested_values_from_device_data(value.schema)
            if isinstance(value, section)
            else device_data.get(field_key)
            for field_key, value in data_schema.schema.items()
        }

    async def async_step_user(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Add a subentry."""
        self._subentry_data = MqttSubentryData(device=MqttDeviceData(), components={})
        return await self.async_step_device()

    async def async_step_reconfigure(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Reconfigure a subentry."""
        reconfigure_subentry = self._get_reconfigure_subentry()
        self._subentry_data = cast(
            MqttSubentryData, deepcopy(dict(reconfigure_subentry.data))
        )
        return await self.async_step_summary_menu()

    async def async_step_device(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Add a new MQTT device."""
        errors: dict[str, Any] = {}
        device_data = self._subentry_data[CONF_DEVICE]
        data_schema = data_schema_from_fields(
            MQTT_DEVICE_PLATFORM_FIELDS,
            device_data=device_data,
            reconfig=True,
        )
        if user_input is not None:
            new_device_data: dict[str, Any] = user_input.copy()
            _, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS)
            if OTHER_SETTINGS in new_device_data:
                new_device_data |= new_device_data.pop(OTHER_SETTINGS)
            if not errors:
                self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, new_device_data)
                if self.source == SOURCE_RECONFIGURE:
                    return await self.async_step_summary_menu()
                return await self.async_step_entity()
            data_schema = self.add_suggested_values_to_schema(
                data_schema, device_data if user_input is None else user_input
            )
        elif self.source == SOURCE_RECONFIGURE:
            data_schema = self.add_suggested_values_to_schema(
                data_schema,
                self.get_suggested_values_from_device_data(data_schema),
            )

        return self.async_show_form(
            step_id=CONF_DEVICE,
            data_schema=data_schema,
            errors=errors,
            last_step=False,
        )

    async def async_step_entity(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Add or edit an mqtt entity."""
        errors: dict[str, str] = {}
        data_schema_fields = COMMON_ENTITY_FIELDS
        entity_name_label: str = ""
        platform_label: str = ""
        component_data: dict[str, Any] | None = None
        if reconfig := (self._component_id is not None):
            component_data = self._subentry_data["components"][self._component_id]
            name: str | None = component_data.get(CONF_NAME)
            platform_label = f"{component_data[CONF_PLATFORM]} "
            entity_name_label = f" ({name})" if name is not None else ""
        data_schema = data_schema_from_fields(data_schema_fields, reconfig=reconfig)
        if user_input is not None:
            merged_user_input, errors = validate_user_input(
                user_input, data_schema_fields, component_data=component_data
            )
            if not errors:
                if self._component_id is None:
                    self._component_id = uuid4().hex
                self._subentry_data["components"].setdefault(self._component_id, {})
                self.update_component_fields(data_schema_fields, merged_user_input)
                return await self.async_step_entity_platform_config()
            data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
        elif self.source == SOURCE_RECONFIGURE and self._component_id is not None:
            data_schema = self.add_suggested_values_to_schema(
                data_schema,
                self.get_suggested_values_from_component(data_schema),
            )
        device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
        return self.async_show_form(
            step_id="entity",
            data_schema=data_schema,
            description_placeholders=TRANSLATION_DESCRIPTION_PLACEHOLDERS
            | {
                "mqtt_device": device_name,
                "entity_name_label": entity_name_label,
                "platform_label": platform_label,
            },
            errors=errors,
            last_step=False,
        )

    def _show_update_or_delete_form(self, step_id: str) -> SubentryFlowResult:
        """Help selecting an entity to update or delete."""
        device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
        entities = [
            SelectOptionDict(
                value=key,
                label=f"{device_name} {component_data.get(CONF_NAME, '-') or '-'}"
                f" ({component_data[CONF_PLATFORM]})",
            )
            for key, component_data in self._subentry_data["components"].items()
        ]
        data_schema = vol.Schema(
            {
                vol.Required("component"): SelectSelector(
                    SelectSelectorConfig(
                        options=entities,
                        mode=SelectSelectorMode.LIST,
                    )
                )
            }
        )
        return self.async_show_form(
            step_id=step_id, data_schema=data_schema, last_step=False
        )

    async def async_step_update_entity(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Select the entity to update."""
        if user_input:
            self._component_id = user_input["component"]
            return await self.async_step_entity()
        if len(self._subentry_data["components"]) == 1:
            # Return first key
            self._component_id = next(iter(self._subentry_data["components"]))
            return await self.async_step_entity()
        return self._show_update_or_delete_form("update_entity")

    async def async_step_delete_entity(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Select the entity to delete."""
        if user_input:
            del self._subentry_data["components"][user_input["component"]]
            return await self.async_step_summary_menu()
        return self._show_update_or_delete_form("delete_entity")

    async def async_step_entity_platform_config(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Configure platform entity details."""
        if TYPE_CHECKING:
            assert self._component_id is not None
        component_data = self._subentry_data["components"][self._component_id]
        platform = component_data[CONF_PLATFORM]
        data_schema_fields = (
            SHARED_PLATFORM_ENTITY_FIELDS | PLATFORM_ENTITY_FIELDS[platform]
        )
        errors: dict[str, str] = {}

        data_schema = data_schema_from_fields(
            data_schema_fields,
            reconfig=bool(
                {field for field in data_schema_fields if field in component_data}
            ),
            component_data=component_data,
            user_input=user_input,
        )
        if user_input is not None:
            # Test entity fields against the validator
            merged_user_input, errors = validate_user_input(
                user_input,
                data_schema_fields,
                component_data=component_data,
                config_validator=ENTITY_CONFIG_VALIDATOR[platform],
            )
            if not errors:
                self.update_component_fields(data_schema_fields, merged_user_input)
                return await self.async_step_mqtt_platform_config()

            data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
        else:
            data_schema = self.add_suggested_values_to_schema(
                data_schema,
                self.get_suggested_values_from_component(data_schema),
            )

        device_name, full_entity_name = self.generate_names()
        return self.async_show_form(
            step_id="entity_platform_config",
            data_schema=data_schema,
            description_placeholders=TRANSLATION_DESCRIPTION_PLACEHOLDERS
            | {
                "mqtt_device": device_name,
                CONF_PLATFORM: platform,
                "entity": full_entity_name,
                "url": learn_more_url(platform),
            }
            | (user_input or {}),
            errors=errors,
            last_step=False,
        )

    async def async_step_mqtt_platform_config(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Configure entity platform MQTT details."""
        errors: dict[str, str] = {}
        if TYPE_CHECKING:
            assert self._component_id is not None
        component_data = self._subentry_data["components"][self._component_id]
        platform = component_data[CONF_PLATFORM]
        data_schema_fields = PLATFORM_MQTT_FIELDS[platform]
        data_schema = data_schema_from_fields(
            data_schema_fields,
            reconfig=bool(
                {field for field in data_schema_fields if field in component_data}
            ),
            component_data=component_data,
        )
        if user_input is not None:
            # Test entity fields against the validator
            merged_user_input, errors = validate_user_input(
                user_input,
                data_schema_fields,
                component_data=component_data,
                config_validator=ENTITY_CONFIG_VALIDATOR[platform],
            )
            if not errors:
                self.update_component_fields(data_schema_fields, merged_user_input)
                self._component_id = None
                if self.source == SOURCE_RECONFIGURE:
                    return await self.async_step_summary_menu()
                return self._async_create_subentry()

            data_schema = self.add_suggested_values_to_schema(data_schema, user_input)
        else:
            data_schema = self.add_suggested_values_to_schema(
                data_schema,
                self.get_suggested_values_from_component(data_schema),
            )
        device_name, full_entity_name = self.generate_names()
        return self.async_show_form(
            step_id="mqtt_platform_config",
            data_schema=data_schema,
            description_placeholders=TRANSLATION_DESCRIPTION_PLACEHOLDERS
            | {
                "mqtt_device": device_name,
                CONF_PLATFORM: platform,
                "entity": full_entity_name,
                "url": learn_more_url(platform),
            },
            errors=errors,
            last_step=False,
        )

    @callback
    def _async_update_component_data_defaults(self) -> None:
        """Update component data defaults."""
        for component_data in self._subentry_data["components"].values():
            platform = component_data[CONF_PLATFORM]
            platform_fields: dict[str, PlatformField] = (
                COMMON_ENTITY_FIELDS
                | SHARED_PLATFORM_ENTITY_FIELDS
                | PLATFORM_ENTITY_FIELDS[platform]
                | PLATFORM_MQTT_FIELDS[platform]
            )
            subentry_default_data = subentry_schema_default_data_from_fields(
                platform_fields,
                component_data,
            )
            component_data.update(subentry_default_data)
            for key, platform_field in platform_fields.items():
                if (
                    not platform_field.exclude_from_config
                    or platform_field.include_in_config
                ):
                    continue
                if key in component_data:
                    component_data.pop(key)

    @callback
    def _async_create_subentry(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Create a subentry for a new MQTT device."""
        device_name = self._subentry_data[CONF_DEVICE][CONF_NAME]
        component_data: dict[str, Any] = next(
            iter(self._subentry_data["components"].values())
        )
        platform = component_data[CONF_PLATFORM]
        entity_name: str | None
        if entity_name := component_data.get(CONF_NAME):
            full_entity_name: str = f"{device_name} {entity_name}"
        else:
            full_entity_name = device_name

        self._async_update_component_data_defaults()
        return self.async_create_entry(
            data=self._subentry_data,
            title=self._subentry_data[CONF_DEVICE][CONF_NAME],
            description_placeholders={
                "entity": full_entity_name,
                CONF_PLATFORM: platform,
            },
        )

    async def async_step_availability(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Configure availability options."""
        errors: dict[str, str] = {}
        validate_field(
            "availability_topic",
            valid_subscribe_topic,
            user_input,
            errors,
            "invalid_subscribe_topic",
        )
        validate_field(
            "availability_template",
            valid_subscribe_topic_template,
            user_input,
            errors,
            "invalid_template",
        )
        if not errors and user_input is not None:
            self._subentry_data.setdefault("availability", MqttAvailabilityData())
            self._subentry_data["availability"] = cast(MqttAvailabilityData, user_input)
            return await self.async_step_summary_menu()

        data_schema = SUBENTRY_AVAILABILITY_SCHEMA
        data_schema = self.add_suggested_values_to_schema(
            data_schema,
            dict(self._subentry_data.setdefault("availability", {}))
            if self.source == SOURCE_RECONFIGURE
            else user_input,
        )
        return self.async_show_form(
            step_id="availability",
            data_schema=data_schema,
            errors=errors,
            last_step=False,
        )

    async def async_step_summary_menu(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Show summary menu and decide to add more entities or to finish the flow."""
        self._component_id = None
        mqtt_device = self._subentry_data[CONF_DEVICE][CONF_NAME]
        mqtt_items = ", ".join(
            f"{mqtt_device} {component_data.get(CONF_NAME, '-') or '-'} "
            f"({component_data[CONF_PLATFORM]})"
            for component_data in self._subentry_data["components"].values()
        )
        menu_options = [
            "entity",
            "update_entity",
        ]
        if len(self._subentry_data["components"]) > 1:
            menu_options.append("delete_entity")
        menu_options.extend(["device", "availability"])
        self._async_update_component_data_defaults()
        menu_options.append(
            "save_changes"
            if self._subentry_data != self._get_reconfigure_subentry().data
            else "export"
        )
        return self.async_show_menu(
            step_id="summary_menu",
            menu_options=menu_options,
            description_placeholders={
                "mqtt_device": mqtt_device,
                "mqtt_items": mqtt_items,
            },
        )

    async def async_step_save_changes(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Save the changes made to the subentry."""
        entry = self._get_entry()
        subentry = self._get_reconfigure_subentry()
        entity_registry = er.async_get(self.hass)

        # When a component is removed from the MQTT device,
        # And we save the changes to the subentry,
        # we need to clean up stale entity registry entries.
        # The component id is used as a part of the unique id of the entity.
        for unique_id, platform in [
            (
                f"{subentry.subentry_id}_{component_id}",
                subentry.data["components"][component_id][CONF_PLATFORM],
            )
            for component_id in subentry.data["components"]
            if component_id not in self._subentry_data["components"]
        ]:
            if entity_id := entity_registry.async_get_entity_id(
                platform, DOMAIN, unique_id
            ):
                entity_registry.async_remove(entity_id)

        return self.async_update_and_abort(
            entry,
            subentry,
            data=self._subentry_data,
            title=self._subentry_data[CONF_DEVICE][CONF_NAME],
        )

    async def async_step_export(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Export the MQTT device config as YAML or discovery payload."""
        return self.async_show_menu(
            step_id="export",
            menu_options=["export_yaml", "export_discovery"],
        )

    async def async_step_export_yaml(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Export the MQTT device config as YAML."""
        if user_input is not None:
            return await self.async_step_summary_menu()

        subentry = self._get_reconfigure_subentry()
        mqtt_yaml_config_base: dict[str, list[dict[str, dict[str, Any]]]] = {DOMAIN: []}
        mqtt_yaml_config = mqtt_yaml_config_base[DOMAIN]

        for component_id, component_data in self._subentry_data["components"].items():
            component_config: dict[str, Any] = component_data.copy()
            component_config[CONF_UNIQUE_ID] = f"{subentry.subentry_id}_{component_id}"
            component_config[CONF_DEVICE] = {
                key: value
                for key, value in self._subentry_data["device"].items()
                if key != "mqtt_settings"
            } | {"identifiers": [subentry.subentry_id]}
            platform = component_config.pop(CONF_PLATFORM)
            component_config.update(self._subentry_data.get("availability", {}))
            component_config.update(
                self._subentry_data["device"].get("mqtt_settings", {}).copy()
            )
            for field in EXCLUDE_FROM_CONFIG_IF_NONE:
                if field in component_config and (
                    component_config[field] is None or component_config[field] == "None"
                ):
                    component_config.pop(field)
            mqtt_yaml_config.append({platform: component_config})

        yaml_config = yaml.dump(mqtt_yaml_config_base)
        data_schema = vol.Schema(
            {
                vol.Optional("yaml"): TEMPLATE_SELECTOR_READ_ONLY,
            }
        )
        data_schema = self.add_suggested_values_to_schema(
            data_schema=data_schema,
            suggested_values={"yaml": yaml_config},
        )
        return self.async_show_form(
            step_id="export_yaml",
            last_step=False,
            data_schema=data_schema,
            description_placeholders={"url": INTEGRATION_URL},
        )

    async def async_step_export_discovery(
        self, user_input: dict[str, Any] | None = None
    ) -> SubentryFlowResult:
        """Export the MQTT device config dor MQTT discovery."""

        if user_input is not None:
            return await self.async_step_summary_menu()

        subentry = self._get_reconfigure_subentry()
        discovery_topic = f"homeassistant/device/{subentry.subentry_id}/config"
        discovery_payload: dict[str, Any] = {}
        discovery_payload.update(self._subentry_data.get("availability", {}))
        discovery_payload["dev"] = {
            key: value
            for key, value in self._subentry_data["device"].items()
            if key != "mqtt_settings"
        } | {"identifiers": [subentry.subentry_id]}
        discovery_payload["o"] = {"name": "MQTT subentry export"}
        discovery_payload["cmps"] = {}

        for component_id, component_data in self._subentry_data["components"].items():
            component_config: dict[str, Any] = component_data.copy()
            component_config[CONF_UNIQUE_ID] = f"{subentry.subentry_id}_{component_id}"
            component_config.update(self._subentry_data.get("availability", {}))
            component_config.update(
                self._subentry_data["device"].get("mqtt_settings", {}).copy()
            )
            for field in EXCLUDE_FROM_CONFIG_IF_NONE:
                if field in component_config and (
                    component_config[field] is None or component_config[field] == "None"
                ):
                    component_config.pop(field)
            discovery_payload["cmps"][component_id] = component_config

        data_schema = vol.Schema(
            {
                vol.Optional("discovery_topic"): TEXT_SELECTOR_READ_ONLY,
                vol.Optional("discovery_payload"): TEMPLATE_SELECTOR_READ_ONLY,
            }
        )
        data_schema = self.add_suggested_values_to_schema(
            data_schema=data_schema,
            suggested_values={
                "discovery_topic": discovery_topic,
                "discovery_payload": json.dumps(discovery_payload, indent=2),
            },
        )
        return self.async_show_form(
            step_id="export_discovery",
            last_step=False,
            data_schema=data_schema,
            description_placeholders={"url": INTEGRATION_URL},
        )


@callback
def async_is_pem_data(data: bytes) -> bool:
    """Return True if data is in PEM format."""
    return (
        b"-----BEGIN CERTIFICATE-----" in data
        or b"-----BEGIN PRIVATE KEY-----" in data
        or b"-----BEGIN EC PRIVATE KEY-----" in data
        or b"-----BEGIN RSA PRIVATE KEY-----" in data
        or b"-----BEGIN ENCRYPTED PRIVATE KEY-----" in data
    )


class PEMType(IntEnum):
    """Type of PEM data."""

    CERTIFICATE = 1
    PRIVATE_KEY = 2


@callback
def async_convert_to_pem(
    data: bytes, pem_type: PEMType, password: str | None = None
) -> str | None:
    """Convert data to PEM format."""
    try:
        if async_is_pem_data(data):
            if not password:
                # Assume unencrypted PEM encoded private key
                return data.decode(DEFAULT_ENCODING)
            # Return decrypted PEM encoded private key
            return (
                load_pem_private_key(data, password=password.encode(DEFAULT_ENCODING))
                .private_bytes(
                    encoding=Encoding.PEM,
                    format=PrivateFormat.TraditionalOpenSSL,
                    encryption_algorithm=NoEncryption(),
                )
                .decode(DEFAULT_ENCODING)
            )
        # Convert from DER encoding to PEM
        if pem_type == PEMType.CERTIFICATE:
            return (
                load_der_x509_certificate(data)
                .public_bytes(
                    encoding=Encoding.PEM,
                )
                .decode(DEFAULT_ENCODING)
            )
        # Assume DER encoded private key
        pem_key_data: bytes = load_der_private_key(
            data, password.encode(DEFAULT_ENCODING) if password else None
        ).private_bytes(
            encoding=Encoding.PEM,
            format=PrivateFormat.TraditionalOpenSSL,
            encryption_algorithm=NoEncryption(),
        )
        return pem_key_data.decode("utf-8")
    except TypeError, ValueError, SSLError:
        _LOGGER.exception("Error converting %s file data to PEM format", pem_type.name)
        return None


async def _get_uploaded_file(hass: HomeAssistant, id: str) -> bytes:
    """Get file content from uploaded certificate or key file."""

    def _proces_uploaded_file() -> bytes:
        with process_uploaded_file(hass, id) as file_path:
            return file_path.read_bytes()

    return await hass.async_add_executor_job(_proces_uploaded_file)


async def async_validate_broker_settings(
    flow: FlowHandler,
    entry_config: MappingProxyType[str, Any] | None,
    user_input: dict[str, Any] | None,
    entry_config_update: dict[str, Any],
    errors: dict[str, str],
) -> bool:
    """Validate the broker settings, and return the updated entry dataset."""

    async def _async_process_file_upload(
        upload_id: str,
        field: str,
        pem_type: PEMType,
        error_code: str,
        password: str | None = None,
    ) -> bool:
        """Get uploaded file, or a preserved copy, and convert to a PEM file."""
        try:
            data_raw = await _get_uploaded_file(hass, upload_id)
        except ValueError:
            # Use preserved file if available.
            # When an uploaded file was read, but an error occurs,
            # the form will reload but the temporary file from the upload
            # will not be available any more. If it was processed correctly,
            # we can use the preserved copy.
            if upload_id in flow.last_uploaded:
                data_raw = flow.last_uploaded[upload_id]
            else:
                raise
        else:
            # Preserve a copy in case the validation fails,
            # and we need it later
            flow.last_uploaded[upload_id] = data_raw
        pem_data = async_convert_to_pem(data_raw, pem_type, password)
        if upload_id and not pem_data:
            errors["base"] = error_code
            return False
        entry_config_update[field] = pem_data
        return True

    if user_input is None:
        return False

    hass = flow.hass

    # Copy basic and other entry fields
    entry_config_update |= user_input
    entry_config_update.update(entry_config_update.pop(OTHER_SETTINGS))
    # Pop incompatible fields for update
    for key in (
        SET_CA_CERT,
        SET_CLIENT_CERT,
        CONF_CERTIFICATE,
        CONF_CLIENT_CERT,
        CONF_CLIENT_KEY,
        CONF_CLIENT_KEY_PASSWORD,
    ):
        entry_config_update.pop(key, None)

    # Get current CA certificate settings from config entry
    if (set_ca_cert := user_input[OTHER_SETTINGS][SET_CA_CERT]) == "auto":
        entry_config_update[CONF_CERTIFICATE] = "auto"
    elif (
        entry_config is not None
        and set_ca_cert == "custom"
        and (current_cert := entry_config.get(CONF_CERTIFICATE))
    ):
        entry_config_update[CONF_CERTIFICATE] = current_cert

    # Prepare entry update with uploaded certificate files
    # converted to PEM format
    new_client_certificate: str | None = user_input[OTHER_SETTINGS].get(
        CONF_CLIENT_CERT
    )
    new_client_key: str | None = user_input[OTHER_SETTINGS].get(CONF_CLIENT_KEY)
    set_client_cert = user_input[OTHER_SETTINGS][SET_CLIENT_CERT]

    if (new_client_certificate and not new_client_key) or (
        not new_client_certificate and new_client_key
    ):
        errors["base"] = "invalid_inclusion"
        return False

    if new_certificate := user_input[OTHER_SETTINGS].get(CONF_CERTIFICATE):
        if not await _async_process_file_upload(
            new_certificate, CONF_CERTIFICATE, PEMType.CERTIFICATE, "bad_certificate"
        ):
            return False

    if new_client_certificate:
        if not await _async_process_file_upload(
            new_client_certificate,
            CONF_CLIENT_CERT,
            PEMType.CERTIFICATE,
            "bad_client_cert",
        ):
            return False
    elif (
        entry_config is not None
        and set_client_cert
        and (client_cert := entry_config.get(CONF_CLIENT_CERT))
    ):
        entry_config_update[CONF_CLIENT_CERT] = client_cert

    if new_client_key:
        if not await _async_process_file_upload(
            new_client_key,
            CONF_CLIENT_KEY,
            PEMType.PRIVATE_KEY,
            "client_key_error",
            password=user_input[OTHER_SETTINGS].get(CONF_CLIENT_KEY_PASSWORD),
        ):
            return False
    elif (
        entry_config is not None
        and set_client_cert
        and (client_key := entry_config.get(CONF_CLIENT_KEY))
    ):
        entry_config_update[CONF_CLIENT_KEY] = client_key

    # We temporarily create the current and new uploaded certificate files
    # and we check the certificate chain.
    await async_create_certificate_temp_files(hass, entry_config_update)
    if error := await hass.async_add_executor_job(
        check_certicate_chain,
    ):
        errors["base"] = error
        return False

    if user_input[OTHER_SETTINGS].get(CONF_TRANSPORT, TRANSPORT_TCP) == TRANSPORT_TCP:
        entry_config_update.pop(CONF_WS_PATH, None)
        entry_config_update.pop(CONF_WS_HEADERS, None)
    else:
        # Web socket transport
        try:
            entry_config_update[CONF_WS_HEADERS] = json_loads(
                user_input[OTHER_SETTINGS].get(CONF_WS_HEADERS, "{}")
            )
            schema = vol.Schema({str: str})
            schema(entry_config_update[CONF_WS_HEADERS])
        except (*JSON_DECODE_EXCEPTIONS, vol.MultipleInvalid):
            errors["base"] = "bad_ws_headers"
            return False

    # Test the configuration
    if entry_config is not None:
        update_password_from_user_input(
            entry_config.get(CONF_PASSWORD), entry_config_update
        )
    if await hass.async_add_executor_job(
        try_connection,
        entry_config_update,
    ):
        return True

    errors["base"] = "cannot_connect"
    return False


def check_certicate_chain() -> str | None:
    """Check the MQTT certificates."""
    if client_certificate := get_file_path(CONF_CLIENT_CERT):
        try:
            with open(client_certificate, "rb") as client_certificate_file:
                load_pem_x509_certificate(client_certificate_file.read())
        except ValueError:
            return "bad_client_cert"
    # Check we can serialize the private key file
    if private_key := get_file_path(CONF_CLIENT_KEY):
        try:
            with open(private_key, "rb") as client_key_file:
                load_pem_private_key(client_key_file.read(), password=None)
        except TypeError, ValueError:
            return "client_key_error"
    # Check the certificate chain
    context = SSLContext(PROTOCOL_TLS_CLIENT)
    if client_certificate and private_key:
        try:
            context.load_cert_chain(client_certificate, private_key)
        except SSLError:
            return "bad_client_cert_key"
    # try to load the custom CA file
    if (ca_cert := get_file_path(CONF_CERTIFICATE)) is None:
        return None

    try:
        context.load_verify_locations(ca_cert)
    except SSLError:
        return "bad_certificate"
    return None
