diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e356f641395..f4db7831235 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -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"]) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index c127208377f..ae12554b89d 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -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: diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index d1eb93771da..0d983864e44 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -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", {}) diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index ac6780c0972..11f17199e5a 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -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() diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index b2bff759085..fe2ba8d4177 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -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( diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index ff82fa3e69c..2098d266f0d 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -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( diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index a16d542becd..d58e3dd7c6e 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -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: diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 1af687a4f5d..0a41df17c8d 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -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) diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index c85c7a6d57b..84fdadfec0d 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -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)])