mirror of
https://github.com/home-assistant/core.git
synced 2025-11-28 12:08:04 +00:00
Compare commits
14 Commits
add-includ
...
frenck-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa13d64586 | ||
|
|
0f780254e1 | ||
|
|
9e40972b11 | ||
|
|
07ef61dd8d | ||
|
|
1bf6771a54 | ||
|
|
e7a7cb829e | ||
|
|
6f6b2f1ad3 | ||
|
|
1cc4890f75 | ||
|
|
d3dd9b26c9 | ||
|
|
a64d61df05 | ||
|
|
e7c6c5311d | ||
|
|
72a524c868 | ||
|
|
b437113f31 | ||
|
|
e0e263d3b5 |
@@ -12,5 +12,10 @@
|
|||||||
"motion": {
|
"motion": {
|
||||||
"default": "mdi:motion-sensor"
|
"default": "mdi:motion-sensor"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"triggers": {
|
||||||
|
"detected": {
|
||||||
|
"trigger": "mdi:eye-check"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,5 +21,18 @@
|
|||||||
"name": "Motion"
|
"name": "Motion"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Event"
|
"title": "Event",
|
||||||
|
"triggers": {
|
||||||
|
"detected": {
|
||||||
|
"description": "Triggers when an event is detected.",
|
||||||
|
"description_configured": "Triggers when an event is detected",
|
||||||
|
"fields": {
|
||||||
|
"event_type": {
|
||||||
|
"description": "The event types to trigger on. If empty, triggers on all event types.",
|
||||||
|
"name": "Event types"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When an event is detected"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
homeassistant/components/event/trigger.py
Normal file
116
homeassistant/components/event/trigger.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""Provides triggers for events."""
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, cast, override
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_OPTIONS,
|
||||||
|
CONF_TARGET,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.target import (
|
||||||
|
TargetStateChangedData,
|
||||||
|
async_track_target_selector_state_change_event,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import ATTR_EVENT_TYPE, DOMAIN
|
||||||
|
|
||||||
|
EVENT_TRIGGER_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_OPTIONS, default={}): {
|
||||||
|
vol.Optional(ATTR_EVENT_TYPE, default=[]): vol.All(
|
||||||
|
cv.ensure_list, [cv.string]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EventDetectedTrigger(Trigger):
|
||||||
|
"""Trigger for when an event is detected."""
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
async def async_validate_config(
|
||||||
|
cls, hass: HomeAssistant, config: ConfigType
|
||||||
|
) -> ConfigType:
|
||||||
|
"""Validate config."""
|
||||||
|
return cast(ConfigType, EVENT_TRIGGER_SCHEMA(config))
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||||
|
"""Initialize the event detected trigger."""
|
||||||
|
super().__init__(hass, config)
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert config.options is not None
|
||||||
|
assert config.target is not None
|
||||||
|
self._options = config.options
|
||||||
|
self._target = config.target
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_attach_runner(
|
||||||
|
self, run_action: TriggerActionRunner
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Attach the trigger to an action runner."""
|
||||||
|
event_types_filter = self._options[ATTR_EVENT_TYPE]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def state_change_listener(
|
||||||
|
target_state_change_data: TargetStateChangedData,
|
||||||
|
) -> None:
|
||||||
|
"""Listen for state changes and call action."""
|
||||||
|
event = target_state_change_data.state_change_event
|
||||||
|
entity_id = event.data["entity_id"]
|
||||||
|
from_state = event.data["old_state"]
|
||||||
|
to_state = event.data["new_state"]
|
||||||
|
|
||||||
|
# Ignore unavailable states
|
||||||
|
if to_state is None or to_state.state == STATE_UNAVAILABLE:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Trigger on any state change (event detection)
|
||||||
|
# Events can have the same event_type triggered sequentially
|
||||||
|
|
||||||
|
# If event_types filter is specified, check if the event_type matches
|
||||||
|
if event_types_filter:
|
||||||
|
event_type = to_state.attributes.get(ATTR_EVENT_TYPE)
|
||||||
|
if event_type not in event_types_filter:
|
||||||
|
return
|
||||||
|
|
||||||
|
run_action(
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
"from_state": from_state,
|
||||||
|
"to_state": to_state,
|
||||||
|
},
|
||||||
|
f"event detected on {entity_id}",
|
||||||
|
event.context,
|
||||||
|
)
|
||||||
|
|
||||||
|
def entity_filter(entities: set[str]) -> set[str]:
|
||||||
|
"""Filter entities of this domain."""
|
||||||
|
return {
|
||||||
|
entity_id
|
||||||
|
for entity_id in entities
|
||||||
|
if split_entity_id(entity_id)[0] == DOMAIN
|
||||||
|
}
|
||||||
|
|
||||||
|
return async_track_target_selector_state_change_event(
|
||||||
|
self._hass, self._target, state_change_listener, entity_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
|
"detected": EventDetectedTrigger,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||||
|
"""Return the triggers for events."""
|
||||||
|
return TRIGGERS
|
||||||
13
homeassistant/components/event/triggers.yaml
Normal file
13
homeassistant/components/event/triggers.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
detected:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: event
|
||||||
|
fields:
|
||||||
|
event_type:
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
multiple: true
|
||||||
|
custom_value: true
|
||||||
|
options: []
|
||||||
@@ -770,7 +770,9 @@ class ManifestJSONView(HomeAssistantView):
|
|||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
"type": "frontend/get_icons",
|
"type": "frontend/get_icons",
|
||||||
vol.Required("category"): vol.In({"entity", "entity_component", "services"}),
|
vol.Required("category"): vol.In(
|
||||||
|
{"entity", "entity_component", "services", "triggers"}
|
||||||
|
),
|
||||||
vol.Optional("integration"): vol.All(cv.ensure_list, [str]),
|
vol.Optional("integration"): vol.All(cv.ensure_list, [str]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
131
homeassistant/components/light/condition.py
Normal file
131
homeassistant/components/light/condition.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"""Provides conditions for lights."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Any, Final, override
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_OPTIONS, CONF_TARGET, STATE_OFF, STATE_ON
|
||||||
|
from homeassistant.core import HomeAssistant, split_entity_id
|
||||||
|
from homeassistant.helpers import config_validation as cv, target
|
||||||
|
from homeassistant.helpers.condition import (
|
||||||
|
Condition,
|
||||||
|
ConditionCheckerType,
|
||||||
|
ConditionConfig,
|
||||||
|
trace_condition_function,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
ATTR_BEHAVIOR: Final = "behavior"
|
||||||
|
BEHAVIOR_ANY: Final = "any"
|
||||||
|
BEHAVIOR_ALL: Final = "all"
|
||||||
|
|
||||||
|
|
||||||
|
STATE_CONDITION_VALID_STATES: Final = [STATE_ON, STATE_OFF]
|
||||||
|
STATE_CONDITION_OPTIONS_SCHEMA: dict[vol.Marker, Any] = {
|
||||||
|
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
|
||||||
|
[BEHAVIOR_ANY, BEHAVIOR_ALL]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
STATE_CONDITION_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||||
|
vol.Required(CONF_OPTIONS): STATE_CONDITION_OPTIONS_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StateConditionBase(Condition):
|
||||||
|
"""State condition."""
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
async def async_validate_config(
|
||||||
|
cls, hass: HomeAssistant, config: ConfigType
|
||||||
|
) -> ConfigType:
|
||||||
|
"""Validate config."""
|
||||||
|
return STATE_CONDITION_SCHEMA(config) # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, config: ConditionConfig, state: str
|
||||||
|
) -> None:
|
||||||
|
"""Initialize condition."""
|
||||||
|
self._hass = hass
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert config.target
|
||||||
|
assert config.options
|
||||||
|
self._target = config.target
|
||||||
|
self._behavior = config.options[ATTR_BEHAVIOR]
|
||||||
|
self._state = state
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_get_checker(self) -> ConditionCheckerType:
|
||||||
|
"""Get the condition checker."""
|
||||||
|
|
||||||
|
def check_any_match_state(states: list[str]) -> bool:
|
||||||
|
"""Test if any entity match the state."""
|
||||||
|
return any(state == self._state for state in states)
|
||||||
|
|
||||||
|
def check_all_match_state(states: list[str]) -> bool:
|
||||||
|
"""Test if all entities match the state."""
|
||||||
|
return all(state == self._state for state in states)
|
||||||
|
|
||||||
|
matcher: Callable[[list[str]], bool]
|
||||||
|
if self._behavior == BEHAVIOR_ANY:
|
||||||
|
matcher = check_any_match_state
|
||||||
|
elif self._behavior == BEHAVIOR_ALL:
|
||||||
|
matcher = check_all_match_state
|
||||||
|
|
||||||
|
@trace_condition_function
|
||||||
|
def test_state(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
||||||
|
"""Test state condition."""
|
||||||
|
selector_data = target.TargetSelectorData(self._target)
|
||||||
|
targeted_entities = target.async_extract_referenced_entity_ids(
|
||||||
|
hass, selector_data, expand_group=False
|
||||||
|
)
|
||||||
|
referenced_entity_ids = targeted_entities.referenced.union(
|
||||||
|
targeted_entities.indirectly_referenced
|
||||||
|
)
|
||||||
|
light_entity_ids = {
|
||||||
|
entity_id
|
||||||
|
for entity_id in referenced_entity_ids
|
||||||
|
if split_entity_id(entity_id)[0] == DOMAIN
|
||||||
|
}
|
||||||
|
light_entity_states = [
|
||||||
|
state.state
|
||||||
|
for entity_id in light_entity_ids
|
||||||
|
if (state := hass.states.get(entity_id))
|
||||||
|
and state.state in STATE_CONDITION_VALID_STATES
|
||||||
|
]
|
||||||
|
return matcher(light_entity_states)
|
||||||
|
|
||||||
|
return test_state
|
||||||
|
|
||||||
|
|
||||||
|
class IsOnCondition(StateConditionBase):
|
||||||
|
"""Is on condition."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||||
|
"""Initialize condition."""
|
||||||
|
super().__init__(hass, config, STATE_ON)
|
||||||
|
|
||||||
|
|
||||||
|
class IsOffCondition(StateConditionBase):
|
||||||
|
"""Is off condition."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||||
|
"""Initialize condition."""
|
||||||
|
super().__init__(hass, config, STATE_OFF)
|
||||||
|
|
||||||
|
|
||||||
|
CONDITIONS: dict[str, type[Condition]] = {
|
||||||
|
"is_off": IsOffCondition,
|
||||||
|
"is_on": IsOnCondition,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||||
|
"""Return the light conditions."""
|
||||||
|
return CONDITIONS
|
||||||
28
homeassistant/components/light/conditions.yaml
Normal file
28
homeassistant/components/light/conditions.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
is_off:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: light
|
||||||
|
fields:
|
||||||
|
behavior:
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: condition_behavior
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- any
|
||||||
|
is_on:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: light
|
||||||
|
fields:
|
||||||
|
behavior:
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: condition_behavior
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- any
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"conditions": {
|
||||||
|
"is_off": {
|
||||||
|
"condition": "mdi:lightbulb-off"
|
||||||
|
},
|
||||||
|
"is_on": {
|
||||||
|
"condition": "mdi:lightbulb-on"
|
||||||
|
}
|
||||||
|
},
|
||||||
"entity_component": {
|
"entity_component": {
|
||||||
"_": {
|
"_": {
|
||||||
"default": "mdi:lightbulb",
|
"default": "mdi:lightbulb",
|
||||||
@@ -25,5 +33,13 @@
|
|||||||
"turn_on": {
|
"turn_on": {
|
||||||
"service": "mdi:lightbulb-on"
|
"service": "mdi:lightbulb-on"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"triggers": {
|
||||||
|
"turned_off": {
|
||||||
|
"trigger": "mdi:lightbulb-off"
|
||||||
|
},
|
||||||
|
"turned_on": {
|
||||||
|
"trigger": "mdi:lightbulb-on"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,30 @@
|
|||||||
"field_xy_color_name": "XY-color",
|
"field_xy_color_name": "XY-color",
|
||||||
"section_advanced_fields_name": "Advanced options"
|
"section_advanced_fields_name": "Advanced options"
|
||||||
},
|
},
|
||||||
|
"conditions": {
|
||||||
|
"is_off": {
|
||||||
|
"description": "Test if a light is off.",
|
||||||
|
"description_configured": "Test if a light is off",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "How the state should match on the targeted lights.",
|
||||||
|
"name": "Behavior"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "If a light is off"
|
||||||
|
},
|
||||||
|
"is_on": {
|
||||||
|
"description": "Test if a light is on.",
|
||||||
|
"description_configured": "Test if a light is on",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "How the state should match on the targeted lights.",
|
||||||
|
"name": "Behavior"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "If a light is on"
|
||||||
|
}
|
||||||
|
},
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
"action_type": {
|
"action_type": {
|
||||||
"brightness_decrease": "Decrease {entity_name} brightness",
|
"brightness_decrease": "Decrease {entity_name} brightness",
|
||||||
@@ -284,11 +308,30 @@
|
|||||||
"yellowgreen": "Yellow green"
|
"yellowgreen": "Yellow green"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"condition_behavior": {
|
||||||
|
"options": {
|
||||||
|
"all": "All",
|
||||||
|
"any": "Any"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flash": {
|
"flash": {
|
||||||
"options": {
|
"options": {
|
||||||
"long": "Long",
|
"long": "Long",
|
||||||
"short": "Short"
|
"short": "Short"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"options": {
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
|
"on": "[%key:common::state::on%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trigger_behavior": {
|
||||||
|
"options": {
|
||||||
|
"any": "Any",
|
||||||
|
"first": "First",
|
||||||
|
"last": "Last"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
@@ -462,5 +505,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Light"
|
"title": "Light",
|
||||||
|
"triggers": {
|
||||||
|
"turned_off": {
|
||||||
|
"description": "Triggers when a light is turned off.",
|
||||||
|
"description_configured": "Triggers when a light is turned off",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "The behavior of the targeted lights to trigger on.",
|
||||||
|
"name": "Behavior"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a light is turned off"
|
||||||
|
},
|
||||||
|
"turned_on": {
|
||||||
|
"description": "Triggers when a light is turned on.",
|
||||||
|
"description_configured": "Triggers when a light is turned on",
|
||||||
|
"fields": {
|
||||||
|
"behavior": {
|
||||||
|
"description": "[%key:component::light::triggers::turned_off::fields::behavior::description%]",
|
||||||
|
"name": "[%key:component::light::triggers::turned_off::fields::behavior::name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "When a light is turned on"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
162
homeassistant/components/light/trigger.py
Normal file
162
homeassistant/components/light/trigger.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""Provides triggers for lights."""
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Final, cast, override
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_TARGET, STATE_OFF, STATE_ON
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.event import process_state_match
|
||||||
|
from homeassistant.helpers.target import (
|
||||||
|
TargetStateChangedData,
|
||||||
|
async_track_target_selector_state_change_event,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
# remove when #151314 is merged
|
||||||
|
CONF_OPTIONS: Final = "options"
|
||||||
|
|
||||||
|
ATTR_BEHAVIOR: Final = "behavior"
|
||||||
|
BEHAVIOR_FIRST: Final = "first"
|
||||||
|
BEHAVIOR_LAST: Final = "last"
|
||||||
|
BEHAVIOR_ANY: Final = "any"
|
||||||
|
|
||||||
|
STATE_TRIGGER_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_OPTIONS): {
|
||||||
|
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
|
||||||
|
[BEHAVIOR_FIRST, BEHAVIOR_LAST, BEHAVIOR_ANY]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StateTriggerBase(Trigger):
|
||||||
|
"""Trigger for state changes."""
|
||||||
|
|
||||||
|
@override
|
||||||
|
@classmethod
|
||||||
|
async def async_validate_config(
|
||||||
|
cls, hass: HomeAssistant, config: ConfigType
|
||||||
|
) -> ConfigType:
|
||||||
|
"""Validate config."""
|
||||||
|
return cast(ConfigType, STATE_TRIGGER_SCHEMA(config))
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: TriggerConfig, state: str) -> None:
|
||||||
|
"""Initialize the state trigger."""
|
||||||
|
super().__init__(hass, config)
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert config.options is not None
|
||||||
|
assert config.target is not None
|
||||||
|
self._options = config.options
|
||||||
|
self._target = config.target
|
||||||
|
self._state = state
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_attach_runner(
|
||||||
|
self, run_action: TriggerActionRunner
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Attach the trigger to an action runner."""
|
||||||
|
match_config_state = process_state_match(self._state)
|
||||||
|
|
||||||
|
def check_all_match(entity_ids: set[str]) -> bool:
|
||||||
|
"""Check if all entity states match."""
|
||||||
|
return all(
|
||||||
|
match_config_state(state.state)
|
||||||
|
for entity_id in entity_ids
|
||||||
|
if (state := self._hass.states.get(entity_id)) is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_one_match(entity_ids: set[str]) -> bool:
|
||||||
|
"""Check that only one entity state matches."""
|
||||||
|
return (
|
||||||
|
sum(
|
||||||
|
match_config_state(state.state)
|
||||||
|
for entity_id in entity_ids
|
||||||
|
if (state := self._hass.states.get(entity_id)) is not None
|
||||||
|
)
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|
||||||
|
behavior = self._options.get(ATTR_BEHAVIOR)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def state_change_listener(
|
||||||
|
target_state_change_data: TargetStateChangedData,
|
||||||
|
) -> None:
|
||||||
|
"""Listen for state changes and call action."""
|
||||||
|
event = target_state_change_data.state_change_event
|
||||||
|
entity_id = event.data["entity_id"]
|
||||||
|
from_state = event.data["old_state"]
|
||||||
|
to_state = event.data["new_state"]
|
||||||
|
|
||||||
|
if to_state is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# This check is required for "first" behavior, to check that it went from zero
|
||||||
|
# entities matching the state to one. Otherwise, if previously there were two
|
||||||
|
# entities on the desired state and one changed, this would trigger.
|
||||||
|
# For "last" behavior it is not required, but serves as a quicker fail check.
|
||||||
|
if not match_config_state(to_state.state):
|
||||||
|
return
|
||||||
|
if behavior == BEHAVIOR_LAST:
|
||||||
|
if not check_all_match(target_state_change_data.targeted_entity_ids):
|
||||||
|
return
|
||||||
|
elif behavior == BEHAVIOR_FIRST:
|
||||||
|
if not check_one_match(target_state_change_data.targeted_entity_ids):
|
||||||
|
return
|
||||||
|
|
||||||
|
run_action(
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
"from_state": from_state,
|
||||||
|
"to_state": to_state,
|
||||||
|
},
|
||||||
|
f"state of {entity_id}",
|
||||||
|
event.context,
|
||||||
|
)
|
||||||
|
|
||||||
|
def entity_filter(entities: set[str]) -> set[str]:
|
||||||
|
"""Filter entities of this domain."""
|
||||||
|
return {
|
||||||
|
entity_id
|
||||||
|
for entity_id in entities
|
||||||
|
if split_entity_id(entity_id)[0] == DOMAIN
|
||||||
|
}
|
||||||
|
|
||||||
|
return async_track_target_selector_state_change_event(
|
||||||
|
self._hass, self._target, state_change_listener, entity_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TurnedOnTrigger(StateTriggerBase):
|
||||||
|
"""Trigger for when a light is turned on."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||||
|
"""Initialize the ON state trigger."""
|
||||||
|
super().__init__(hass, config, STATE_ON)
|
||||||
|
|
||||||
|
|
||||||
|
class TurnedOffTrigger(StateTriggerBase):
|
||||||
|
"""Trigger for when a light is turned off."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||||
|
"""Initialize the OFF state trigger."""
|
||||||
|
super().__init__(hass, config, STATE_OFF)
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGERS: dict[str, type[Trigger]] = {
|
||||||
|
"turned_off": TurnedOffTrigger,
|
||||||
|
"turned_on": TurnedOnTrigger,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||||
|
"""Return the triggers for lights."""
|
||||||
|
return TRIGGERS
|
||||||
31
homeassistant/components/light/triggers.yaml
Normal file
31
homeassistant/components/light/triggers.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
turned_on:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: light
|
||||||
|
fields:
|
||||||
|
behavior:
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- first
|
||||||
|
- last
|
||||||
|
- any
|
||||||
|
translation_key: behavior
|
||||||
|
|
||||||
|
turned_off:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: light
|
||||||
|
fields:
|
||||||
|
behavior:
|
||||||
|
required: true
|
||||||
|
default: any
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: trigger_behavior
|
||||||
|
options:
|
||||||
|
- first
|
||||||
|
- last
|
||||||
|
- any
|
||||||
@@ -467,7 +467,10 @@ async def _async_get_trigger_platform(
|
|||||||
) -> tuple[str, TriggerProtocol]:
|
) -> tuple[str, TriggerProtocol]:
|
||||||
platform_and_sub_type = trigger_key.split(".")
|
platform_and_sub_type = trigger_key.split(".")
|
||||||
platform = platform_and_sub_type[0]
|
platform = platform_and_sub_type[0]
|
||||||
platform = _PLATFORM_ALIASES.get(platform, platform)
|
# Only apply aliases if there's no sub-type specified
|
||||||
|
# This allows "event" → "homeassistant" but "event.detected" → "event"
|
||||||
|
if len(platform_and_sub_type) == 1:
|
||||||
|
platform = _PLATFORM_ALIASES.get(platform, platform)
|
||||||
try:
|
try:
|
||||||
integration = await async_get_integration(hass, platform)
|
integration = await async_get_integration(hass, platform)
|
||||||
except IntegrationNotFound:
|
except IntegrationNotFound:
|
||||||
@@ -806,6 +809,9 @@ async def async_get_all_descriptions(
|
|||||||
|
|
||||||
description = {"fields": yaml_description.get("fields", {})}
|
description = {"fields": yaml_description.get("fields", {})}
|
||||||
|
|
||||||
|
if (target := yaml_description.get("target")) is not None:
|
||||||
|
description["target"] = target
|
||||||
|
|
||||||
new_descriptions_cache[missing_trigger] = description
|
new_descriptions_cache[missing_trigger] = description
|
||||||
|
|
||||||
hass.data[TRIGGER_DESCRIPTION_CACHE] = new_descriptions_cache
|
hass.data[TRIGGER_DESCRIPTION_CACHE] = new_descriptions_cache
|
||||||
|
|||||||
275
tests/components/event/test_trigger.py
Normal file
275
tests/components/event/test_trigger.py
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
"""Test event trigger."""
|
||||||
|
|
||||||
|
from homeassistant.components import automation
|
||||||
|
from homeassistant.components.event import ATTR_EVENT_TYPE
|
||||||
|
from homeassistant.const import CONF_ENTITY_ID, STATE_UNAVAILABLE
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
|
async def test_event_detected_trigger(
|
||||||
|
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||||
|
) -> None:
|
||||||
|
"""Test that the event detected trigger fires when an event is detected."""
|
||||||
|
entity_id = "event.test_event"
|
||||||
|
await async_setup_component(hass, "event", {})
|
||||||
|
|
||||||
|
# Set initial state
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"triggers": {
|
||||||
|
"trigger": "event.detected",
|
||||||
|
"target": {CONF_ENTITY_ID: entity_id},
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"action": "test.automation",
|
||||||
|
"data": {
|
||||||
|
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trigger event
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
|
service_calls.clear()
|
||||||
|
|
||||||
|
# Trigger same event type again - should still trigger
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_event_detected_trigger_with_event_type_filter(
|
||||||
|
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||||
|
) -> None:
|
||||||
|
"""Test that the event detected trigger with event_type filter."""
|
||||||
|
entity_id = "event.test_event"
|
||||||
|
await async_setup_component(hass, "event", {})
|
||||||
|
|
||||||
|
# Set initial state
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"triggers": {
|
||||||
|
"trigger": "event.detected",
|
||||||
|
"target": {
|
||||||
|
CONF_ENTITY_ID: entity_id,
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"event_type": ["button_press", "button_hold"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"action": "test.automation",
|
||||||
|
"data": {
|
||||||
|
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trigger matching event type
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
|
service_calls.clear()
|
||||||
|
|
||||||
|
# Trigger different matching event type
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_hold"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
|
service_calls.clear()
|
||||||
|
|
||||||
|
# Trigger non-matching event type - should not trigger
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_release"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_event_detected_trigger_ignores_unavailable(
|
||||||
|
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||||
|
) -> None:
|
||||||
|
"""Test that the event detected trigger ignores unavailable states."""
|
||||||
|
entity_id = "event.test_event"
|
||||||
|
await async_setup_component(hass, "event", {})
|
||||||
|
|
||||||
|
# Set initial state
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"triggers": {
|
||||||
|
"trigger": "event.detected",
|
||||||
|
"target": {
|
||||||
|
CONF_ENTITY_ID: entity_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"action": "test.automation",
|
||||||
|
"data": {
|
||||||
|
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set to unavailable - should not trigger
|
||||||
|
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 0
|
||||||
|
|
||||||
|
# Trigger event after unavailable - should trigger
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_event_detected_trigger_sequential_same_event_type(
|
||||||
|
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||||
|
) -> None:
|
||||||
|
"""Test that the event detected trigger fires for sequential events of the same type."""
|
||||||
|
entity_id = "event.test_event"
|
||||||
|
await async_setup_component(hass, "event", {})
|
||||||
|
|
||||||
|
# Set initial state
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"triggers": {
|
||||||
|
"trigger": "event.detected",
|
||||||
|
"target": {CONF_ENTITY_ID: entity_id},
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"action": "test.automation",
|
||||||
|
"data": {CONF_ENTITY_ID: entity_id},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trigger same event type multiple times in a row
|
||||||
|
for _ in range(3):
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Should have triggered 3 times
|
||||||
|
assert len(service_calls) == 3
|
||||||
|
for service_call in service_calls:
|
||||||
|
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||||
|
|
||||||
|
|
||||||
|
async def test_event_detected_trigger_from_unknown_state(
|
||||||
|
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||||
|
) -> None:
|
||||||
|
"""Test that the trigger fires when entity goes from unknown/None to first event.
|
||||||
|
|
||||||
|
Event entities restore their state, so on first creation they have no state.
|
||||||
|
"""
|
||||||
|
entity_id = "event.test_event"
|
||||||
|
await async_setup_component(hass, "event", {})
|
||||||
|
|
||||||
|
# Do NOT set any initial state - entity starts with None state
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"triggers": {
|
||||||
|
"trigger": "event.detected",
|
||||||
|
"target": {CONF_ENTITY_ID: entity_id},
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"action": "test.automation",
|
||||||
|
"data": {
|
||||||
|
CONF_ENTITY_ID: "{{ trigger.entity_id }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# First event should trigger even though entity had no previous state
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
dt_util.utcnow().isoformat(timespec="milliseconds"),
|
||||||
|
{ATTR_EVENT_TYPE: "button_press"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
@@ -33,7 +33,10 @@ async def test_if_fires_on_event(
|
|||||||
automation.DOMAIN,
|
automation.DOMAIN,
|
||||||
{
|
{
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "test_event",
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"service": "test.automation",
|
"service": "test.automation",
|
||||||
"data_template": {"id": "{{ trigger.id}}"},
|
"data_template": {"id": "{{ trigger.id}}"},
|
||||||
@@ -73,7 +76,10 @@ async def test_if_fires_on_templated_event(
|
|||||||
{
|
{
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
"trigger_variables": {"event_type": "test_event"},
|
"trigger_variables": {"event_type": "test_event"},
|
||||||
"trigger": {"platform": "event", "event_type": "{{event_type}}"},
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "{{event_type}}",
|
||||||
|
},
|
||||||
"action": {"service": "test.automation"},
|
"action": {"service": "test.automation"},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -135,7 +141,10 @@ async def test_if_fires_on_event_extra_data(
|
|||||||
automation.DOMAIN,
|
automation.DOMAIN,
|
||||||
{
|
{
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
"trigger": {"platform": "event", "event_type": "test_event"},
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "test_event",
|
||||||
|
},
|
||||||
"action": {"service": "test.automation"},
|
"action": {"service": "test.automation"},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -580,7 +589,10 @@ async def test_state_reported_event(
|
|||||||
automation.DOMAIN,
|
automation.DOMAIN,
|
||||||
{
|
{
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
"trigger": {"platform": "event", "event_type": event_type},
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": event_type,
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"service": "test.automation",
|
"service": "test.automation",
|
||||||
"data_template": {"id": "{{ trigger.id}}"},
|
"data_template": {"id": "{{ trigger.id}}"},
|
||||||
@@ -613,7 +625,10 @@ async def test_templated_state_reported_event(
|
|||||||
{
|
{
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
"trigger_variables": {"event_type": "state_reported"},
|
"trigger_variables": {"event_type": "state_reported"},
|
||||||
"trigger": {"platform": "event", "event_type": "{{event_type}}"},
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "{{event_type}}",
|
||||||
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"service": "test.automation",
|
"service": "test.automation",
|
||||||
"data_template": {"id": "{{ trigger.id}}"},
|
"data_template": {"id": "{{ trigger.id}}"},
|
||||||
|
|||||||
203
tests/components/light/test_condition.py
Normal file
203
tests/components/light/test_condition.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
"""Test light conditions."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components import automation
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_LABEL_ID,
|
||||||
|
CONF_CONDITION,
|
||||||
|
CONF_OPTIONS,
|
||||||
|
CONF_TARGET,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.helpers import entity_registry as er, label_registry as lr
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||||
|
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||||
|
"""Stub copying the blueprints to the config folder."""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def label_entities(hass: HomeAssistant) -> list[str]:
|
||||||
|
"""Create multiple entities associated with labels."""
|
||||||
|
await async_setup_component(hass, "light", {})
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(domain="test_labels")
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
label_reg = lr.async_get(hass)
|
||||||
|
label = label_reg.async_create("Test Label")
|
||||||
|
|
||||||
|
entity_reg = er.async_get(hass)
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
light_entity = entity_reg.async_get_or_create(
|
||||||
|
domain="light",
|
||||||
|
platform="test",
|
||||||
|
unique_id=f"label_light_{i}",
|
||||||
|
suggested_object_id=f"label_light_{i}",
|
||||||
|
)
|
||||||
|
entity_reg.async_update_entity(light_entity.entity_id, labels={label.label_id})
|
||||||
|
|
||||||
|
# Also create switches to test that they don't impact the conditions
|
||||||
|
for i in range(2):
|
||||||
|
switch_entity = entity_reg.async_get_or_create(
|
||||||
|
domain="switch",
|
||||||
|
platform="test",
|
||||||
|
unique_id=f"label_switch_{i}",
|
||||||
|
suggested_object_id=f"label_switch_{i}",
|
||||||
|
)
|
||||||
|
entity_reg.async_update_entity(switch_entity.entity_id, labels={label.label_id})
|
||||||
|
|
||||||
|
return [
|
||||||
|
"light.label_light_0",
|
||||||
|
"light.label_light_1",
|
||||||
|
"light.label_light_2",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_automation_with_light_condition(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
*,
|
||||||
|
condition: str,
|
||||||
|
target: dict,
|
||||||
|
behavior: str,
|
||||||
|
) -> None:
|
||||||
|
"""Set up automation with light state condition."""
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {"platform": "event", "event_type": "test_event"},
|
||||||
|
"condition": {
|
||||||
|
CONF_CONDITION: condition,
|
||||||
|
CONF_TARGET: target,
|
||||||
|
CONF_OPTIONS: {"behavior": behavior},
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def has_calls_after_trigger(
|
||||||
|
hass: HomeAssistant, service_calls: list[ServiceCall]
|
||||||
|
) -> bool:
|
||||||
|
"""Check if there are service calls after the trigger event."""
|
||||||
|
hass.bus.async_fire("test_event")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
has_calls = len(service_calls) == 1
|
||||||
|
service_calls.clear()
|
||||||
|
return has_calls
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("condition", "state", "reverse_state"),
|
||||||
|
[("light.is_on", STATE_ON, STATE_OFF), ("light.is_off", STATE_OFF, STATE_ON)],
|
||||||
|
)
|
||||||
|
async def test_light_state_condition_behavior_any(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
service_calls: list[ServiceCall],
|
||||||
|
label_entities: list[str],
|
||||||
|
condition: str,
|
||||||
|
state: str,
|
||||||
|
reverse_state: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test the light state condition with the 'any' behavior."""
|
||||||
|
await async_setup_component(hass, "light", {})
|
||||||
|
|
||||||
|
for entity_id in label_entities:
|
||||||
|
hass.states.async_set(entity_id, reverse_state)
|
||||||
|
|
||||||
|
await setup_automation_with_light_condition(
|
||||||
|
hass,
|
||||||
|
condition=condition,
|
||||||
|
target={ATTR_LABEL_ID: "test_label", "entity_id": "light.nonexistent"},
|
||||||
|
behavior="any",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set state for two switches to ensure that they don't impact the condition
|
||||||
|
hass.states.async_set("switch.label_switch_1", STATE_OFF)
|
||||||
|
hass.states.async_set("switch.label_switch_2", STATE_ON)
|
||||||
|
|
||||||
|
# No lights on the condition state
|
||||||
|
assert not await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
# Set one light to the condition state -> condition pass
|
||||||
|
hass.states.async_set(label_entities[0], state)
|
||||||
|
assert await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
# Set all lights to the condition state -> condition pass
|
||||||
|
for entity_id in label_entities:
|
||||||
|
hass.states.async_set(entity_id, state)
|
||||||
|
assert await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
# Set one light to unavailable -> condition pass
|
||||||
|
hass.states.async_set(label_entities[0], STATE_UNAVAILABLE)
|
||||||
|
assert await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
# Set all lights to unavailable -> condition fail
|
||||||
|
for entity_id in label_entities:
|
||||||
|
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||||
|
assert not await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("condition", "state", "reverse_state"),
|
||||||
|
[("light.is_on", STATE_ON, STATE_OFF), ("light.is_off", STATE_OFF, STATE_ON)],
|
||||||
|
)
|
||||||
|
async def test_light_state_condition_behavior_all(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
service_calls: list[ServiceCall],
|
||||||
|
label_entities: list[str],
|
||||||
|
condition: str,
|
||||||
|
state: str,
|
||||||
|
reverse_state: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test the light state condition with the 'all' behavior."""
|
||||||
|
await async_setup_component(hass, "light", {})
|
||||||
|
|
||||||
|
# Set state for two switches to ensure that they don't impact the condition
|
||||||
|
hass.states.async_set("switch.label_switch_1", STATE_OFF)
|
||||||
|
hass.states.async_set("switch.label_switch_2", STATE_ON)
|
||||||
|
|
||||||
|
for entity_id in label_entities:
|
||||||
|
hass.states.async_set(entity_id, reverse_state)
|
||||||
|
|
||||||
|
await setup_automation_with_light_condition(
|
||||||
|
hass,
|
||||||
|
condition=condition,
|
||||||
|
target={ATTR_LABEL_ID: "test_label", "entity_id": "light.nonexistent"},
|
||||||
|
behavior="all",
|
||||||
|
)
|
||||||
|
|
||||||
|
# No lights on the condition state
|
||||||
|
assert not await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
# Set one light to the condition state -> condition fail
|
||||||
|
hass.states.async_set(label_entities[0], state)
|
||||||
|
assert not await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
# Set all lights to the condition state -> condition pass
|
||||||
|
for entity_id in label_entities:
|
||||||
|
hass.states.async_set(entity_id, state)
|
||||||
|
assert await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
# Set one light to unavailable -> condition still pass
|
||||||
|
hass.states.async_set(label_entities[0], STATE_UNAVAILABLE)
|
||||||
|
assert await has_calls_after_trigger(hass, service_calls)
|
||||||
|
|
||||||
|
# Set all lights to unavailable -> condition passes
|
||||||
|
for entity_id in label_entities:
|
||||||
|
hass.states.async_set(entity_id, STATE_UNAVAILABLE)
|
||||||
|
assert await has_calls_after_trigger(hass, service_calls)
|
||||||
297
tests/components/light/test_trigger.py
Normal file
297
tests/components/light/test_trigger.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
"""Test light trigger."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components import automation
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_AREA_ID,
|
||||||
|
ATTR_DEVICE_ID,
|
||||||
|
ATTR_FLOOR_ID,
|
||||||
|
ATTR_LABEL_ID,
|
||||||
|
CONF_ENTITY_ID,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_TARGET,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.helpers import (
|
||||||
|
area_registry as ar,
|
||||||
|
device_registry as dr,
|
||||||
|
entity_registry as er,
|
||||||
|
floor_registry as fr,
|
||||||
|
label_registry as lr,
|
||||||
|
)
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, mock_device_registry
|
||||||
|
|
||||||
|
# remove when #151314 is merged
|
||||||
|
CONF_OPTIONS = "options"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||||
|
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||||
|
"""Stub copying the blueprints to the config folder."""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def target_lights(hass: HomeAssistant) -> None:
|
||||||
|
"""Create multiple light entities associated with different targets."""
|
||||||
|
await async_setup_component(hass, "light", {})
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(domain="test")
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
floor_reg = fr.async_get(hass)
|
||||||
|
floor = floor_reg.async_create("Test Floor")
|
||||||
|
|
||||||
|
area_reg = ar.async_get(hass)
|
||||||
|
area = area_reg.async_create("Test Area", floor_id=floor.floor_id)
|
||||||
|
|
||||||
|
label_reg = lr.async_get(hass)
|
||||||
|
label = label_reg.async_create("Test Label")
|
||||||
|
|
||||||
|
device = dr.DeviceEntry(id="test_device", area_id=area.id, labels={label.label_id})
|
||||||
|
mock_device_registry(hass, {device.id: device})
|
||||||
|
|
||||||
|
entity_reg = er.async_get(hass)
|
||||||
|
# Light associated with area
|
||||||
|
light_area = entity_reg.async_get_or_create(
|
||||||
|
domain="light",
|
||||||
|
platform="test",
|
||||||
|
unique_id="light_area",
|
||||||
|
suggested_object_id="area_light",
|
||||||
|
)
|
||||||
|
entity_reg.async_update_entity(light_area.entity_id, area_id=area.id)
|
||||||
|
|
||||||
|
# Light associated with device
|
||||||
|
entity_reg.async_get_or_create(
|
||||||
|
domain="light",
|
||||||
|
platform="test",
|
||||||
|
unique_id="light_device",
|
||||||
|
suggested_object_id="device_light",
|
||||||
|
device_id=device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Light associated with label
|
||||||
|
light_label = entity_reg.async_get_or_create(
|
||||||
|
domain="light",
|
||||||
|
platform="test",
|
||||||
|
unique_id="light_label",
|
||||||
|
suggested_object_id="label_light",
|
||||||
|
)
|
||||||
|
entity_reg.async_update_entity(light_label.entity_id, labels={label.label_id})
|
||||||
|
|
||||||
|
# Return all available light entities
|
||||||
|
return [
|
||||||
|
"light.standalone_light",
|
||||||
|
"light.label_light",
|
||||||
|
"light.area_light",
|
||||||
|
"light.device_light",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("target_lights")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("trigger_target_config", "entity_id"),
|
||||||
|
[
|
||||||
|
({CONF_ENTITY_ID: "light.standalone_light"}, "light.standalone_light"),
|
||||||
|
({ATTR_LABEL_ID: "test_label"}, "light.label_light"),
|
||||||
|
({ATTR_AREA_ID: "test_area"}, "light.area_light"),
|
||||||
|
({ATTR_FLOOR_ID: "test_floor"}, "light.area_light"),
|
||||||
|
({ATTR_LABEL_ID: "test_label"}, "light.device_light"),
|
||||||
|
({ATTR_AREA_ID: "test_area"}, "light.device_light"),
|
||||||
|
({ATTR_FLOOR_ID: "test_floor"}, "light.device_light"),
|
||||||
|
({ATTR_DEVICE_ID: "test_device"}, "light.device_light"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("trigger", "state", "reverse_state"),
|
||||||
|
[
|
||||||
|
("light.turned_on", STATE_ON, STATE_OFF),
|
||||||
|
("light.turned_off", STATE_OFF, STATE_ON),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_light_state_trigger_behavior_any(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
service_calls: list[ServiceCall],
|
||||||
|
trigger_target_config: dict,
|
||||||
|
entity_id: str,
|
||||||
|
trigger: str,
|
||||||
|
state: str,
|
||||||
|
reverse_state: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the light state trigger fires when any light state changes to a specific state."""
|
||||||
|
await async_setup_component(hass, "light", {})
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, reverse_state)
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {
|
||||||
|
CONF_PLATFORM: trigger,
|
||||||
|
CONF_TARGET: {**trigger_target_config},
|
||||||
|
CONF_OPTIONS: {},
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {CONF_ENTITY_ID: f"{entity_id}"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
|
service_calls.clear()
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, reverse_state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("trigger_target_config", "entity_id"),
|
||||||
|
[
|
||||||
|
({CONF_ENTITY_ID: "light.standalone_light"}, "light.standalone_light"),
|
||||||
|
({ATTR_LABEL_ID: "test_label"}, "light.label_light"),
|
||||||
|
({ATTR_AREA_ID: "test_area"}, "light.area_light"),
|
||||||
|
({ATTR_FLOOR_ID: "test_floor"}, "light.area_light"),
|
||||||
|
({ATTR_LABEL_ID: "test_label"}, "light.device_light"),
|
||||||
|
({ATTR_AREA_ID: "test_area"}, "light.device_light"),
|
||||||
|
({ATTR_FLOOR_ID: "test_floor"}, "light.device_light"),
|
||||||
|
({ATTR_DEVICE_ID: "test_device"}, "light.device_light"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("trigger", "state", "reverse_state"),
|
||||||
|
[
|
||||||
|
("light.turned_on", STATE_ON, STATE_OFF),
|
||||||
|
("light.turned_off", STATE_OFF, STATE_ON),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_light_state_trigger_behavior_first(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
service_calls: list[ServiceCall],
|
||||||
|
target_lights: list[str],
|
||||||
|
trigger_target_config: dict,
|
||||||
|
entity_id: str,
|
||||||
|
trigger: str,
|
||||||
|
state: str,
|
||||||
|
reverse_state: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the light state trigger fires when the first light changes to a specific state."""
|
||||||
|
await async_setup_component(hass, "light", {})
|
||||||
|
|
||||||
|
for other_entity_id in target_lights:
|
||||||
|
hass.states.async_set(other_entity_id, reverse_state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {
|
||||||
|
CONF_PLATFORM: trigger,
|
||||||
|
CONF_TARGET: {**trigger_target_config},
|
||||||
|
CONF_OPTIONS: {"behavior": "first"},
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {CONF_ENTITY_ID: f"{entity_id}"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
hass.states.async_set(entity_id, state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
|
service_calls.clear()
|
||||||
|
|
||||||
|
# Triggering other lights should not cause any service calls after the first one
|
||||||
|
for other_entity_id in target_lights:
|
||||||
|
hass.states.async_set(other_entity_id, state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
for other_entity_id in target_lights:
|
||||||
|
hass.states.async_set(other_entity_id, reverse_state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 0
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
|
assert service_calls[0].data[CONF_ENTITY_ID] == entity_id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("trigger_target_config", "entity_id"),
|
||||||
|
[
|
||||||
|
({CONF_ENTITY_ID: "light.standalone_light"}, "light.standalone_light"),
|
||||||
|
({ATTR_LABEL_ID: "test_label"}, "light.label_light"),
|
||||||
|
({ATTR_AREA_ID: "test_area"}, "light.area_light"),
|
||||||
|
({ATTR_FLOOR_ID: "test_floor"}, "light.area_light"),
|
||||||
|
({ATTR_LABEL_ID: "test_label"}, "light.device_light"),
|
||||||
|
({ATTR_AREA_ID: "test_area"}, "light.device_light"),
|
||||||
|
({ATTR_FLOOR_ID: "test_floor"}, "light.device_light"),
|
||||||
|
({ATTR_DEVICE_ID: "test_device"}, "light.device_light"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("trigger", "state", "reverse_state"),
|
||||||
|
[
|
||||||
|
("light.turned_on", STATE_ON, STATE_OFF),
|
||||||
|
("light.turned_off", STATE_OFF, STATE_ON),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_light_state_trigger_behavior_last(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
service_calls: list[ServiceCall],
|
||||||
|
target_lights: list[str],
|
||||||
|
trigger_target_config: dict,
|
||||||
|
entity_id: str,
|
||||||
|
trigger: str,
|
||||||
|
state: str,
|
||||||
|
reverse_state: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the light state trigger fires when the last light changes to a specific state."""
|
||||||
|
await async_setup_component(hass, "light", {})
|
||||||
|
|
||||||
|
for other_entity_id in target_lights:
|
||||||
|
hass.states.async_set(other_entity_id, reverse_state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {
|
||||||
|
CONF_PLATFORM: trigger,
|
||||||
|
CONF_TARGET: {**trigger_target_config},
|
||||||
|
CONF_OPTIONS: {"behavior": "last"},
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {CONF_ENTITY_ID: f"{entity_id}"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
target_lights.remove(entity_id)
|
||||||
|
for other_entity_id in target_lights:
|
||||||
|
hass.states.async_set(other_entity_id, state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 0
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, state)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(service_calls) == 1
|
||||||
@@ -450,10 +450,10 @@ async def test_caching(hass: HomeAssistant) -> None:
|
|||||||
side_effect=translation.build_resources,
|
side_effect=translation.build_resources,
|
||||||
) as mock_build_resources:
|
) as mock_build_resources:
|
||||||
load1 = await translation.async_get_translations(hass, "en", "entity_component")
|
load1 = await translation.async_get_translations(hass, "en", "entity_component")
|
||||||
assert len(mock_build_resources.mock_calls) == 7
|
assert len(mock_build_resources.mock_calls) == 9
|
||||||
|
|
||||||
load2 = await translation.async_get_translations(hass, "en", "entity_component")
|
load2 = await translation.async_get_translations(hass, "en", "entity_component")
|
||||||
assert len(mock_build_resources.mock_calls) == 7
|
assert len(mock_build_resources.mock_calls) == 9
|
||||||
|
|
||||||
assert load1 == load2
|
assert load1 == load2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user