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."""
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"])

View file

@ -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:

View file

@ -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", {})

View file

@ -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()

View file

@ -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(

View file

@ -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(

View file

@ -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:

View file

@ -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)

View file

@ -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)])