mirror of
https://github.com/home-assistant/core.git
synced 2026-05-19 23:35:20 +02:00
Set scripts which fail validation unavailable (#95381)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Support for scripts."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
@@ -94,12 +95,12 @@ def _scripts_with_x(
|
||||
if DOMAIN not in hass.data:
|
||||
return []
|
||||
|
||||
component: EntityComponent[ScriptEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseScriptEntity] = hass.data[DOMAIN]
|
||||
|
||||
return [
|
||||
script_entity.entity_id
|
||||
for script_entity in component.entities
|
||||
if referenced_id in getattr(script_entity.script, property_name)
|
||||
if referenced_id in getattr(script_entity, property_name)
|
||||
]
|
||||
|
||||
|
||||
@@ -108,12 +109,12 @@ def _x_in_script(hass: HomeAssistant, entity_id: str, property_name: str) -> lis
|
||||
if DOMAIN not in hass.data:
|
||||
return []
|
||||
|
||||
component: EntityComponent[ScriptEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseScriptEntity] = hass.data[DOMAIN]
|
||||
|
||||
if (script_entity := component.get_entity(entity_id)) is None:
|
||||
return []
|
||||
|
||||
return list(getattr(script_entity.script, property_name))
|
||||
return list(getattr(script_entity, property_name))
|
||||
|
||||
|
||||
@callback
|
||||
@@ -158,7 +159,7 @@ def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str
|
||||
if DOMAIN not in hass.data:
|
||||
return []
|
||||
|
||||
component: EntityComponent[ScriptEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseScriptEntity] = hass.data[DOMAIN]
|
||||
|
||||
return [
|
||||
script_entity.entity_id
|
||||
@@ -173,7 +174,7 @@ def blueprint_in_script(hass: HomeAssistant, entity_id: str) -> str | None:
|
||||
if DOMAIN not in hass.data:
|
||||
return None
|
||||
|
||||
component: EntityComponent[ScriptEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseScriptEntity] = hass.data[DOMAIN]
|
||||
|
||||
if (script_entity := component.get_entity(entity_id)) is None:
|
||||
return None
|
||||
@@ -183,7 +184,9 @@ def blueprint_in_script(hass: HomeAssistant, entity_id: str) -> str | None:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Load the scripts from the configuration."""
|
||||
hass.data[DOMAIN] = component = EntityComponent[ScriptEntity](LOGGER, DOMAIN, hass)
|
||||
hass.data[DOMAIN] = component = EntityComponent[BaseScriptEntity](
|
||||
LOGGER, DOMAIN, hass
|
||||
)
|
||||
|
||||
# Process integration platforms right away since
|
||||
# we will create entities before firing EVENT_COMPONENT_LOADED
|
||||
@@ -260,6 +263,7 @@ class ScriptEntityConfig:
|
||||
key: str
|
||||
raw_blueprint_inputs: ConfigType | None
|
||||
raw_config: ConfigType | None
|
||||
validation_failed: bool
|
||||
|
||||
|
||||
async def _prepare_script_config(
|
||||
@@ -274,9 +278,12 @@ async def _prepare_script_config(
|
||||
for key, config_block in conf.items():
|
||||
raw_config = cast(ScriptConfig, config_block).raw_config
|
||||
raw_blueprint_inputs = cast(ScriptConfig, config_block).raw_blueprint_inputs
|
||||
validation_failed = cast(ScriptConfig, config_block).validation_failed
|
||||
|
||||
script_configs.append(
|
||||
ScriptEntityConfig(config_block, key, raw_blueprint_inputs, raw_config)
|
||||
ScriptEntityConfig(
|
||||
config_block, key, raw_blueprint_inputs, raw_config, validation_failed
|
||||
)
|
||||
)
|
||||
|
||||
return script_configs
|
||||
@@ -284,11 +291,20 @@ async def _prepare_script_config(
|
||||
|
||||
async def _create_script_entities(
|
||||
hass: HomeAssistant, script_configs: list[ScriptEntityConfig]
|
||||
) -> list[ScriptEntity]:
|
||||
) -> list[BaseScriptEntity]:
|
||||
"""Create script entities from prepared configuration."""
|
||||
entities: list[ScriptEntity] = []
|
||||
entities: list[BaseScriptEntity] = []
|
||||
|
||||
for script_config in script_configs:
|
||||
if script_config.validation_failed:
|
||||
entities.append(
|
||||
UnavailableScriptEntity(
|
||||
script_config.key,
|
||||
script_config.raw_config,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
entity = ScriptEntity(
|
||||
hass,
|
||||
script_config.key,
|
||||
@@ -302,16 +318,20 @@ async def _create_script_entities(
|
||||
|
||||
|
||||
async def _async_process_config(
|
||||
hass: HomeAssistant, config: ConfigType, component: EntityComponent[ScriptEntity]
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
component: EntityComponent[BaseScriptEntity],
|
||||
) -> None:
|
||||
"""Process script configuration."""
|
||||
entities = []
|
||||
|
||||
def script_matches_config(script: ScriptEntity, config: ScriptEntityConfig) -> bool:
|
||||
def script_matches_config(
|
||||
script: BaseScriptEntity, config: ScriptEntityConfig
|
||||
) -> bool:
|
||||
return script.unique_id == config.key and script.raw_config == config.raw_config
|
||||
|
||||
def find_matches(
|
||||
scripts: list[ScriptEntity],
|
||||
scripts: list[BaseScriptEntity],
|
||||
script_configs: list[ScriptEntityConfig],
|
||||
) -> tuple[set[int], set[int]]:
|
||||
"""Find matches between a list of script entities and a list of configurations.
|
||||
@@ -338,7 +358,7 @@ async def _async_process_config(
|
||||
return script_matches, config_matches
|
||||
|
||||
script_configs = await _prepare_script_config(hass, config)
|
||||
scripts: list[ScriptEntity] = list(component.entities)
|
||||
scripts: list[BaseScriptEntity] = list(component.entities)
|
||||
|
||||
# Find scripts and configurations which have matches
|
||||
script_matches, config_matches = find_matches(scripts, script_configs)
|
||||
@@ -359,7 +379,78 @@ async def _async_process_config(
|
||||
await component.async_add_entities(entities)
|
||||
|
||||
|
||||
class ScriptEntity(ToggleEntity, RestoreEntity):
|
||||
class BaseScriptEntity(ToggleEntity, ABC):
|
||||
"""Base class for script entities."""
|
||||
|
||||
raw_config: ConfigType | None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def referenced_areas(self) -> set[str]:
|
||||
"""Return a set of referenced areas."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def referenced_blueprint(self) -> str | None:
|
||||
"""Return referenced blueprint or None."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def referenced_devices(self) -> set[str]:
|
||||
"""Return a set of referenced devices."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def referenced_entities(self) -> set[str]:
|
||||
"""Return a set of referenced entities."""
|
||||
|
||||
|
||||
class UnavailableScriptEntity(BaseScriptEntity):
|
||||
"""A non-functional script entity with its state set to unavailable.
|
||||
|
||||
This class is instatiated when an script fails to validate.
|
||||
"""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_available = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: str,
|
||||
raw_config: ConfigType | None,
|
||||
) -> None:
|
||||
"""Initialize a script entity."""
|
||||
self._name = raw_config.get(CONF_ALIAS, key) if raw_config else key
|
||||
self._attr_unique_id = key
|
||||
self.raw_config = raw_config
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def referenced_areas(self) -> set[str]:
|
||||
"""Return a set of referenced areas."""
|
||||
return set()
|
||||
|
||||
@property
|
||||
def referenced_blueprint(self) -> str | None:
|
||||
"""Return referenced blueprint or None."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def referenced_devices(self) -> set[str]:
|
||||
"""Return a set of referenced devices."""
|
||||
return set()
|
||||
|
||||
@property
|
||||
def referenced_entities(self) -> set[str]:
|
||||
"""Return a set of referenced entities."""
|
||||
return set()
|
||||
|
||||
|
||||
class ScriptEntity(BaseScriptEntity, RestoreEntity):
|
||||
"""Representation of a script entity."""
|
||||
|
||||
icon = None
|
||||
@@ -421,6 +512,11 @@ class ScriptEntity(ToggleEntity, RestoreEntity):
|
||||
"""Return true if script is on."""
|
||||
return self.script.is_running
|
||||
|
||||
@property
|
||||
def referenced_areas(self) -> set[str]:
|
||||
"""Return a set of referenced areas."""
|
||||
return self.script.referenced_areas
|
||||
|
||||
@property
|
||||
def referenced_blueprint(self):
|
||||
"""Return referenced blueprint or None."""
|
||||
@@ -428,6 +524,16 @@ class ScriptEntity(ToggleEntity, RestoreEntity):
|
||||
return None
|
||||
return self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH]
|
||||
|
||||
@property
|
||||
def referenced_devices(self) -> set[str]:
|
||||
"""Return a set of referenced devices."""
|
||||
return self.script.referenced_devices
|
||||
|
||||
@property
|
||||
def referenced_entities(self) -> set[str]:
|
||||
"""Return a set of referenced entities."""
|
||||
return self.script.referenced_entities
|
||||
|
||||
@callback
|
||||
def async_change_listener(self):
|
||||
"""Update state."""
|
||||
@@ -544,7 +650,7 @@ def websocket_config(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Get script config."""
|
||||
component: EntityComponent[ScriptEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseScriptEntity] = hass.data[DOMAIN]
|
||||
|
||||
script = component.get_entity(msg["entity_id"])
|
||||
|
||||
|
||||
@@ -49,6 +49,15 @@ from .helpers import async_get_blueprints
|
||||
|
||||
PACKAGE_MERGE_HINT = "dict"
|
||||
|
||||
_MINIMAL_SCRIPT_ENTITY_SCHEMA = vol.Schema(
|
||||
{
|
||||
CONF_ALIAS: cv.string,
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
SCRIPT_ENTITY_SCHEMA = make_script_schema(
|
||||
{
|
||||
vol.Optional(CONF_ALIAS): cv.string,
|
||||
@@ -74,7 +83,11 @@ SCRIPT_ENTITY_SCHEMA = make_script_schema(
|
||||
|
||||
|
||||
async def _async_validate_config_item(
|
||||
hass: HomeAssistant, object_id: str, config: ConfigType, warn_on_errors: bool
|
||||
hass: HomeAssistant,
|
||||
object_id: str,
|
||||
config: ConfigType,
|
||||
raise_on_errors: bool,
|
||||
warn_on_errors: bool,
|
||||
) -> ScriptConfig:
|
||||
"""Validate config item."""
|
||||
raw_config = None
|
||||
@@ -110,6 +123,15 @@ async def _async_validate_config_item(
|
||||
)
|
||||
return
|
||||
|
||||
def _minimal_config() -> ScriptConfig:
|
||||
"""Try validating id, alias and description."""
|
||||
minimal_config = _MINIMAL_SCRIPT_ENTITY_SCHEMA(config)
|
||||
script_config = ScriptConfig(minimal_config)
|
||||
script_config.raw_blueprint_inputs = raw_blueprint_inputs
|
||||
script_config.raw_config = raw_config
|
||||
script_config.validation_failed = True
|
||||
return script_config
|
||||
|
||||
if is_blueprint_instance_config(config):
|
||||
uses_blueprint = True
|
||||
blueprints = async_get_blueprints(hass)
|
||||
@@ -121,7 +143,9 @@ async def _async_validate_config_item(
|
||||
"Failed to generate script from blueprint: %s",
|
||||
err,
|
||||
)
|
||||
raise
|
||||
if raise_on_errors:
|
||||
raise
|
||||
return _minimal_config()
|
||||
|
||||
raw_blueprint_inputs = blueprint_inputs.config_with_inputs
|
||||
|
||||
@@ -136,7 +160,9 @@ async def _async_validate_config_item(
|
||||
blueprint_inputs.inputs,
|
||||
err,
|
||||
)
|
||||
raise HomeAssistantError from err
|
||||
if raise_on_errors:
|
||||
raise HomeAssistantError(err) from err
|
||||
return _minimal_config()
|
||||
|
||||
script_name = f"Script with object id '{object_id}'"
|
||||
if isinstance(config, Mapping):
|
||||
@@ -152,10 +178,16 @@ async def _async_validate_config_item(
|
||||
validated_config = SCRIPT_ENTITY_SCHEMA(config)
|
||||
except vol.Invalid as err:
|
||||
_log_invalid_script(err, script_name, "could not be validated", config)
|
||||
raise
|
||||
if raise_on_errors:
|
||||
raise
|
||||
return _minimal_config()
|
||||
|
||||
script_config = ScriptConfig(validated_config)
|
||||
script_config.raw_blueprint_inputs = raw_blueprint_inputs
|
||||
script_config.raw_config = raw_config
|
||||
|
||||
try:
|
||||
validated_config[CONF_SEQUENCE] = await async_validate_actions_config(
|
||||
script_config[CONF_SEQUENCE] = await async_validate_actions_config(
|
||||
hass, validated_config[CONF_SEQUENCE]
|
||||
)
|
||||
except (
|
||||
@@ -165,11 +197,11 @@ async def _async_validate_config_item(
|
||||
_log_invalid_script(
|
||||
err, script_name, "failed to setup actions", validated_config
|
||||
)
|
||||
raise
|
||||
if raise_on_errors:
|
||||
raise
|
||||
script_config.validation_failed = True
|
||||
return script_config
|
||||
|
||||
script_config = ScriptConfig(validated_config)
|
||||
script_config.raw_blueprint_inputs = raw_blueprint_inputs
|
||||
script_config.raw_config = raw_config
|
||||
return script_config
|
||||
|
||||
|
||||
@@ -178,6 +210,7 @@ class ScriptConfig(dict):
|
||||
|
||||
raw_config: ConfigType | None = None
|
||||
raw_blueprint_inputs: ConfigType | None = None
|
||||
validation_failed: bool = False
|
||||
|
||||
|
||||
async def _try_async_validate_config_item(
|
||||
@@ -187,7 +220,7 @@ async def _try_async_validate_config_item(
|
||||
) -> ScriptConfig | None:
|
||||
"""Validate config item."""
|
||||
try:
|
||||
return await _async_validate_config_item(hass, object_id, config, True)
|
||||
return await _async_validate_config_item(hass, object_id, config, False, True)
|
||||
except (vol.Invalid, HomeAssistantError):
|
||||
return None
|
||||
|
||||
@@ -198,7 +231,7 @@ async def async_validate_config_item(
|
||||
config: dict[str, Any],
|
||||
) -> ScriptConfig | None:
|
||||
"""Validate config item, called by EditScriptConfigView."""
|
||||
return await _async_validate_config_item(hass, object_id, config, False)
|
||||
return await _async_validate_config_item(hass, object_id, config, True, False)
|
||||
|
||||
|
||||
async def async_validate_config(hass, config):
|
||||
|
||||
Reference in New Issue
Block a user