Compare commits

...

3 Commits

Author SHA1 Message Date
Erik
0b9c864d4e Update test 2025-12-19 22:53:19 +01:00
Erik
6186cdce5c Improve test 2025-12-19 22:08:05 +01:00
Erik
45b8363a19 Add light brightness triggers 2025-12-19 21:58:43 +01:00
6 changed files with 440 additions and 5 deletions

View File

@@ -35,6 +35,12 @@
}
},
"triggers": {
"brightness_changed": {
"trigger": "mdi:lightbulb-on-50"
},
"brightness_crossed_threshold": {
"trigger": "mdi:lightbulb-on-50"
},
"turned_off": {
"trigger": "mdi:lightbulb-off"
},

View File

@@ -322,6 +322,12 @@
"short": "Short"
}
},
"number_or_entity": {
"choices": {
"entity": "Entity",
"number": "Number"
}
},
"state": {
"options": {
"off": "[%key:common::state::off%]",
@@ -334,6 +340,14 @@
"first": "First",
"last": "Last"
}
},
"trigger_threshold_type": {
"options": {
"above": "Above a value",
"below": "Below a value",
"between": "In a range",
"outside": "Outside a range"
}
}
},
"services": {
@@ -509,6 +523,42 @@
},
"title": "Light",
"triggers": {
"brightness_changed": {
"description": "Triggers after the brightness of one or more lights changes.",
"fields": {
"above": {
"description": "Trigger when the target brightness is above this value.",
"name": "Above"
},
"below": {
"description": "Trigger when the target brightness is below this value.",
"name": "Below"
}
},
"name": "Light brightness changed"
},
"brightness_crossed_threshold": {
"description": "Triggers after the brightness of one or more lights crosses a threshold.",
"fields": {
"behavior": {
"description": "[%key:component::light::common::trigger_behavior_description%]",
"name": "[%key:component::light::common::trigger_behavior_name%]"
},
"lower_limit": {
"description": "Lower threshold limit.",
"name": "Lower threshold"
},
"threshold_type": {
"description": "Type of threshold crossing to trigger on.",
"name": "Threshold type"
},
"upper_limit": {
"description": "Upper threshold limit.",
"name": "Upper threshold"
}
},
"name": "Light brightness crossed threshold"
},
"turned_off": {
"description": "Triggers after one or more lights turn off.",
"fields": {

View File

@@ -2,11 +2,23 @@
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
from homeassistant.helpers.trigger import (
Trigger,
make_entity_numerical_state_attribute_changed_trigger,
make_entity_numerical_state_attribute_crossed_threshold_trigger,
make_entity_target_state_trigger,
)
from . import ATTR_BRIGHTNESS
from .const import DOMAIN
TRIGGERS: dict[str, type[Trigger]] = {
"brightness_changed": make_entity_numerical_state_attribute_changed_trigger(
DOMAIN, ATTR_BRIGHTNESS
),
"brightness_crossed_threshold": make_entity_numerical_state_attribute_crossed_threshold_trigger(
DOMAIN, ATTR_BRIGHTNESS
),
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
}

View File

@@ -1,9 +1,9 @@
.trigger_common: &trigger_common
target:
target: &trigger_light_target
entity:
domain: light
fields:
behavior:
behavior: &trigger_behavior
required: true
default: any
selector:
@@ -14,5 +14,47 @@
- any
translation_key: trigger_behavior
.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
turned_on: *trigger_common
turned_off: *trigger_common
brightness_changed:
target: *trigger_light_target
fields:
above: *number_or_entity
below: *number_or_entity
brightness_crossed_threshold:
target: *trigger_light_target
fields:
behavior: *trigger_behavior
threshold_type:
required: true
selector:
select:
options:
- above
- below
- between
- outside
translation_key: trigger_threshold_type
lower_limit: *number_or_entity
upper_limit: *number_or_entity

View File

@@ -1,12 +1,27 @@
"""Test light trigger."""
from collections.abc import Generator
from typing import Any
from unittest.mock import patch
import pytest
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import (
ATTR_LABEL_ID,
CONF_ABOVE,
CONF_BELOW,
CONF_ENTITY_ID,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.trigger import (
CONF_LOWER_LIMIT,
CONF_THRESHOLD_TYPE,
CONF_UPPER_LIMIT,
ThresholdType,
)
from tests.components import (
StateDescription,
@@ -42,6 +57,8 @@ async def target_lights(hass: HomeAssistant) -> list[str]:
@pytest.mark.parametrize(
"trigger_key",
[
"light.brightness_changed",
"light.brightness_crossed_threshold",
"light.turned_off",
"light.turned_on",
],
@@ -59,6 +76,147 @@ async def test_light_triggers_gated_by_labs_flag(
) in caplog.text
def parametrize_light_trigger_states(
*,
trigger: str,
trigger_options: dict | None = None,
target_states: list[str | None | tuple[str | None, dict]],
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 {}
return [
(s[0], trigger_options, *s[1:])
for s in parametrize_trigger_states(
trigger=trigger,
target_states=target_states,
other_states=other_states,
additional_attributes=additional_attributes,
trigger_from_none=trigger_from_none,
retrigger_on_target_state=retrigger_on_target_state,
)
]
def parametrize_xxx_changed_trigger_states(
trigger: str, attribute: str
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
"""Parametrize states and expected service call counts for xxx_changed triggers."""
return [
*parametrize_light_trigger_states(
trigger=trigger,
target_states=[
(STATE_ON, {attribute: 0}),
(STATE_ON, {attribute: 50}),
(STATE_ON, {attribute: 100}),
],
other_states=[(STATE_ON, {attribute: None})],
retrigger_on_target_state=True,
),
*parametrize_light_trigger_states(
trigger=trigger,
trigger_options={CONF_ABOVE: 10},
target_states=[
(STATE_ON, {attribute: 50}),
(STATE_ON, {attribute: 100}),
],
other_states=[
(STATE_ON, {attribute: None}),
(STATE_ON, {attribute: 0}),
],
retrigger_on_target_state=True,
),
*parametrize_light_trigger_states(
trigger=trigger,
trigger_options={CONF_BELOW: 90},
target_states=[
(STATE_ON, {attribute: 0}),
(STATE_ON, {attribute: 50}),
],
other_states=[
(STATE_ON, {attribute: None}),
(STATE_ON, {attribute: 100}),
],
retrigger_on_target_state=True,
),
]
def parametrize_xxx_crossed_threshold_trigger_states(
trigger: str, attribute: str
) -> list[tuple[str, dict[str, Any], list[StateDescription]]]:
"""Parametrize states and expected service call counts for xxx_crossed_threshold triggers."""
return [
*parametrize_light_trigger_states(
trigger=trigger,
trigger_options={
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
CONF_LOWER_LIMIT: 10,
CONF_UPPER_LIMIT: 90,
},
target_states=[
(STATE_ON, {attribute: 50}),
(STATE_ON, {attribute: 60}),
],
other_states=[
(STATE_ON, {attribute: None}),
(STATE_ON, {attribute: 0}),
(STATE_ON, {attribute: 100}),
],
),
*parametrize_light_trigger_states(
trigger=trigger,
trigger_options={
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
CONF_LOWER_LIMIT: 10,
CONF_UPPER_LIMIT: 90,
},
target_states=[
(STATE_ON, {attribute: 0}),
(STATE_ON, {attribute: 100}),
],
other_states=[
(STATE_ON, {attribute: None}),
(STATE_ON, {attribute: 50}),
(STATE_ON, {attribute: 60}),
],
),
*parametrize_light_trigger_states(
trigger=trigger,
trigger_options={
CONF_THRESHOLD_TYPE: ThresholdType.ABOVE,
CONF_LOWER_LIMIT: 10,
},
target_states=[
(STATE_ON, {attribute: 50}),
(STATE_ON, {attribute: 100}),
],
other_states=[
(STATE_ON, {attribute: None}),
(STATE_ON, {attribute: 0}),
],
),
*parametrize_light_trigger_states(
trigger=trigger,
trigger_options={
CONF_THRESHOLD_TYPE: ThresholdType.BELOW,
CONF_UPPER_LIMIT: 90,
},
target_states=[
(STATE_ON, {attribute: 0}),
(STATE_ON, {attribute: 50}),
],
other_states=[
(STATE_ON, {attribute: None}),
(STATE_ON, {attribute: 100}),
],
),
]
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
@@ -116,6 +274,60 @@ async def test_light_state_trigger_behavior_any(
service_calls.clear()
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("light"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_xxx_changed_trigger_states(
"light.brightness_changed", ATTR_BRIGHTNESS
),
*parametrize_xxx_crossed_threshold_trigger_states(
"light.brightness_crossed_threshold", ATTR_BRIGHTNESS
),
],
)
async def test_light_state_attribute_trigger_behavior_any(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_lights: list[str],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[StateDescription],
) -> None:
"""Test that the light state trigger fires when any light state changes to a specific state."""
other_entity_ids = set(target_lights) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(hass, trigger, trigger_options, trigger_target_config)
for state in states[1:]:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# Check if changing other lights also triggers
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == (entities_in_target - 1) * state["count"]
service_calls.clear()
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
@@ -172,6 +384,58 @@ async def test_light_state_trigger_behavior_first(
assert len(service_calls) == 0
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("light"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_xxx_crossed_threshold_trigger_states(
"light.brightness_crossed_threshold", ATTR_BRIGHTNESS
),
],
)
async def test_light_state_attribute_trigger_behavior_first(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_lights: list[str],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[tuple[tuple[str, dict], int]],
) -> None:
"""Test that the light state trigger fires when the first light state changes to a specific state."""
other_entity_ids = set(target_lights) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(
hass, trigger, {"behavior": "first"} | trigger_options, trigger_target_config
)
for state in states[1:]:
included_state = state["included"]
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
# Triggering other lights should not cause the trigger to fire again
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
@@ -225,3 +489,54 @@ async def test_light_state_trigger_behavior_last(
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
@pytest.mark.parametrize(
("trigger_target_config", "entity_id", "entities_in_target"),
parametrize_target_entities("light"),
)
@pytest.mark.parametrize(
("trigger", "trigger_options", "states"),
[
*parametrize_xxx_crossed_threshold_trigger_states(
"light.brightness_crossed_threshold", ATTR_BRIGHTNESS
),
],
)
async def test_light_state_attribute_trigger_behavior_last(
hass: HomeAssistant,
service_calls: list[ServiceCall],
target_lights: list[str],
trigger_target_config: dict,
entity_id: str,
entities_in_target: int,
trigger: str,
trigger_options: dict[str, Any],
states: list[tuple[tuple[str, dict], int]],
) -> None:
"""Test that the light state trigger fires when the last light state changes to a specific state."""
other_entity_ids = set(target_lights) - {entity_id}
# Set all lights, including the tested light, to the initial state
for eid in target_lights:
set_or_remove_state(hass, eid, states[0]["included"])
await hass.async_block_till_done()
await arm_trigger(
hass, trigger, {"behavior": "last"} | trigger_options, trigger_target_config
)
for state in states[1:]:
included_state = state["included"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == 0
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
assert len(service_calls) == state["count"]
for service_call in service_calls:
assert service_call.data[CONF_ENTITY_ID] == entity_id
service_calls.clear()

View File

@@ -1038,7 +1038,17 @@ async def test_subscribe_triggers(
@pytest.mark.parametrize(
("new_triggers_conditions_enabled", "expected_events"),
[
(True, [{"light.turned_off", "light.turned_on"}]),
(
True,
[
{
"light.brightness_changed",
"light.brightness_crossed_threshold",
"light.turned_off",
"light.turned_on",
}
],
),
(False, []),
],
)