core/homeassistant/components/template/config.py

214 lines
7.3 KiB
Python

"""Template config validator."""
from contextlib import suppress
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.blueprint import (
BLUEPRINT_INSTANCE_FIELDS,
is_blueprint_instance_config,
)
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.config import async_log_schema_error, config_without_domain
from homeassistant.const import (
CONF_BINARY_SENSORS,
CONF_NAME,
CONF_SENSORS,
CONF_UNIQUE_ID,
CONF_VARIABLES,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.condition import async_validate_conditions_config
from homeassistant.helpers.trigger import async_validate_trigger_config
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_notify_setup_error
from . import (
binary_sensor as binary_sensor_platform,
button as button_platform,
image as image_platform,
number as number_platform,
select as select_platform,
sensor as sensor_platform,
weather as weather_platform,
)
from .const import (
CONF_ACTION,
CONF_CONDITION,
CONF_TRIGGER,
DOMAIN,
PLATFORMS,
TemplateConfig,
)
from .helpers import async_get_blueprints
PACKAGE_MERGE_HINT = "list"
CONFIG_SECTION_SCHEMA = vol.Schema(
{
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): cv.CONDITIONS_SCHEMA,
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Optional(NUMBER_DOMAIN): vol.All(
cv.ensure_list, [number_platform.NUMBER_SCHEMA]
),
vol.Optional(SENSOR_DOMAIN): vol.All(
cv.ensure_list, [sensor_platform.SENSOR_SCHEMA]
),
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(
sensor_platform.LEGACY_SENSOR_SCHEMA
),
vol.Optional(BINARY_SENSOR_DOMAIN): vol.All(
cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_SCHEMA]
),
vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys(
binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA
),
vol.Optional(SELECT_DOMAIN): vol.All(
cv.ensure_list, [select_platform.SELECT_SCHEMA]
),
vol.Optional(BUTTON_DOMAIN): vol.All(
cv.ensure_list, [button_platform.BUTTON_SCHEMA]
),
vol.Optional(IMAGE_DOMAIN): vol.All(
cv.ensure_list, [image_platform.IMAGE_SCHEMA]
),
vol.Optional(WEATHER_DOMAIN): vol.All(
cv.ensure_list, [weather_platform.WEATHER_SCHEMA]
),
},
)
TEMPLATE_BLUEPRINT_INSTANCE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
).extend(BLUEPRINT_INSTANCE_FIELDS.schema)
async def _async_resolve_blueprints(
hass: HomeAssistant,
config: ConfigType,
) -> TemplateConfig:
"""If a config item requires a blueprint, resolve that item to an actual config."""
raw_config = None
raw_blueprint_inputs = None
with suppress(ValueError): # Invalid config
raw_config = dict(config)
if is_blueprint_instance_config(config):
config = TEMPLATE_BLUEPRINT_INSTANCE_SCHEMA(config)
blueprints = async_get_blueprints(hass)
blueprint_inputs = await blueprints.async_inputs_from_config(config)
raw_blueprint_inputs = blueprint_inputs.config_with_inputs
config = blueprint_inputs.async_substitute()
platforms = [platform for platform in PLATFORMS if platform in config]
if len(platforms) > 1:
raise vol.Invalid("more than one platform defined per blueprint")
if len(platforms) == 1:
platform = platforms.pop()
for prop in (CONF_NAME, CONF_UNIQUE_ID, CONF_VARIABLES):
if prop in config:
config[platform][prop] = config.pop(prop)
raw_config = dict(config)
template_config = TemplateConfig(CONFIG_SECTION_SCHEMA(config))
template_config.raw_blueprint_inputs = raw_blueprint_inputs
template_config.raw_config = raw_config
return template_config
async def async_validate_config_section(
hass: HomeAssistant, config: ConfigType
) -> TemplateConfig:
"""Validate an entire config section for the template integration."""
validated_config = await _async_resolve_blueprints(hass, config)
if CONF_TRIGGER in validated_config:
validated_config[CONF_TRIGGER] = await async_validate_trigger_config(
hass, validated_config[CONF_TRIGGER]
)
if CONF_CONDITION in validated_config:
validated_config[CONF_CONDITION] = await async_validate_conditions_config(
hass, validated_config[CONF_CONDITION]
)
return validated_config
async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType:
"""Validate config."""
if DOMAIN not in config:
return config
config_sections = []
for cfg in cv.ensure_list(config[DOMAIN]):
try:
template_config: TemplateConfig = await async_validate_config_section(
hass, cfg
)
except vol.Invalid as err:
async_log_schema_error(err, DOMAIN, cfg, hass)
async_notify_setup_error(hass, DOMAIN)
continue
legacy_warn_printed = False
for old_key, new_key, transform in (
(
CONF_SENSORS,
SENSOR_DOMAIN,
sensor_platform.rewrite_legacy_to_modern_conf,
),
(
CONF_BINARY_SENSORS,
BINARY_SENSOR_DOMAIN,
binary_sensor_platform.rewrite_legacy_to_modern_conf,
),
):
if old_key not in template_config:
continue
if not legacy_warn_printed:
legacy_warn_printed = True
logging.getLogger(__name__).warning(
"The entity definition format under template: differs from the"
" platform "
"configuration format. See "
"https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors"
)
definitions = (
list(template_config[new_key]) if new_key in template_config else []
)
definitions.extend(transform(hass, template_config[old_key]))
template_config = TemplateConfig({**template_config, new_key: definitions})
config_sections.append(template_config)
# Create a copy of the configuration with all config for current
# component removed and add validated config back in.
config = config_without_domain(config, DOMAIN)
config[DOMAIN] = config_sections
return config