Set automations which fail validation unavailable (#94856)
This commit is contained in:
parent
5c4d010b90
commit
17ac1a6d32
9 changed files with 383 additions and 53 deletions
|
@ -1,6 +1,7 @@
|
|||
"""Allow to set up simple automation rules via the config file."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
|
@ -153,7 +154,7 @@ def _automations_with_x(
|
|||
if DOMAIN not in hass.data:
|
||||
return []
|
||||
|
||||
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
|
||||
|
||||
return [
|
||||
automation_entity.entity_id
|
||||
|
@ -169,7 +170,7 @@ def _x_in_automation(
|
|||
if DOMAIN not in hass.data:
|
||||
return []
|
||||
|
||||
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
|
||||
|
||||
if (automation_entity := component.get_entity(entity_id)) is None:
|
||||
return []
|
||||
|
@ -219,7 +220,7 @@ def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list
|
|||
if DOMAIN not in hass.data:
|
||||
return []
|
||||
|
||||
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
|
||||
|
||||
return [
|
||||
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:
|
||||
return None
|
||||
|
||||
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
|
||||
|
||||
if (automation_entity := component.get_entity(entity_id)) is 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:
|
||||
"""Set up all automations."""
|
||||
hass.data[DOMAIN] = component = EntityComponent[AutomationEntity](
|
||||
hass.data[DOMAIN] = component = EntityComponent[BaseAutomationEntity](
|
||||
LOGGER, DOMAIN, hass
|
||||
)
|
||||
|
||||
|
@ -262,7 +263,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
await async_get_blueprints(hass).async_populate()
|
||||
|
||||
async def trigger_service_handler(
|
||||
entity: AutomationEntity, service_call: ServiceCall
|
||||
entity: BaseAutomationEntity, service_call: ServiceCall
|
||||
) -> None:
|
||||
"""Handle forced automation trigger, e.g. from frontend."""
|
||||
await entity.async_trigger(
|
||||
|
@ -310,7 +311,103 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
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."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
@ -363,8 +460,6 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||
}
|
||||
if self.action_script.supports_max:
|
||||
attrs[ATTR_MAX] = self.action_script.max_runs
|
||||
if self.unique_id is not None:
|
||||
attrs[CONF_ID] = self.unique_id
|
||||
return attrs
|
||||
|
||||
@property
|
||||
|
@ -686,6 +781,7 @@ class AutomationEntityConfig:
|
|||
list_no: int
|
||||
raw_blueprint_inputs: ConfigType | None
|
||||
raw_config: ConfigType | None
|
||||
validation_failed: bool
|
||||
|
||||
|
||||
async def _prepare_automation_config(
|
||||
|
@ -700,9 +796,14 @@ async def _prepare_automation_config(
|
|||
for list_no, config_block in enumerate(conf):
|
||||
raw_config = cast(AutomationConfig, config_block).raw_config
|
||||
raw_blueprint_inputs = cast(AutomationConfig, config_block).raw_blueprint_inputs
|
||||
validation_failed = cast(AutomationConfig, config_block).validation_failed
|
||||
automation_configs.append(
|
||||
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(
|
||||
hass: HomeAssistant, automation_configs: list[AutomationEntityConfig]
|
||||
) -> list[AutomationEntity]:
|
||||
) -> list[BaseAutomationEntity]:
|
||||
"""Create automation entities from prepared configuration."""
|
||||
entities: list[AutomationEntity] = []
|
||||
entities: list[BaseAutomationEntity] = []
|
||||
|
||||
for automation_config in automation_configs:
|
||||
config_block = automation_config.config_block
|
||||
|
@ -728,6 +829,16 @@ async def _create_automation_entities(
|
|||
automation_id: str | None = config_block.get(CONF_ID)
|
||||
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)
|
||||
|
||||
action_script = Script(
|
||||
|
@ -786,18 +897,18 @@ async def _create_automation_entities(
|
|||
async def _async_process_config(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
component: EntityComponent[AutomationEntity],
|
||||
component: EntityComponent[BaseAutomationEntity],
|
||||
) -> None:
|
||||
"""Process config and add automations."""
|
||||
|
||||
def automation_matches_config(
|
||||
automation: AutomationEntity, config: AutomationEntityConfig
|
||||
automation: BaseAutomationEntity, config: AutomationEntityConfig
|
||||
) -> bool:
|
||||
name = _automation_name(config)
|
||||
return automation.name == name and automation.raw_config == config.raw_config
|
||||
|
||||
def find_matches(
|
||||
automations: list[AutomationEntity],
|
||||
automations: list[BaseAutomationEntity],
|
||||
automation_configs: list[AutomationEntityConfig],
|
||||
) -> tuple[set[int], set[int]]:
|
||||
"""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
|
||||
|
||||
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
|
||||
automation_matches, config_matches = find_matches(automations, automation_configs)
|
||||
|
@ -968,7 +1079,7 @@ def websocket_config(
|
|||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Get automation config."""
|
||||
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN]
|
||||
component: EntityComponent[BaseAutomationEntity] = hass.data[DOMAIN]
|
||||
|
||||
automation = component.get_entity(msg["entity_id"])
|
||||
|
||||
|
|
|
@ -43,6 +43,16 @@ PACKAGE_MERGE_HINT = "list"
|
|||
|
||||
_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(
|
||||
cv.deprecated(CONF_HIDE_ENTITY),
|
||||
script.make_script_schema(
|
||||
|
@ -68,6 +78,7 @@ PLATFORM_SCHEMA = vol.All(
|
|||
async def _async_validate_config_item(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
raise_on_errors: bool,
|
||||
warn_on_errors: bool,
|
||||
) -> AutomationConfig:
|
||||
"""Validate config item."""
|
||||
|
@ -104,6 +115,15 @@ async def _async_validate_config_item(
|
|||
)
|
||||
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):
|
||||
uses_blueprint = True
|
||||
blueprints = async_get_blueprints(hass)
|
||||
|
@ -115,7 +135,9 @@ async def _async_validate_config_item(
|
|||
"Failed to generate automation from blueprint: %s",
|
||||
err,
|
||||
)
|
||||
raise
|
||||
if raise_on_errors:
|
||||
raise
|
||||
return _minimal_config()
|
||||
|
||||
raw_blueprint_inputs = blueprint_inputs.config_with_inputs
|
||||
|
||||
|
@ -130,7 +152,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()
|
||||
|
||||
automation_name = "Unnamed automation"
|
||||
if isinstance(config, Mapping):
|
||||
|
@ -143,10 +167,16 @@ async def _async_validate_config_item(
|
|||
validated_config = PLATFORM_SCHEMA(config)
|
||||
except vol.Invalid as err:
|
||||
_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:
|
||||
validated_config[CONF_TRIGGER] = await async_validate_trigger_config(
|
||||
automation_config[CONF_TRIGGER] = await async_validate_trigger_config(
|
||||
hass, validated_config[CONF_TRIGGER]
|
||||
)
|
||||
except (
|
||||
|
@ -156,11 +186,14 @@ async def _async_validate_config_item(
|
|||
_log_invalid_automation(
|
||||
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:
|
||||
try:
|
||||
validated_config[CONF_CONDITION] = await async_validate_conditions_config(
|
||||
automation_config[CONF_CONDITION] = await async_validate_conditions_config(
|
||||
hass, validated_config[CONF_CONDITION]
|
||||
)
|
||||
except (
|
||||
|
@ -170,10 +203,13 @@ async def _async_validate_config_item(
|
|||
_log_invalid_automation(
|
||||
err, automation_name, "failed to setup conditions", validated_config
|
||||
)
|
||||
raise
|
||||
if raise_on_errors:
|
||||
raise
|
||||
automation_config.validation_failed = True
|
||||
return automation_config
|
||||
|
||||
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]
|
||||
)
|
||||
except (
|
||||
|
@ -183,11 +219,11 @@ async def _async_validate_config_item(
|
|||
_log_invalid_automation(
|
||||
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
|
||||
|
||||
|
||||
|
@ -196,6 +232,7 @@ class AutomationConfig(dict):
|
|||
|
||||
raw_config: dict[str, Any] | None = None
|
||||
raw_blueprint_inputs: dict[str, Any] | None = None
|
||||
validation_failed: bool = False
|
||||
|
||||
|
||||
async def _try_async_validate_config_item(
|
||||
|
@ -204,7 +241,7 @@ async def _try_async_validate_config_item(
|
|||
) -> AutomationConfig | None:
|
||||
"""Validate config item."""
|
||||
try:
|
||||
return await _async_validate_config_item(hass, config, True)
|
||||
return await _async_validate_config_item(hass, config, False, True)
|
||||
except (vol.Invalid, HomeAssistantError):
|
||||
return None
|
||||
|
||||
|
@ -215,7 +252,7 @@ async def async_validate_config_item(
|
|||
config: dict[str, Any],
|
||||
) -> AutomationConfig | None:
|
||||
"""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:
|
||||
|
|
|
@ -25,6 +25,7 @@ from homeassistant.const import (
|
|||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
Context,
|
||||
|
@ -1428,8 +1429,13 @@ async def test_automation_bad_config_validation(
|
|||
f" {details}"
|
||||
) in caplog.text
|
||||
|
||||
# Make sure one bad automation does not prevent other automations from setting up
|
||||
assert hass.states.async_entity_ids("automation") == ["automation.good_automation"]
|
||||
# Make sure both automations are setup
|
||||
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(
|
||||
|
@ -1558,6 +1564,31 @@ async def test_extraction_functions_unknown_automation(hass: HomeAssistant) -> N
|
|||
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:
|
||||
"""Test extraction functions."""
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
"""Test Automation config panel."""
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.bootstrap import async_setup_component
|
||||
from homeassistant.components import config
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import yaml
|
||||
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
@ -75,8 +78,11 @@ async def test_update_automation_config(
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
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
|
||||
result = await resp.json()
|
||||
|
@ -88,12 +94,61 @@ async def test_update_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(
|
||||
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"]):
|
||||
|
@ -108,14 +163,70 @@ async def test_update_automation_config_with_error(
|
|||
|
||||
resp = await client.post(
|
||||
"/api/config/automation/config/moon",
|
||||
data=json.dumps({"action": []}),
|
||||
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()
|
||||
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 the validation error is not logged
|
||||
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()
|
||||
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
|
||||
result = await resp.json()
|
||||
|
@ -187,8 +301,11 @@ async def test_bad_formatted_automations(
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
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
|
||||
result = await resp.json()
|
||||
|
|
|
@ -24,6 +24,7 @@ from homeassistant.const import (
|
|||
CONF_DOMAIN,
|
||||
CONF_PLATFORM,
|
||||
CONF_TYPE,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
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()
|
||||
|
||||
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(
|
||||
|
@ -481,7 +484,9 @@ async def test_validate_trigger_unsupported_trigger(
|
|||
)
|
||||
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(
|
||||
|
|
|
@ -10,7 +10,12 @@ import homeassistant.components.automation as automation
|
|||
from homeassistant.components.homeassistant.triggers import (
|
||||
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.helpers import entity_registry as er
|
||||
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)
|
||||
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(
|
||||
hass,
|
||||
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(
|
||||
hass: HomeAssistant, calls
|
||||
) -> None:
|
||||
"""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(
|
||||
hass,
|
||||
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(
|
||||
|
|
|
@ -6,7 +6,12 @@ import pytest
|
|||
|
||||
import homeassistant.components.automation as automation
|
||||
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.helpers import entity_registry as er
|
||||
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:
|
||||
"""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(
|
||||
hass,
|
||||
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:
|
||||
"""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(
|
||||
hass,
|
||||
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:
|
||||
"""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(
|
||||
hass,
|
||||
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(
|
||||
|
@ -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:
|
||||
"""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(
|
||||
hass,
|
||||
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:
|
||||
"""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(
|
||||
hass,
|
||||
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:
|
||||
|
|
|
@ -8,7 +8,12 @@ import voluptuous as vol
|
|||
from homeassistant.components import automation
|
||||
from homeassistant.components.homeassistant.triggers import time
|
||||
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.setup import async_setup_component
|
||||
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(
|
||||
"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(
|
||||
hass,
|
||||
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()
|
||||
assert hass.states.get("automation.automation_0").state == STATE_UNAVAILABLE
|
||||
|
||||
async_fire_time_changed(
|
||||
hass, now.replace(year=now.year + 1, hour=1, minute=0, second=5)
|
||||
|
|
|
@ -7,7 +7,12 @@ import pytest
|
|||
|
||||
import homeassistant.components.automation as automation
|
||||
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.setup import async_setup_component
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("count", "domain"), [(0, automation.DOMAIN)])
|
||||
@pytest.mark.parametrize(("count", "domain"), [(1, automation.DOMAIN)])
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
|
@ -405,6 +410,7 @@ async def test_if_fires_on_change_with_bad_template(
|
|||
hass: HomeAssistant, start_ha, calls
|
||||
) -> None:
|
||||
"""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)])
|
||||
|
|
Loading…
Add table
Reference in a new issue