Set automations which fail validation unavailable (#94856)

This commit is contained in:
Erik Montnemery 2023-06-27 18:23:33 +02:00 committed by GitHub
parent 5c4d010b90
commit 17ac1a6d32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 383 additions and 53 deletions

View File

@ -1,6 +1,7 @@
"""Allow to set up simple automation rules via the config file.""" """Allow to set up simple automation rules via the config file."""
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio import asyncio
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
@ -153,7 +154,7 @@ def _automations_with_x(
if DOMAIN not in hass.data: if DOMAIN not in hass.data:
return [] return []
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
return [ return [
automation_entity.entity_id automation_entity.entity_id
@ -169,7 +170,7 @@ def _x_in_automation(
if DOMAIN not in hass.data: if DOMAIN not in hass.data:
return [] return []
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
if (automation_entity := component.get_entity(entity_id)) is None: if (automation_entity := component.get_entity(entity_id)) is None:
return [] return []
@ -219,7 +220,7 @@ def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list
if DOMAIN not in hass.data: if DOMAIN not in hass.data:
return [] return []
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
return [ return [
automation_entity.entity_id automation_entity.entity_id
@ -234,7 +235,7 @@ def blueprint_in_automation(hass: HomeAssistant, entity_id: str) -> str | None:
if DOMAIN not in hass.data: if DOMAIN not in hass.data:
return None return None
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
if (automation_entity := component.get_entity(entity_id)) is None: if (automation_entity := component.get_entity(entity_id)) is None:
return None return None
@ -244,7 +245,7 @@ def blueprint_in_automation(hass: HomeAssistant, entity_id: str) -> str | None:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up all automations.""" """Set up all automations."""
hass.data[DOMAIN] = component = EntityComponent[AutomationEntity]( hass.data[DOMAIN] = component = EntityComponent[BaseAutomationEntity](
LOGGER, DOMAIN, hass LOGGER, DOMAIN, hass
) )
@ -262,7 +263,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await async_get_blueprints(hass).async_populate() await async_get_blueprints(hass).async_populate()
async def trigger_service_handler( async def trigger_service_handler(
entity: AutomationEntity, service_call: ServiceCall entity: BaseAutomationEntity, service_call: ServiceCall
) -> None: ) -> None:
"""Handle forced automation trigger, e.g. from frontend.""" """Handle forced automation trigger, e.g. from frontend."""
await entity.async_trigger( await entity.async_trigger(
@ -310,7 +311,103 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
class AutomationEntity(ToggleEntity, RestoreEntity): class BaseAutomationEntity(ToggleEntity, ABC):
"""Base class for automation entities."""
raw_config: ConfigType | None
@property
def capability_attributes(self) -> dict[str, Any] | None:
"""Return capability attributes."""
if self.unique_id is not None:
return {CONF_ID: self.unique_id}
return 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."""
@abstractmethod
async def async_trigger(
self,
run_variables: dict[str, Any],
context: Context | None = None,
skip_condition: bool = False,
) -> None:
"""Trigger automation."""
class UnavailableAutomationEntity(BaseAutomationEntity):
"""A non-functional automation entity with its state set to unavailable.
This class is instatiated when an automation fails to validate.
"""
_attr_should_poll = False
_attr_available = False
def __init__(
self,
automation_id: str | None,
name: str,
raw_config: ConfigType | None,
) -> None:
"""Initialize an automation entity."""
self._name = name
self._attr_unique_id = automation_id
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()
async def async_trigger(
self,
run_variables: dict[str, Any],
context: Context | None = None,
skip_condition: bool = False,
) -> None:
"""Trigger automation."""
class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Entity to show status of entity.""" """Entity to show status of entity."""
_attr_should_poll = False _attr_should_poll = False
@ -363,8 +460,6 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
} }
if self.action_script.supports_max: if self.action_script.supports_max:
attrs[ATTR_MAX] = self.action_script.max_runs attrs[ATTR_MAX] = self.action_script.max_runs
if self.unique_id is not None:
attrs[CONF_ID] = self.unique_id
return attrs return attrs
@property @property
@ -686,6 +781,7 @@ class AutomationEntityConfig:
list_no: int list_no: int
raw_blueprint_inputs: ConfigType | None raw_blueprint_inputs: ConfigType | None
raw_config: ConfigType | None raw_config: ConfigType | None
validation_failed: bool
async def _prepare_automation_config( async def _prepare_automation_config(
@ -700,9 +796,14 @@ async def _prepare_automation_config(
for list_no, config_block in enumerate(conf): for list_no, config_block in enumerate(conf):
raw_config = cast(AutomationConfig, config_block).raw_config raw_config = cast(AutomationConfig, config_block).raw_config
raw_blueprint_inputs = cast(AutomationConfig, config_block).raw_blueprint_inputs raw_blueprint_inputs = cast(AutomationConfig, config_block).raw_blueprint_inputs
validation_failed = cast(AutomationConfig, config_block).validation_failed
automation_configs.append( automation_configs.append(
AutomationEntityConfig( AutomationEntityConfig(
config_block, list_no, raw_blueprint_inputs, raw_config config_block,
list_no,
raw_blueprint_inputs,
raw_config,
validation_failed,
) )
) )
@ -718,9 +819,9 @@ def _automation_name(automation_config: AutomationEntityConfig) -> str:
async def _create_automation_entities( async def _create_automation_entities(
hass: HomeAssistant, automation_configs: list[AutomationEntityConfig] hass: HomeAssistant, automation_configs: list[AutomationEntityConfig]
) -> list[AutomationEntity]: ) -> list[BaseAutomationEntity]:
"""Create automation entities from prepared configuration.""" """Create automation entities from prepared configuration."""
entities: list[AutomationEntity] = [] entities: list[BaseAutomationEntity] = []
for automation_config in automation_configs: for automation_config in automation_configs:
config_block = automation_config.config_block config_block = automation_config.config_block
@ -728,6 +829,16 @@ async def _create_automation_entities(
automation_id: str | None = config_block.get(CONF_ID) automation_id: str | None = config_block.get(CONF_ID)
name = _automation_name(automation_config) name = _automation_name(automation_config)
if automation_config.validation_failed:
entities.append(
UnavailableAutomationEntity(
automation_id,
name,
automation_config.raw_config,
)
)
continue
initial_state: bool | None = config_block.get(CONF_INITIAL_STATE) initial_state: bool | None = config_block.get(CONF_INITIAL_STATE)
action_script = Script( action_script = Script(
@ -786,18 +897,18 @@ async def _create_automation_entities(
async def _async_process_config( async def _async_process_config(
hass: HomeAssistant, hass: HomeAssistant,
config: dict[str, Any], config: dict[str, Any],
component: EntityComponent[AutomationEntity], component: EntityComponent[BaseAutomationEntity],
) -> None: ) -> None:
"""Process config and add automations.""" """Process config and add automations."""
def automation_matches_config( def automation_matches_config(
automation: AutomationEntity, config: AutomationEntityConfig automation: BaseAutomationEntity, config: AutomationEntityConfig
) -> bool: ) -> bool:
name = _automation_name(config) name = _automation_name(config)
return automation.name == name and automation.raw_config == config.raw_config return automation.name == name and automation.raw_config == config.raw_config
def find_matches( def find_matches(
automations: list[AutomationEntity], automations: list[BaseAutomationEntity],
automation_configs: list[AutomationEntityConfig], automation_configs: list[AutomationEntityConfig],
) -> tuple[set[int], set[int]]: ) -> tuple[set[int], set[int]]:
"""Find matches between a list of automation entities and a list of configurations. """Find matches between a list of automation entities and a list of configurations.
@ -843,7 +954,7 @@ async def _async_process_config(
return automation_matches, config_matches return automation_matches, config_matches
automation_configs = await _prepare_automation_config(hass, config) automation_configs = await _prepare_automation_config(hass, config)
automations: list[AutomationEntity] = list(component.entities) automations: list[BaseAutomationEntity] = list(component.entities)
# Find automations and configurations which have matches # Find automations and configurations which have matches
automation_matches, config_matches = find_matches(automations, automation_configs) automation_matches, config_matches = find_matches(automations, automation_configs)
@ -968,7 +1079,7 @@ def websocket_config(
msg: dict[str, Any], msg: dict[str, Any],
) -> None: ) -> None:
"""Get automation config.""" """Get automation config."""
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
automation = component.get_entity(msg["entity_id"]) automation = component.get_entity(msg["entity_id"])

View File

@ -43,6 +43,16 @@ PACKAGE_MERGE_HINT = "list"
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
_MINIMAL_PLATFORM_SCHEMA = vol.Schema(
{
CONF_ID: str,
CONF_ALIAS: cv.string,
vol.Optional(CONF_DESCRIPTION): cv.string,
},
extra=vol.ALLOW_EXTRA,
)
PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_HIDE_ENTITY), cv.deprecated(CONF_HIDE_ENTITY),
script.make_script_schema( script.make_script_schema(
@ -68,6 +78,7 @@ PLATFORM_SCHEMA = vol.All(
async def _async_validate_config_item( async def _async_validate_config_item(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config: ConfigType,
raise_on_errors: bool,
warn_on_errors: bool, warn_on_errors: bool,
) -> AutomationConfig: ) -> AutomationConfig:
"""Validate config item.""" """Validate config item."""
@ -104,6 +115,15 @@ async def _async_validate_config_item(
) )
return return
def _minimal_config() -> AutomationConfig:
"""Try validating id, alias and description."""
minimal_config = _MINIMAL_PLATFORM_SCHEMA(config)
automation_config = AutomationConfig(minimal_config)
automation_config.raw_blueprint_inputs = raw_blueprint_inputs
automation_config.raw_config = raw_config
automation_config.validation_failed = True
return automation_config
if blueprint.is_blueprint_instance_config(config): if blueprint.is_blueprint_instance_config(config):
uses_blueprint = True uses_blueprint = True
blueprints = async_get_blueprints(hass) blueprints = async_get_blueprints(hass)
@ -115,7 +135,9 @@ async def _async_validate_config_item(
"Failed to generate automation from blueprint: %s", "Failed to generate automation from blueprint: %s",
err, err,
) )
raise if raise_on_errors:
raise
return _minimal_config()
raw_blueprint_inputs = blueprint_inputs.config_with_inputs raw_blueprint_inputs = blueprint_inputs.config_with_inputs
@ -130,7 +152,9 @@ async def _async_validate_config_item(
blueprint_inputs.inputs, blueprint_inputs.inputs,
err, err,
) )
raise HomeAssistantError from err if raise_on_errors:
raise HomeAssistantError(err) from err
return _minimal_config()
automation_name = "Unnamed automation" automation_name = "Unnamed automation"
if isinstance(config, Mapping): if isinstance(config, Mapping):
@ -143,10 +167,16 @@ async def _async_validate_config_item(
validated_config = PLATFORM_SCHEMA(config) validated_config = PLATFORM_SCHEMA(config)
except vol.Invalid as err: except vol.Invalid as err:
_log_invalid_automation(err, automation_name, "could not be validated", config) _log_invalid_automation(err, automation_name, "could not be validated", config)
raise if raise_on_errors:
raise
return _minimal_config()
automation_config = AutomationConfig(validated_config)
automation_config.raw_blueprint_inputs = raw_blueprint_inputs
automation_config.raw_config = raw_config
try: try:
validated_config[CONF_TRIGGER] = await async_validate_trigger_config( automation_config[CONF_TRIGGER] = await async_validate_trigger_config(
hass, validated_config[CONF_TRIGGER] hass, validated_config[CONF_TRIGGER]
) )
except ( except (
@ -156,11 +186,14 @@ async def _async_validate_config_item(
_log_invalid_automation( _log_invalid_automation(
err, automation_name, "failed to setup triggers", validated_config err, automation_name, "failed to setup triggers", validated_config
) )
raise if raise_on_errors:
raise
automation_config.validation_failed = True
return automation_config
if CONF_CONDITION in validated_config: if CONF_CONDITION in validated_config:
try: try:
validated_config[CONF_CONDITION] = await async_validate_conditions_config( automation_config[CONF_CONDITION] = await async_validate_conditions_config(
hass, validated_config[CONF_CONDITION] hass, validated_config[CONF_CONDITION]
) )
except ( except (
@ -170,10 +203,13 @@ async def _async_validate_config_item(
_log_invalid_automation( _log_invalid_automation(
err, automation_name, "failed to setup conditions", validated_config err, automation_name, "failed to setup conditions", validated_config
) )
raise if raise_on_errors:
raise
automation_config.validation_failed = True
return automation_config
try: try:
validated_config[CONF_ACTION] = await script.async_validate_actions_config( automation_config[CONF_ACTION] = await script.async_validate_actions_config(
hass, validated_config[CONF_ACTION] hass, validated_config[CONF_ACTION]
) )
except ( except (
@ -183,11 +219,11 @@ async def _async_validate_config_item(
_log_invalid_automation( _log_invalid_automation(
err, automation_name, "failed to setup actions", validated_config err, automation_name, "failed to setup actions", validated_config
) )
raise if raise_on_errors:
raise
automation_config.validation_failed = True
return automation_config
automation_config = AutomationConfig(validated_config)
automation_config.raw_blueprint_inputs = raw_blueprint_inputs
automation_config.raw_config = raw_config
return automation_config return automation_config
@ -196,6 +232,7 @@ class AutomationConfig(dict):
raw_config: dict[str, Any] | None = None raw_config: dict[str, Any] | None = None
raw_blueprint_inputs: dict[str, Any] | None = None raw_blueprint_inputs: dict[str, Any] | None = None
validation_failed: bool = False
async def _try_async_validate_config_item( async def _try_async_validate_config_item(
@ -204,7 +241,7 @@ async def _try_async_validate_config_item(
) -> AutomationConfig | None: ) -> AutomationConfig | None:
"""Validate config item.""" """Validate config item."""
try: try:
return await _async_validate_config_item(hass, config, True) return await _async_validate_config_item(hass, config, False, True)
except (vol.Invalid, HomeAssistantError): except (vol.Invalid, HomeAssistantError):
return None return None
@ -215,7 +252,7 @@ async def async_validate_config_item(
config: dict[str, Any], config: dict[str, Any],
) -> AutomationConfig | None: ) -> AutomationConfig | None:
"""Validate config item, called by EditAutomationConfigView.""" """Validate config item, called by EditAutomationConfigView."""
return await _async_validate_config_item(hass, config, False) return await _async_validate_config_item(hass, config, True, False)
async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType: async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType:

View File

@ -25,6 +25,7 @@ from homeassistant.const import (
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE,
) )
from homeassistant.core import ( from homeassistant.core import (
Context, Context,
@ -1428,8 +1429,13 @@ async def test_automation_bad_config_validation(
f" {details}" f" {details}"
) in caplog.text ) in caplog.text
# Make sure one bad automation does not prevent other automations from setting up # Make sure both automations are setup
assert hass.states.async_entity_ids("automation") == ["automation.good_automation"] assert set(hass.states.async_entity_ids("automation")) == {
"automation.bad_automation",
"automation.good_automation",
}
# The automation failing validation should be unavailable
assert hass.states.get("automation.bad_automation").state == STATE_UNAVAILABLE
async def test_automation_with_error_in_script( async def test_automation_with_error_in_script(
@ -1558,6 +1564,31 @@ async def test_extraction_functions_unknown_automation(hass: HomeAssistant) -> N
assert automation.entities_in_automation(hass, "automation.unknown") == [] assert automation.entities_in_automation(hass, "automation.unknown") == []
async def test_extraction_functions_unavailable_automation(hass: HomeAssistant) -> None:
"""Test extraction functions for an unknown automation."""
entity_id = "automation.test1"
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"alias": "test1",
}
]
},
)
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
assert automation.automations_with_area(hass, "area-in-both") == []
assert automation.areas_in_automation(hass, entity_id) == []
assert automation.automations_with_blueprint(hass, "blabla.yaml") == []
assert automation.blueprint_in_automation(hass, entity_id) is None
assert automation.automations_with_device(hass, "device-in-both") == []
assert automation.devices_in_automation(hass, entity_id) == []
assert automation.automations_with_entity(hass, "light.in_both") == []
assert automation.entities_in_automation(hass, entity_id) == []
async def test_extraction_functions(hass: HomeAssistant) -> None: async def test_extraction_functions(hass: HomeAssistant) -> None:
"""Test extraction functions.""" """Test extraction functions."""
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})

View File

@ -1,14 +1,17 @@
"""Test Automation config panel.""" """Test Automation config panel."""
from http import HTTPStatus from http import HTTPStatus
import json import json
from typing import Any
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from homeassistant.bootstrap import async_setup_component from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config from homeassistant.components import config
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import yaml
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
@ -75,8 +78,11 @@ async def test_update_automation_config(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids("automation")) == [ assert sorted(hass.states.async_entity_ids("automation")) == [
"automation.automation_0" "automation.automation_0",
"automation.automation_1",
] ]
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
assert hass.states.get("automation.automation_1").state == STATE_ON
assert resp.status == HTTPStatus.OK assert resp.status == HTTPStatus.OK
result = await resp.json() result = await resp.json()
@ -88,12 +94,61 @@ async def test_update_automation_config(
@pytest.mark.parametrize("automation_config", ({},)) @pytest.mark.parametrize("automation_config", ({},))
@pytest.mark.parametrize(
("updated_config", "validation_error"),
[
(
{"action": []},
"required key not provided @ data['trigger']",
),
(
{
"trigger": {"platform": "automation"},
"action": [],
},
"Integration 'automation' does not provide trigger support",
),
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"condition": {
"condition": "state",
# The UUID will fail being resolved to en entity_id
"entity_id": "abcdabcdabcdabcdabcdabcdabcdabcd",
"state": "blah",
},
"action": [],
},
"Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd",
),
(
{
"trigger": {"platform": "event", "event_type": "test_event"},
"action": {
"condition": "state",
# The UUID will fail being resolved to en entity_id
"entity_id": "abcdabcdabcdabcdabcdabcdabcdabcd",
"state": "blah",
},
},
"Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd",
),
(
{
"use_blueprint": {"path": "test_event_service.yaml", "input": {}},
},
"Missing input a_number, service_to_call, trigger_event",
),
],
)
async def test_update_automation_config_with_error( async def test_update_automation_config_with_error(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
hass_config_store, hass_config_store,
setup_automation, setup_automation,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
updated_config: Any,
validation_error: str,
) -> None: ) -> None:
"""Test updating automation config with errors.""" """Test updating automation config with errors."""
with patch.object(config, "SECTIONS", ["automation"]): with patch.object(config, "SECTIONS", ["automation"]):
@ -108,14 +163,70 @@ async def test_update_automation_config_with_error(
resp = await client.post( resp = await client.post(
"/api/config/automation/config/moon", "/api/config/automation/config/moon",
data=json.dumps({"action": []}), data=json.dumps(updated_config),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids("automation")) == [] assert sorted(hass.states.async_entity_ids("automation")) == []
assert resp.status != HTTPStatus.OK assert resp.status != HTTPStatus.OK
result = await resp.json() result = await resp.json()
validation_error = "required key not provided @ data['trigger']" assert result == {"message": f"Message malformed: {validation_error}"}
# Assert the validation error is not logged
assert validation_error not in caplog.text
@pytest.mark.parametrize("automation_config", ({},))
@pytest.mark.parametrize(
("updated_config", "validation_error"),
[
(
{
"use_blueprint": {
"path": "test_event_service.yaml",
"input": {
"trigger_event": "test_event",
"service_to_call": "test.automation",
"a_number": 5,
},
},
},
"No substitution found for input blah",
),
],
)
async def test_update_automation_config_with_blueprint_substitution_error(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_config_store,
setup_automation,
caplog: pytest.LogCaptureFixture,
updated_config: Any,
validation_error: str,
) -> None:
"""Test updating automation config with errors."""
with patch.object(config, "SECTIONS", ["automation"]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("automation")) == []
client = await hass_client()
orig_data = [{"id": "sun"}, {"id": "moon"}]
hass_config_store["automations.yaml"] = orig_data
with patch(
"homeassistant.components.blueprint.models.BlueprintInputs.async_substitute",
side_effect=yaml.UndefinedSubstitution("blah"),
):
resp = await client.post(
"/api/config/automation/config/moon",
data=json.dumps(updated_config),
)
await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids("automation")) == []
assert resp.status != HTTPStatus.OK
result = await resp.json()
assert result == {"message": f"Message malformed: {validation_error}"} assert result == {"message": f"Message malformed: {validation_error}"}
# Assert the validation error is not logged # Assert the validation error is not logged
assert validation_error not in caplog.text assert validation_error not in caplog.text
@ -145,8 +256,11 @@ async def test_update_remove_key_automation_config(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids("automation")) == [ assert sorted(hass.states.async_entity_ids("automation")) == [
"automation.automation_0" "automation.automation_0",
"automation.automation_1",
] ]
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
assert hass.states.get("automation.automation_1").state == STATE_ON
assert resp.status == HTTPStatus.OK assert resp.status == HTTPStatus.OK
result = await resp.json() result = await resp.json()
@ -187,8 +301,11 @@ async def test_bad_formatted_automations(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert sorted(hass.states.async_entity_ids("automation")) == [ assert sorted(hass.states.async_entity_ids("automation")) == [
"automation.automation_0" "automation.automation_0",
"automation.automation_1",
] ]
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
assert hass.states.get("automation.automation_1").state == STATE_ON
assert resp.status == HTTPStatus.OK assert resp.status == HTTPStatus.OK
result = await resp.json() result = await resp.json()

View File

@ -24,6 +24,7 @@ from homeassistant.const import (
CONF_DOMAIN, CONF_DOMAIN,
CONF_PLATFORM, CONF_PLATFORM,
CONF_TYPE, CONF_TYPE,
STATE_UNAVAILABLE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
@ -440,7 +441,9 @@ async def test_validate_trigger_unsupported_device(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 automations = hass.states.async_entity_ids(AUTOMATION_DOMAIN)
assert len(automations) == 1
assert hass.states.get(automations[0]).state == STATE_UNAVAILABLE
async def test_validate_trigger_unsupported_trigger( async def test_validate_trigger_unsupported_trigger(
@ -481,7 +484,9 @@ async def test_validate_trigger_unsupported_trigger(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 automations = hass.states.async_entity_ids(AUTOMATION_DOMAIN)
assert len(automations) == 1
assert hass.states.get(automations[0]).state == STATE_UNAVAILABLE
async def test_attach_trigger_no_matching_event( async def test_attach_trigger_no_matching_event(

View File

@ -10,7 +10,12 @@ import homeassistant.components.automation as automation
from homeassistant.components.homeassistant.triggers import ( from homeassistant.components.homeassistant.triggers import (
numeric_state as numeric_state_trigger, numeric_state as numeric_state_trigger,
) )
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.const import (
ATTR_ENTITY_ID,
ENTITY_MATCH_ALL,
SERVICE_TURN_OFF,
STATE_UNAVAILABLE,
)
from homeassistant.core import Context, HomeAssistant from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -1090,7 +1095,7 @@ async def test_if_fails_setup_bad_for(hass: HomeAssistant, calls, above, below)
hass.states.async_set("test.entity", 5) hass.states.async_set("test.entity", 5)
await hass.async_block_till_done() await hass.async_block_till_done()
with assert_setup_component(0, automation.DOMAIN): with assert_setup_component(1, automation.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -1107,13 +1112,14 @@ async def test_if_fails_setup_bad_for(hass: HomeAssistant, calls, above, below)
} }
}, },
) )
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
async def test_if_fails_setup_for_without_above_below( async def test_if_fails_setup_for_without_above_below(
hass: HomeAssistant, calls hass: HomeAssistant, calls
) -> None: ) -> None:
"""Test for setup failures for missing above or below.""" """Test for setup failures for missing above or below."""
with assert_setup_component(0, automation.DOMAIN): with assert_setup_component(1, automation.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -1128,6 +1134,7 @@ async def test_if_fails_setup_for_without_above_below(
} }
}, },
) )
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -6,7 +6,12 @@ import pytest
import homeassistant.components.automation as automation import homeassistant.components.automation as automation
from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.components.homeassistant.triggers import state as state_trigger
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.const import (
ATTR_ENTITY_ID,
ENTITY_MATCH_ALL,
SERVICE_TURN_OFF,
STATE_UNAVAILABLE,
)
from homeassistant.core import Context, HomeAssistant, ServiceCall from homeassistant.core import Context, HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -554,7 +559,7 @@ async def test_if_action(hass: HomeAssistant, calls) -> None:
async def test_if_fails_setup_if_to_boolean_value(hass: HomeAssistant, calls) -> None: async def test_if_fails_setup_if_to_boolean_value(hass: HomeAssistant, calls) -> None:
"""Test for setup failure for boolean to.""" """Test for setup failure for boolean to."""
with assert_setup_component(0, automation.DOMAIN): with assert_setup_component(1, automation.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -569,11 +574,12 @@ async def test_if_fails_setup_if_to_boolean_value(hass: HomeAssistant, calls) ->
} }
}, },
) )
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
async def test_if_fails_setup_if_from_boolean_value(hass: HomeAssistant, calls) -> None: async def test_if_fails_setup_if_from_boolean_value(hass: HomeAssistant, calls) -> None:
"""Test for setup failure for boolean from.""" """Test for setup failure for boolean from."""
with assert_setup_component(0, automation.DOMAIN): with assert_setup_component(1, automation.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -588,11 +594,12 @@ async def test_if_fails_setup_if_from_boolean_value(hass: HomeAssistant, calls)
} }
}, },
) )
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
async def test_if_fails_setup_bad_for(hass: HomeAssistant, calls) -> None: async def test_if_fails_setup_bad_for(hass: HomeAssistant, calls) -> None:
"""Test for setup failure for bad for.""" """Test for setup failure for bad for."""
with assert_setup_component(0, automation.DOMAIN): with assert_setup_component(1, automation.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -608,6 +615,7 @@ async def test_if_fails_setup_bad_for(hass: HomeAssistant, calls) -> None:
} }
}, },
) )
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
async def test_if_not_fires_on_entity_change_with_for( async def test_if_not_fires_on_entity_change_with_for(
@ -1018,7 +1026,7 @@ async def test_if_fires_on_for_condition_attribute_change(
async def test_if_fails_setup_for_without_time(hass: HomeAssistant, calls) -> None: async def test_if_fails_setup_for_without_time(hass: HomeAssistant, calls) -> None:
"""Test for setup failure if no time is provided.""" """Test for setup failure if no time is provided."""
with assert_setup_component(0, automation.DOMAIN): with assert_setup_component(1, automation.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -1035,11 +1043,12 @@ async def test_if_fails_setup_for_without_time(hass: HomeAssistant, calls) -> No
} }
}, },
) )
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
async def test_if_fails_setup_for_without_entity(hass: HomeAssistant, calls) -> None: async def test_if_fails_setup_for_without_entity(hass: HomeAssistant, calls) -> None:
"""Test for setup failure if no entity is provided.""" """Test for setup failure if no entity is provided."""
with assert_setup_component(0, automation.DOMAIN): with assert_setup_component(1, automation.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -1055,6 +1064,7 @@ async def test_if_fails_setup_for_without_entity(hass: HomeAssistant, calls) ->
} }
}, },
) )
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
async def test_wait_template_with_trigger(hass: HomeAssistant, calls) -> None: async def test_wait_template_with_trigger(hass: HomeAssistant, calls) -> None:

View File

@ -8,7 +8,12 @@ import voluptuous as vol
from homeassistant.components import automation from homeassistant.components import automation
from homeassistant.components.homeassistant.triggers import time from homeassistant.components.homeassistant.triggers import time
from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, SERVICE_TURN_OFF from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -210,7 +215,7 @@ async def test_if_not_fires_using_wrong_at(hass: HomeAssistant, calls) -> None:
with patch( with patch(
"homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away
): ):
with assert_setup_component(0, automation.DOMAIN): with assert_setup_component(1, automation.DOMAIN):
assert await async_setup_component( assert await async_setup_component(
hass, hass,
automation.DOMAIN, automation.DOMAIN,
@ -226,6 +231,7 @@ async def test_if_not_fires_using_wrong_at(hass: HomeAssistant, calls) -> None:
}, },
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
async_fire_time_changed( async_fire_time_changed(
hass, now.replace(year=now.year + 1, hour=1, minute=0, second=5) hass, now.replace(year=now.year + 1, hour=1, minute=0, second=5)

View File

@ -7,7 +7,12 @@ import pytest
import homeassistant.components.automation as automation import homeassistant.components.automation as automation
from homeassistant.components.template import trigger as template_trigger from homeassistant.components.template import trigger as template_trigger
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.const import (
ATTR_ENTITY_ID,
ENTITY_MATCH_ALL,
SERVICE_TURN_OFF,
STATE_UNAVAILABLE,
)
from homeassistant.core import Context, HomeAssistant, callback from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -389,7 +394,7 @@ async def test_if_action(hass: HomeAssistant, start_ha, calls) -> None:
assert len(calls) == 1 assert len(calls) == 1
@pytest.mark.parametrize(("count", "domain"), [(0, automation.DOMAIN)]) @pytest.mark.parametrize(("count", "domain"), [(1, automation.DOMAIN)])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", "config",
[ [
@ -405,6 +410,7 @@ async def test_if_fires_on_change_with_bad_template(
hass: HomeAssistant, start_ha, calls hass: HomeAssistant, start_ha, calls
) -> None: ) -> None:
"""Test for firing on change with bad template.""" """Test for firing on change with bad template."""
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
@pytest.mark.parametrize(("count", "domain"), [(1, automation.DOMAIN)]) @pytest.mark.parametrize(("count", "domain"), [(1, automation.DOMAIN)])