Set scripts which fail validation unavailable (#95381)

This commit is contained in:
Erik Montnemery
2023-06-27 18:24:34 +02:00
committed by GitHub
parent 17ac1a6d32
commit 1fec407a24
4 changed files with 357 additions and 39 deletions
+122 -16
View File
@@ -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"])
+44 -11
View File
@@ -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):