mirror of
https://github.com/home-assistant/core.git
synced 2026-01-20 06:26:56 +01:00
Compare commits
3 Commits
improve_li
...
multi_doma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
565aebd305 | ||
|
|
8a9ca9bd98 | ||
|
|
cb5daaf3fe |
@@ -176,6 +176,9 @@
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"_door.opened": {
|
||||
"trigger": "mdi:door-open"
|
||||
},
|
||||
"occupancy_cleared": {
|
||||
"trigger": "mdi:home-outline"
|
||||
},
|
||||
|
||||
@@ -332,6 +332,16 @@
|
||||
},
|
||||
"title": "Binary sensor",
|
||||
"triggers": {
|
||||
"_door.opened": {
|
||||
"description": "Triggers after one or more occupancy doors open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::binary_sensor::common::trigger_behavior_description_occupancy%]",
|
||||
"name": "[%key:component::binary_sensor::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Door opened"
|
||||
},
|
||||
"occupancy_cleared": {
|
||||
"description": "Triggers after one or more occupancy sensors stop detecting occupancy.",
|
||||
"fields": {
|
||||
|
||||
@@ -53,6 +53,7 @@ def make_binary_sensor_trigger(
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"_door.opened": make_binary_sensor_trigger(BinarySensorDeviceClass.DOOR, STATE_ON),
|
||||
"occupancy_detected": make_binary_sensor_trigger(
|
||||
BinarySensorDeviceClass.OCCUPANCY, STATE_ON
|
||||
),
|
||||
|
||||
@@ -23,3 +23,10 @@ occupancy_detected:
|
||||
entity:
|
||||
domain: binary_sensor
|
||||
device_class: occupancy
|
||||
|
||||
_door.opened:
|
||||
fields: *trigger_common_fields
|
||||
target:
|
||||
entity:
|
||||
domain: binary_sensor
|
||||
device_class: door
|
||||
|
||||
@@ -1,50 +1,24 @@
|
||||
"""Provides triggers for lights."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityNumericalStateAttributeChangedTriggerBase,
|
||||
EntityNumericalStateAttributeCrossedThresholdTriggerBase,
|
||||
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
|
||||
|
||||
|
||||
def _convert_uint8_to_percentage(value: Any) -> float:
|
||||
"""Convert a uint8 value (0-255) to a percentage (0-100)."""
|
||||
value_float = float(value)
|
||||
if value_float == 0:
|
||||
return 0.0
|
||||
return (value_float / 255.0) * 100.0
|
||||
|
||||
|
||||
class BrightnessChangedTrigger(EntityNumericalStateAttributeChangedTriggerBase):
|
||||
"""Trigger for brightness changed."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_attribute = ATTR_BRIGHTNESS
|
||||
|
||||
_convert_attribute = staticmethod(_convert_uint8_to_percentage)
|
||||
|
||||
|
||||
class BrightnessCrossedThresholdTrigger(
|
||||
EntityNumericalStateAttributeCrossedThresholdTriggerBase
|
||||
):
|
||||
"""Trigger for brightness crossed threshold."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_attribute = ATTR_BRIGHTNESS
|
||||
_convert_attribute = staticmethod(_convert_uint8_to_percentage)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"brightness_changed": BrightnessChangedTrigger,
|
||||
"brightness_crossed_threshold": BrightnessCrossedThresholdTrigger,
|
||||
"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),
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
max: 100
|
||||
min: 0
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
|
||||
@@ -655,10 +655,15 @@ def slug(value: Any) -> str:
|
||||
raise vol.Invalid(f"invalid slug {value} (try {slg})")
|
||||
|
||||
|
||||
def slugs(value: Any) -> list[str]:
|
||||
"""Validate slugs separated by period."""
|
||||
return [slug(item) for item in value.split(".")]
|
||||
|
||||
|
||||
def underscore_slug(value: Any) -> str:
|
||||
"""Validate value is a valid slug, possibly starting with an underscore."""
|
||||
if value.startswith("_"):
|
||||
return f"_{slug(value[1:])}"
|
||||
return f"_{'.'.join(slugs(value[1:]))}"
|
||||
return slug(value)
|
||||
|
||||
|
||||
|
||||
@@ -594,8 +594,6 @@ class EntityNumericalStateAttributeChangedTriggerBase(EntityTriggerBase):
|
||||
_above: None | float | str
|
||||
_below: None | float | str
|
||||
|
||||
_convert_attribute: Callable[[Any], float] = staticmethod(float)
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the state trigger."""
|
||||
super().__init__(hass, config)
|
||||
@@ -618,7 +616,7 @@ class EntityNumericalStateAttributeChangedTriggerBase(EntityTriggerBase):
|
||||
return False
|
||||
|
||||
try:
|
||||
current_value = self._convert_attribute(_attribute_value)
|
||||
current_value = float(_attribute_value)
|
||||
except (TypeError, ValueError):
|
||||
# Attribute is not a valid number, don't trigger
|
||||
return False
|
||||
@@ -708,8 +706,6 @@ class EntityNumericalStateAttributeCrossedThresholdTriggerBase(EntityTriggerBase
|
||||
_upper_limit: float | str | None = None
|
||||
_threshold_type: ThresholdType
|
||||
|
||||
_convert_attribute: Callable[[Any], float] = float
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize the state trigger."""
|
||||
super().__init__(hass, config)
|
||||
@@ -745,7 +741,7 @@ class EntityNumericalStateAttributeCrossedThresholdTriggerBase(EntityTriggerBase
|
||||
return False
|
||||
|
||||
try:
|
||||
current_value = self._convert_attribute(_attribute_value)
|
||||
current_value = float(_attribute_value)
|
||||
except (TypeError, ValueError):
|
||||
# Attribute is not a valid number, don't trigger
|
||||
return False
|
||||
@@ -1077,9 +1073,10 @@ async def _async_get_trigger_platform(
|
||||
) -> tuple[str, TriggerProtocol]:
|
||||
from homeassistant.components import automation # noqa: PLC0415
|
||||
|
||||
platform_and_sub_type = trigger_key.split(".")
|
||||
platform = platform_and_sub_type[0]
|
||||
platform = _PLATFORM_ALIASES.get(platform, platform)
|
||||
if not (platform := hass.data[TRIGGERS].get(trigger_key)):
|
||||
platform = _PLATFORM_ALIASES.get(trigger_key)
|
||||
if not platform:
|
||||
raise vol.Invalid(f"Invalid trigger '{trigger_key}' specified")
|
||||
|
||||
if automation.is_disabled_experimental_trigger(hass, platform):
|
||||
raise vol.Invalid(
|
||||
@@ -1437,6 +1434,10 @@ async def async_get_all_descriptions(
|
||||
if (target := yaml_description.get("target")) is not None:
|
||||
description["target"] = target
|
||||
|
||||
prefix, sep, _ = missing_trigger.partition(".")
|
||||
if sep and domain != prefix:
|
||||
description["translation_domain"] = domain
|
||||
|
||||
new_descriptions_cache[missing_trigger] = description
|
||||
hass.data[TRIGGER_DESCRIPTION_CACHE] = new_descriptions_cache
|
||||
return new_descriptions_cache
|
||||
|
||||
@@ -5,25 +5,14 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
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.const import ATTR_LABEL_ID, 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 (
|
||||
TriggerStateDescription,
|
||||
arm_trigger,
|
||||
parametrize_numerical_attribute_changed_trigger_states,
|
||||
parametrize_numerical_attribute_crossed_threshold_trigger_states,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
@@ -42,123 +31,6 @@ async def target_lights(hass: HomeAssistant) -> list[str]:
|
||||
return (await target_entities(hass, "light"))["included"]
|
||||
|
||||
|
||||
def parametrize_brightness_changed_trigger_states(
|
||||
trigger: str, state: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[TriggerStateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for numerical changed triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
other_states=[(state, {attribute: None})],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_ABOVE: 10},
|
||||
target_states=[
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 0}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={CONF_BELOW: 90},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 128}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
retrigger_on_target_state=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def parametrize_brightness_crossed_threshold_trigger_states(
|
||||
trigger: str, state: str, attribute: str
|
||||
) -> list[tuple[str, dict[str, Any], list[TriggerStateDescription]]]:
|
||||
"""Parametrize states and expected service call counts for numerical crossed threshold triggers."""
|
||||
return [
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BETWEEN,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 153}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.OUTSIDE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 153}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.ABOVE,
|
||||
CONF_LOWER_LIMIT: 10,
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 128}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 0}),
|
||||
],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger=trigger,
|
||||
trigger_options={
|
||||
CONF_THRESHOLD_TYPE: ThresholdType.BELOW,
|
||||
CONF_UPPER_LIMIT: 90,
|
||||
},
|
||||
target_states=[
|
||||
(state, {attribute: 0}),
|
||||
(state, {attribute: 128}),
|
||||
],
|
||||
other_states=[
|
||||
(state, {attribute: None}),
|
||||
(state, {attribute: 255}),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
@@ -247,10 +119,10 @@ async def test_light_state_trigger_behavior_any(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_brightness_changed_trigger_states(
|
||||
*parametrize_numerical_attribute_changed_trigger_states(
|
||||
"light.brightness_changed", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
*parametrize_brightness_crossed_threshold_trigger_states(
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
],
|
||||
@@ -358,7 +230,7 @@ async def test_light_state_trigger_behavior_first(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_brightness_crossed_threshold_trigger_states(
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
],
|
||||
@@ -466,7 +338,7 @@ async def test_light_state_trigger_behavior_last(
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "trigger_options", "states"),
|
||||
[
|
||||
*parametrize_brightness_crossed_threshold_trigger_states(
|
||||
*parametrize_numerical_attribute_crossed_threshold_trigger_states(
|
||||
"light.brightness_crossed_threshold", STATE_ON, ATTR_BRIGHTNESS
|
||||
),
|
||||
],
|
||||
|
||||
@@ -66,6 +66,16 @@ async def test_bad_trigger_platform(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_trigger_subtype(hass: HomeAssistant) -> None:
|
||||
"""Test trigger subtypes."""
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
return {
|
||||
"subtype": Mock(),
|
||||
}
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", Mock(async_get_triggers=async_get_triggers))
|
||||
await async_setup_component(hass, "test", {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.trigger.async_get_integration",
|
||||
return_value=MagicMock(async_get_platform=AsyncMock()),
|
||||
@@ -530,6 +540,7 @@ async def test_platform_multiple_triggers(
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", Mock(async_get_triggers=async_get_triggers))
|
||||
await async_setup_component(hass, "test", {})
|
||||
|
||||
config_1 = [{"platform": "test"}]
|
||||
config_2 = [{"platform": "test.trig_2", "options": {"x": 1}}]
|
||||
@@ -576,7 +587,9 @@ async def test_platform_multiple_triggers(
|
||||
}
|
||||
action_helper.action_calls.clear()
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
with pytest.raises(
|
||||
vol.Invalid, match="Invalid trigger 'test.unknown_trig' specified"
|
||||
):
|
||||
await async_initialize_triggers(
|
||||
hass, config_3, action_method, "test", "", log_cb
|
||||
)
|
||||
@@ -617,6 +630,7 @@ async def test_platform_migrate_trigger(hass: HomeAssistant) -> None:
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", Mock(async_get_triggers=async_get_triggers))
|
||||
await async_setup_component(hass, "test", {})
|
||||
|
||||
config_1 = [{"platform": "test", "option_1": "value_1", "option_2": 2}]
|
||||
config_2 = [{"platform": "test", "option_1": "value_1"}]
|
||||
@@ -646,6 +660,7 @@ async def test_platform_backwards_compatibility_for_new_style_configs(
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", MockTriggerPlatform())
|
||||
await async_setup_component(hass, "test", {})
|
||||
|
||||
config_old_style = [{"platform": "test", "option_1": "value_1", "option_2": 2}]
|
||||
result = await async_validate_trigger_config(hass, config_old_style)
|
||||
@@ -1255,6 +1270,7 @@ async def test_numerical_state_attribute_changed_trigger_config_validation(
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", Mock(async_get_triggers=async_get_triggers))
|
||||
await async_setup_component(hass, "test", {})
|
||||
|
||||
with expected_result:
|
||||
await async_validate_trigger_config(
|
||||
@@ -1283,6 +1299,7 @@ async def test_numerical_state_attribute_changed_error_handling(
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", Mock(async_get_triggers=async_get_triggers))
|
||||
await async_setup_component(hass, "test", {})
|
||||
|
||||
hass.states.async_set("test.test_entity", "on", {"test_attribute": 20})
|
||||
|
||||
@@ -1565,6 +1582,7 @@ async def test_numerical_state_attribute_crossed_threshold_trigger_config_valida
|
||||
|
||||
mock_integration(hass, MockModule("test"))
|
||||
mock_platform(hass, "test.trigger", Mock(async_get_triggers=async_get_triggers))
|
||||
await async_setup_component(hass, "test", {})
|
||||
|
||||
with expected_result:
|
||||
await async_validate_trigger_config(
|
||||
|
||||
Reference in New Issue
Block a user