mirror of
https://github.com/home-assistant/core.git
synced 2025-12-19 18:38:58 +00:00
Add trigger climate.target_temperature_changed (#159434)
This commit is contained in:
@@ -110,6 +110,9 @@
|
||||
"started_heating": {
|
||||
"trigger": "mdi:fire"
|
||||
},
|
||||
"target_temperature_changed": {
|
||||
"trigger": "mdi:thermometer"
|
||||
},
|
||||
"turned_off": {
|
||||
"trigger": "mdi:power-off"
|
||||
},
|
||||
|
||||
@@ -192,6 +192,12 @@
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
@@ -342,6 +348,20 @@
|
||||
},
|
||||
"name": "Climate-control device started heating"
|
||||
},
|
||||
"target_temperature_changed": {
|
||||
"description": "Triggers after the temperature setpoint of one or more climate-control devices changes.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Trigger when the target temperature is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Trigger when the target temperature is below this value.",
|
||||
"name": "Below"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target temperature changed"
|
||||
},
|
||||
"turned_off": {
|
||||
"description": "Triggers after one or more climate-control devices turn off.",
|
||||
"fields": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_OPTIONS
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.trigger import (
|
||||
@@ -10,6 +10,7 @@ from homeassistant.helpers.trigger import (
|
||||
EntityTargetStateTriggerBase,
|
||||
Trigger,
|
||||
TriggerConfig,
|
||||
make_entity_numerical_state_attribute_changed_trigger,
|
||||
make_entity_target_state_attribute_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
make_entity_transition_trigger,
|
||||
@@ -50,6 +51,9 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"started_drying": make_entity_target_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
|
||||
),
|
||||
"target_temperature_changed": make_entity_numerical_state_attribute_changed_trigger(
|
||||
DOMAIN, ATTR_TEMPERATURE
|
||||
),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
|
||||
"turned_on": make_entity_transition_trigger(
|
||||
DOMAIN,
|
||||
|
||||
@@ -14,6 +14,25 @@
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
translation_key: number_or_entity
|
||||
|
||||
started_cooling: *trigger_common
|
||||
started_drying: *trigger_common
|
||||
started_heating: *trigger_common
|
||||
@@ -34,3 +53,9 @@ hvac_mode_changed:
|
||||
- unavailable
|
||||
- unknown
|
||||
multiple: true
|
||||
|
||||
target_temperature_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
|
||||
@@ -16,7 +16,9 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ABOVE,
|
||||
CONF_ALIAS,
|
||||
CONF_BELOW,
|
||||
CONF_ENABLED,
|
||||
CONF_ID,
|
||||
CONF_OPTIONS,
|
||||
@@ -504,6 +506,139 @@ class EntityTargetStateAttributeTriggerBase(EntityTriggerBase):
|
||||
return state.attributes.get(self._attribute) == self._attribute_to_state
|
||||
|
||||
|
||||
def _validate_range[_T: dict[str, Any]](
|
||||
lower_limit: str, upper_limit: str
|
||||
) -> Callable[[_T], _T]:
|
||||
"""Generate range validator."""
|
||||
|
||||
def _validate_range(value: _T) -> _T:
|
||||
above = value.get(lower_limit)
|
||||
below = value.get(upper_limit)
|
||||
|
||||
if above is None or below is None:
|
||||
return value
|
||||
|
||||
if isinstance(above, str) or isinstance(below, str):
|
||||
return value
|
||||
|
||||
if above > below:
|
||||
raise vol.Invalid(
|
||||
(
|
||||
f"A value can never be above {above} and below {below} at the same"
|
||||
" time. You probably want two different triggers."
|
||||
),
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
return _validate_range
|
||||
|
||||
|
||||
_NUMBER_OR_ENTITY_CHOOSE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("chosen_selector"): vol.In(["number", "entity"]),
|
||||
vol.Optional("entity"): cv.entity_id,
|
||||
vol.Optional("number"): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_number_or_entity(value: dict | float | str) -> float | str:
|
||||
"""Validate number or entity selector result."""
|
||||
if isinstance(value, dict):
|
||||
_NUMBER_OR_ENTITY_CHOOSE_SCHEMA(value)
|
||||
return value[value["chosen_selector"]] # type: ignore[no-any-return]
|
||||
return value
|
||||
|
||||
|
||||
_number_or_entity = vol.All(
|
||||
_validate_number_or_entity, vol.Any(vol.Coerce(float), cv.entity_id)
|
||||
)
|
||||
|
||||
NUMERICAL_ATTRIBUTE_CHANGED_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): vol.All(
|
||||
{
|
||||
vol.Optional(CONF_ABOVE): _number_or_entity,
|
||||
vol.Optional(CONF_BELOW): _number_or_entity,
|
||||
},
|
||||
_validate_range(CONF_ABOVE, CONF_BELOW),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_numerical_value(
|
||||
hass: HomeAssistant, entity_or_float: float | str
|
||||
) -> float | None:
|
||||
"""Get numerical value from float or entity state."""
|
||||
if isinstance(entity_or_float, str):
|
||||
if not (state := hass.states.get(entity_or_float)):
|
||||
# Entity not found
|
||||
return None
|
||||
try:
|
||||
return float(state.state)
|
||||
except (TypeError, ValueError):
|
||||
# Entity state is not a valid number
|
||||
return None
|
||||
return entity_or_float
|
||||
|
||||
|
||||
class EntityNumericalStateAttributeChangedTriggerBase(EntityTriggerBase):
|
||||
"""Trigger for numerical state attribute changes."""
|
||||
|
||||
_attribute: str
|
||||
_schema = NUMERICAL_ATTRIBUTE_CHANGED_TRIGGER_SCHEMA
|
||||
|
||||
_above: None | float | str
|
||||
_below: None | float | str
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the state trigger."""
|
||||
super().__init__(hass, config)
|
||||
self._above = self._options.get(CONF_ABOVE)
|
||||
self._below = self._options.get(CONF_BELOW)
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and the state has changed."""
|
||||
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
return False
|
||||
|
||||
return from_state.attributes.get(self._attribute) != to_state.attributes.get(
|
||||
self._attribute
|
||||
)
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state attribute matches the expected one."""
|
||||
# Handle missing or None attribute case first to avoid expensive exceptions
|
||||
if (_attribute_value := state.attributes.get(self._attribute)) is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
current_value = float(_attribute_value)
|
||||
except (TypeError, ValueError):
|
||||
# Attribute is not a valid number, don't trigger
|
||||
return False
|
||||
|
||||
if self._above is not None:
|
||||
if (above := _get_numerical_value(self._hass, self._above)) is None:
|
||||
# Entity not found or invalid number, don't trigger
|
||||
return False
|
||||
if current_value <= above:
|
||||
# The number is not above the limit, don't trigger
|
||||
return False
|
||||
|
||||
if self._below is not None:
|
||||
if (below := _get_numerical_value(self._hass, self._below)) is None:
|
||||
# Entity not found or invalid number, don't trigger
|
||||
return False
|
||||
if current_value >= below:
|
||||
# The number is not below the limit, don't trigger
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def make_entity_target_state_trigger(
|
||||
domain: str, to_states: str | set[str]
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
@@ -552,6 +687,20 @@ def make_entity_origin_state_trigger(
|
||||
return CustomTrigger
|
||||
|
||||
|
||||
def make_entity_numerical_state_attribute_changed_trigger(
|
||||
domain: str, attribute: str
|
||||
) -> type[EntityNumericalStateAttributeChangedTriggerBase]:
|
||||
"""Create a trigger for numerical state attribute change."""
|
||||
|
||||
class CustomTrigger(EntityNumericalStateAttributeChangedTriggerBase):
|
||||
"""Trigger for numerical state attribute changes."""
|
||||
|
||||
_domain = domain
|
||||
_attribute = attribute
|
||||
|
||||
return CustomTrigger
|
||||
|
||||
|
||||
def make_entity_target_state_attribute_trigger(
|
||||
domain: str, attribute: str, to_state: str
|
||||
) -> type[EntityTargetStateAttributeTriggerBase]:
|
||||
|
||||
@@ -171,6 +171,7 @@ def parametrize_trigger_states(
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
additional_attributes: dict | None = None,
|
||||
trigger_from_none: bool = True,
|
||||
retrigger_on_target_state: bool = False,
|
||||
) -> list[tuple[str, list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts.
|
||||
|
||||
@@ -180,6 +181,9 @@ def parametrize_trigger_states(
|
||||
Set `trigger_from_none` to False if the trigger is not expected to fire
|
||||
when the initial state is None.
|
||||
|
||||
Set `retrigger_on_target_state` to True if the trigger is expected to fire
|
||||
when the state changes to another target state.
|
||||
|
||||
Returns a list of tuples with (trigger, list of states),
|
||||
where states is a list of StateDescription dicts.
|
||||
"""
|
||||
@@ -214,7 +218,7 @@ def parametrize_trigger_states(
|
||||
"count": count,
|
||||
}
|
||||
|
||||
return [
|
||||
tests = [
|
||||
# Initial state None
|
||||
(
|
||||
trigger,
|
||||
@@ -260,6 +264,9 @@ def parametrize_trigger_states(
|
||||
state_with_attributes(target_state, 0),
|
||||
state_with_attributes(other_state, 0),
|
||||
state_with_attributes(target_state, 1),
|
||||
# Repeat target state to test retriggering
|
||||
state_with_attributes(target_state, 0),
|
||||
state_with_attributes(STATE_UNAVAILABLE, 0),
|
||||
)
|
||||
for target_state in target_states
|
||||
for other_state in other_states
|
||||
@@ -299,6 +306,34 @@ def parametrize_trigger_states(
|
||||
),
|
||||
]
|
||||
|
||||
if len(target_states) > 1:
|
||||
# If more than one target state, test state change between target states
|
||||
tests.append(
|
||||
(
|
||||
trigger,
|
||||
list(
|
||||
itertools.chain.from_iterable(
|
||||
(
|
||||
state_with_attributes(target_states[idx - 1], 0),
|
||||
state_with_attributes(
|
||||
target_state, 1 if retrigger_on_target_state else 0
|
||||
),
|
||||
state_with_attributes(other_state, 0),
|
||||
state_with_attributes(target_states[idx - 1], 1),
|
||||
state_with_attributes(
|
||||
target_state, 1 if retrigger_on_target_state else 0
|
||||
),
|
||||
state_with_attributes(STATE_UNAVAILABLE, 0),
|
||||
)
|
||||
for idx, target_state in enumerate(target_states[1:], start=1)
|
||||
for other_state in other_states
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return tests
|
||||
|
||||
|
||||
async def arm_trigger(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -14,7 +14,15 @@ from homeassistant.components.climate.const import (
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.climate.trigger import CONF_HVAC_MODE
|
||||
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID, CONF_OPTIONS, CONF_TARGET
|
||||
from homeassistant.const import (
|
||||
ATTR_LABEL_ID,
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers.trigger import async_validate_trigger_config
|
||||
|
||||
@@ -54,6 +62,7 @@ async def target_climates(hass: HomeAssistant) -> list[str]:
|
||||
"trigger_key",
|
||||
[
|
||||
"climate.hvac_mode_changed",
|
||||
"climate.target_temperature_changed",
|
||||
"climate.turned_off",
|
||||
"climate.turned_on",
|
||||
"climate.started_heating",
|
||||
@@ -136,6 +145,7 @@ def parametrize_climate_trigger_states(
|
||||
other_states: list[str | None | tuple[str | None, dict]],
|
||||
additional_attributes: dict | None = None,
|
||||
trigger_from_none: bool = True,
|
||||
retrigger_on_target_state: bool = False,
|
||||
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
|
||||
"""Parametrize states and expected service call counts."""
|
||||
trigger_options = trigger_options or {}
|
||||
@@ -147,6 +157,7 @@ def parametrize_climate_trigger_states(
|
||||
other_states=other_states,
|
||||
additional_attributes=additional_attributes,
|
||||
trigger_from_none=trigger_from_none,
|
||||
retrigger_on_target_state=retrigger_on_target_state,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -230,19 +241,56 @@ async def test_climate_state_trigger_behavior_any(
|
||||
parametrize_target_entities("climate"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
*parametrize_climate_trigger_states(
|
||||
trigger="climate.target_temperature_changed",
|
||||
trigger_options={},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 0}),
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 50}),
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 100}),
|
||||
],
|
||||
other_states=[(HVACMode.AUTO, {ATTR_TEMPERATURE: None})],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_climate_trigger_states(
|
||||
trigger="climate.target_temperature_changed",
|
||||
trigger_options={CONF_ABOVE: 10},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 50}),
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 100}),
|
||||
],
|
||||
other_states=[
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: None}),
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 0}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_climate_trigger_states(
|
||||
trigger="climate.target_temperature_changed",
|
||||
trigger_options={CONF_BELOW: 90},
|
||||
target_states=[
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 0}),
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 50}),
|
||||
],
|
||||
other_states=[
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: None}),
|
||||
(HVACMode.AUTO, {ATTR_TEMPERATURE: 100}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_climate_trigger_states(
|
||||
trigger="climate.started_cooling",
|
||||
target_states=[(HVACMode.AUTO, {ATTR_HVAC_ACTION: HVACAction.COOLING})],
|
||||
other_states=[(HVACMode.AUTO, {ATTR_HVAC_ACTION: HVACAction.IDLE})],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
*parametrize_climate_trigger_states(
|
||||
trigger="climate.started_drying",
|
||||
target_states=[(HVACMode.AUTO, {ATTR_HVAC_ACTION: HVACAction.DRYING})],
|
||||
other_states=[(HVACMode.AUTO, {ATTR_HVAC_ACTION: HVACAction.IDLE})],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
*parametrize_climate_trigger_states(
|
||||
trigger="climate.started_heating",
|
||||
target_states=[(HVACMode.AUTO, {ATTR_HVAC_ACTION: HVACAction.HEATING})],
|
||||
other_states=[(HVACMode.AUTO, {ATTR_HVAC_ACTION: HVACAction.IDLE})],
|
||||
@@ -257,6 +305,7 @@ async def test_climate_state_attribute_trigger_behavior_any(
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
trigger_options: dict[str, Any],
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the climate state trigger fires when any climate state changes to a specific state."""
|
||||
@@ -267,7 +316,7 @@ async def test_climate_state_attribute_trigger_behavior_any(
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {}, trigger_target_config)
|
||||
await arm_trigger(hass, trigger, trigger_options, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""The tests for the trigger helper."""
|
||||
|
||||
from contextlib import AbstractContextManager, nullcontext as does_not_raise
|
||||
import io
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, call, patch
|
||||
|
||||
import pytest
|
||||
@@ -11,6 +13,13 @@ from homeassistant.components.sun import DOMAIN as DOMAIN_SUN
|
||||
from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH
|
||||
from homeassistant.components.tag import DOMAIN as DOMAIN_TAG
|
||||
from homeassistant.components.text import DOMAIN as DOMAIN_TEXT
|
||||
from homeassistant.const import (
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Context,
|
||||
@@ -29,6 +38,7 @@ from homeassistant.helpers.trigger import (
|
||||
_async_get_trigger_platform,
|
||||
async_initialize_triggers,
|
||||
async_validate_trigger_config,
|
||||
make_entity_numerical_state_attribute_changed_trigger,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import Integration, async_get_integration
|
||||
@@ -1131,3 +1141,109 @@ async def test_subscribe_triggers_no_triggers(
|
||||
assert await async_setup_component(hass, "light", {})
|
||||
await hass.async_block_till_done()
|
||||
assert trigger_events == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_options", "expected_result"),
|
||||
[
|
||||
# Test validating climate.target_temperature_changed
|
||||
# Valid configurations
|
||||
(
|
||||
{},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_ABOVE: 10},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_ABOVE: "sensor.test"},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_BELOW: 90},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_BELOW: "sensor.test"},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_ABOVE: 10, CONF_BELOW: 90},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_ABOVE: "sensor.test", CONF_BELOW: 90},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_ABOVE: 10, CONF_BELOW: "sensor.test"},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_ABOVE: "sensor.test", CONF_BELOW: "sensor.test"},
|
||||
does_not_raise(),
|
||||
),
|
||||
# Test verbose choose selector options
|
||||
(
|
||||
{CONF_ABOVE: {"chosen_selector": "entity", "entity": "sensor.test"}},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_ABOVE: {"chosen_selector": "number", "number": 10}},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_BELOW: {"chosen_selector": "entity", "entity": "sensor.test"}},
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
{CONF_BELOW: {"chosen_selector": "number", "number": 90}},
|
||||
does_not_raise(),
|
||||
),
|
||||
# Test invalid configurations
|
||||
(
|
||||
# Must be valid entity id
|
||||
{CONF_ABOVE: "cat", CONF_BELOW: "dog"},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Above must be smaller than below
|
||||
{CONF_ABOVE: 90, CONF_BELOW: 10},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
(
|
||||
# Invalid choose selector option
|
||||
{CONF_BELOW: {"chosen_selector": "cat", "cat": 90}},
|
||||
pytest.raises(vol.Invalid),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_numerical_state_attribute_changed_trigger_config_validation(
|
||||
hass: HomeAssistant,
|
||||
trigger_options: dict[str, Any],
|
||||
expected_result: AbstractContextManager,
|
||||
) -> None:
|
||||
"""Test numerical state attribute change trigger config validation."""
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
return {
|
||||
"test_trigger": make_entity_numerical_state_attribute_changed_trigger(
|
||||
"test", "test_attribute"
|
||||
),
|
||||
}
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", Mock(async_get_triggers=async_get_triggers))
|
||||
|
||||
with expected_result:
|
||||
await async_validate_trigger_config(
|
||||
hass,
|
||||
[
|
||||
{
|
||||
"platform": "test.test_trigger",
|
||||
CONF_TARGET: {CONF_ENTITY_ID: "test.test_entity"},
|
||||
CONF_OPTIONS: trigger_options,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user