Skip to content

Commit 4900d25

Browse files
authored
Disable experimental triggers according to labs flag setting (home-assistant#157320)
1 parent ea10cdb commit 4900d25

File tree

14 files changed

+608
-32
lines changed

14 files changed

+608
-32
lines changed

homeassistant/components/automation/__init__.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from propcache.api import cached_property
1313
import voluptuous as vol
1414

15-
from homeassistant.components import websocket_api
15+
from homeassistant.components import labs, websocket_api
1616
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
1717
from homeassistant.const import (
1818
ATTR_ENTITY_ID,
@@ -114,6 +114,34 @@
114114
ATTR_VARIABLES = "variables"
115115
SERVICE_TRIGGER = "trigger"
116116

117+
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
118+
119+
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
120+
"alarm_control_panel",
121+
"assist_satellite",
122+
"climate",
123+
"cover",
124+
"fan",
125+
"lawn_mower",
126+
"light",
127+
"media_player",
128+
"text",
129+
"vacuum",
130+
}
131+
132+
133+
@callback
134+
def is_disabled_experimental_trigger(hass: HomeAssistant, platform: str) -> bool:
135+
"""Check if the platform is a disabled experimental trigger platform."""
136+
return (
137+
platform in _EXPERIMENTAL_TRIGGER_PLATFORMS
138+
and not labs.async_is_preview_feature_enabled(
139+
hass,
140+
DOMAIN,
141+
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
142+
)
143+
)
144+
117145

118146
class IfAction(Protocol):
119147
"""Define the format of if_action."""

homeassistant/helpers/trigger.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
TRIGGER_DESCRIPTION_CACHE: HassKey[dict[str, dict[str, Any] | None]] = HassKey(
8484
"trigger_description_cache"
8585
)
86+
TRIGGER_DISABLED_TRIGGERS: HassKey[set[str]] = HassKey("trigger_disabled_triggers")
8687
TRIGGER_PLATFORM_SUBSCRIPTIONS: HassKey[
8788
list[Callable[[set[str]], Coroutine[Any, Any, None]]]
8889
] = HassKey("trigger_platform_subscriptions")
@@ -124,9 +125,27 @@ def starts_with_dot(key: str) -> str:
124125

125126
async def async_setup(hass: HomeAssistant) -> None:
126127
"""Set up the trigger helper."""
128+
from homeassistant.components import automation, labs # noqa: PLC0415
129+
127130
hass.data[TRIGGER_DESCRIPTION_CACHE] = {}
131+
hass.data[TRIGGER_DISABLED_TRIGGERS] = set()
128132
hass.data[TRIGGER_PLATFORM_SUBSCRIPTIONS] = []
129133
hass.data[TRIGGERS] = {}
134+
135+
@callback
136+
def new_triggers_conditions_listener() -> None:
137+
"""Handle new_triggers_conditions flag change."""
138+
# Invalidate the cache
139+
hass.data[TRIGGER_DESCRIPTION_CACHE] = {}
140+
hass.data[TRIGGER_DISABLED_TRIGGERS] = set()
141+
142+
labs.async_listen(
143+
hass,
144+
automation.DOMAIN,
145+
automation.NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
146+
new_triggers_conditions_listener,
147+
)
148+
130149
await async_process_integration_platforms(
131150
hass, "trigger", _register_trigger_platform, wait_for_platforms=True
132151
)
@@ -694,9 +713,19 @@ async def async_run(
694713
async def _async_get_trigger_platform(
695714
hass: HomeAssistant, trigger_key: str
696715
) -> tuple[str, TriggerProtocol]:
716+
from homeassistant.components import automation # noqa: PLC0415
717+
697718
platform_and_sub_type = trigger_key.split(".")
698719
platform = platform_and_sub_type[0]
699720
platform = _PLATFORM_ALIASES.get(platform, platform)
721+
722+
if automation.is_disabled_experimental_trigger(hass, platform):
723+
raise vol.Invalid(
724+
f"Trigger '{trigger_key}' requires the experimental 'New triggers and "
725+
"conditions' feature to be enabled in Home Assistant Labs settings "
726+
f"(feature flag: '{automation.NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG}')"
727+
)
728+
700729
try:
701730
integration = await async_get_integration(hass, platform)
702731
except IntegrationNotFound:
@@ -976,6 +1005,8 @@ async def async_get_all_descriptions(
9761005
hass: HomeAssistant,
9771006
) -> dict[str, dict[str, Any] | None]:
9781007
"""Return descriptions (i.e. user documentation) for all triggers."""
1008+
from homeassistant.components import automation # noqa: PLC0415
1009+
9791010
descriptions_cache = hass.data[TRIGGER_DESCRIPTION_CACHE]
9801011

9811012
triggers = hass.data[TRIGGERS]
@@ -984,7 +1015,9 @@ async def async_get_all_descriptions(
9841015
all_triggers = set(triggers)
9851016
previous_all_triggers = set(descriptions_cache)
9861017
# If the triggers are the same, we can return the cache
987-
if previous_all_triggers == all_triggers:
1018+
1019+
# mypy complains: Invalid index type "HassKey[set[str]]" for "HassDict"
1020+
if previous_all_triggers | hass.data[TRIGGER_DISABLED_TRIGGERS] == all_triggers: # type: ignore[index]
9881021
return descriptions_cache
9891022

9901023
# Files we loaded for missing descriptions
@@ -1022,6 +1055,9 @@ async def async_get_all_descriptions(
10221055
new_descriptions_cache = descriptions_cache.copy()
10231056
for missing_trigger in missing_triggers:
10241057
domain = triggers[missing_trigger]
1058+
if automation.is_disabled_experimental_trigger(hass, domain):
1059+
hass.data[TRIGGER_DISABLED_TRIGGERS].add(missing_trigger)
1060+
continue
10251061

10261062
if (
10271063
yaml_description := new_triggers_descriptions.get(domain, {}).get(

tests/components/alarm_control_panel/test_trigger.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"""Test alarm control panel triggers."""
22

3+
from collections.abc import Generator
4+
from unittest.mock import patch
5+
36
import pytest
47

58
from homeassistant.components.alarm_control_panel import (
69
AlarmControlPanelEntityFeature,
710
AlarmControlPanelState,
811
)
9-
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_ENTITY_ID
12+
from homeassistant.const import ATTR_LABEL_ID, ATTR_SUPPORTED_FEATURES, CONF_ENTITY_ID
1013
from homeassistant.core import HomeAssistant, ServiceCall
1114
from homeassistant.setup import async_setup_component
1215

@@ -26,12 +29,48 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
2629
"""Stub copying the blueprints to the config folder."""
2730

2831

32+
@pytest.fixture(name="enable_experimental_triggers_conditions")
33+
def enable_experimental_triggers_conditions() -> Generator[None]:
34+
"""Enable experimental triggers and conditions."""
35+
with patch(
36+
"homeassistant.components.labs.async_is_preview_feature_enabled",
37+
return_value=True,
38+
):
39+
yield
40+
41+
2942
@pytest.fixture
3043
async def target_alarm_control_panels(hass: HomeAssistant) -> list[str]:
3144
"""Create multiple alarm control panel entities associated with different targets."""
3245
return await target_entities(hass, "alarm_control_panel")
3346

3447

48+
@pytest.mark.parametrize(
49+
"trigger_key",
50+
[
51+
"alarm_control_panel.armed",
52+
"alarm_control_panel.armed_away",
53+
"alarm_control_panel.armed_home",
54+
"alarm_control_panel.armed_night",
55+
"alarm_control_panel.armed_vacation",
56+
"alarm_control_panel.disarmed",
57+
"alarm_control_panel.triggered",
58+
],
59+
)
60+
async def test_alarm_control_panel_triggers_gated_by_labs_flag(
61+
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
62+
) -> None:
63+
"""Test the ACP triggers are gated by the labs flag."""
64+
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
65+
assert (
66+
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
67+
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
68+
"feature to be enabled in Home Assistant Labs settings (feature flag: "
69+
"'new_triggers_conditions')"
70+
) in caplog.text
71+
72+
73+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
3574
@pytest.mark.parametrize(
3675
("trigger_target_config", "entity_id", "entities_in_target"),
3776
parametrize_target_entities("alarm_control_panel"),
@@ -142,6 +181,7 @@ async def test_alarm_control_panel_state_trigger_behavior_any(
142181
service_calls.clear()
143182

144183

184+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
145185
@pytest.mark.parametrize(
146186
("trigger_target_config", "entity_id", "entities_in_target"),
147187
parametrize_target_entities("alarm_control_panel"),
@@ -251,6 +291,7 @@ async def test_alarm_control_panel_state_trigger_behavior_first(
251291
assert len(service_calls) == 0
252292

253293

294+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
254295
@pytest.mark.parametrize(
255296
("trigger_target_config", "entity_id", "entities_in_target"),
256297
parametrize_target_entities("alarm_control_panel"),

tests/components/assist_satellite/test_trigger.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""Test assist satellite triggers."""
22

3+
from collections.abc import Generator
4+
from unittest.mock import patch
5+
36
import pytest
47

58
from homeassistant.components.assist_satellite.entity import AssistSatelliteState
6-
from homeassistant.const import CONF_ENTITY_ID
9+
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID
710
from homeassistant.core import HomeAssistant, ServiceCall
811
from homeassistant.setup import async_setup_component
912

@@ -23,12 +26,45 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
2326
"""Stub copying the blueprints to the config folder."""
2427

2528

29+
@pytest.fixture(name="enable_experimental_triggers_conditions")
30+
def enable_experimental_triggers_conditions() -> Generator[None]:
31+
"""Enable experimental triggers and conditions."""
32+
with patch(
33+
"homeassistant.components.labs.async_is_preview_feature_enabled",
34+
return_value=True,
35+
):
36+
yield
37+
38+
2639
@pytest.fixture
2740
async def target_assist_satellites(hass: HomeAssistant) -> list[str]:
2841
"""Create multiple assist satellite entities associated with different targets."""
2942
return await target_entities(hass, "assist_satellite")
3043

3144

45+
@pytest.mark.parametrize(
46+
"trigger_key",
47+
[
48+
"assist_satellite.idle",
49+
"assist_satellite.listening",
50+
"assist_satellite.processing",
51+
"assist_satellite.responding",
52+
],
53+
)
54+
async def test_assist_satellite_triggers_gated_by_labs_flag(
55+
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
56+
) -> None:
57+
"""Test the Assist satellite triggers are gated by the labs flag."""
58+
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
59+
assert (
60+
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
61+
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
62+
"feature to be enabled in Home Assistant Labs settings (feature flag: "
63+
"'new_triggers_conditions')"
64+
) in caplog.text
65+
66+
67+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
3268
@pytest.mark.parametrize(
3369
("trigger_target_config", "entity_id", "entities_in_target"),
3470
parametrize_target_entities("assist_satellite"),
@@ -96,6 +132,7 @@ async def test_assist_satellite_state_trigger_behavior_any(
96132
service_calls.clear()
97133

98134

135+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
99136
@pytest.mark.parametrize(
100137
("trigger_target_config", "entity_id", "entities_in_target"),
101138
parametrize_target_entities("assist_satellite"),
@@ -162,6 +199,7 @@ async def test_assist_satellite_state_trigger_behavior_first(
162199
assert len(service_calls) == 0
163200

164201

202+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
165203
@pytest.mark.parametrize(
166204
("trigger_target_config", "entity_id", "entities_in_target"),
167205
parametrize_target_entities("assist_satellite"),

tests/components/climate/test_trigger.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
"""Test climate trigger."""
22

3+
from collections.abc import Generator
4+
from unittest.mock import patch
5+
36
import pytest
47

58
from homeassistant.components.climate.const import (
69
ATTR_HVAC_ACTION,
710
HVACAction,
811
HVACMode,
912
)
10-
from homeassistant.const import CONF_ENTITY_ID
13+
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID
1114
from homeassistant.core import HomeAssistant, ServiceCall
1215
from homeassistant.setup import async_setup_component
1316

@@ -27,12 +30,44 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
2730
"""Stub copying the blueprints to the config folder."""
2831

2932

33+
@pytest.fixture(name="enable_experimental_triggers_conditions")
34+
def enable_experimental_triggers_conditions() -> Generator[None]:
35+
"""Enable experimental triggers and conditions."""
36+
with patch(
37+
"homeassistant.components.labs.async_is_preview_feature_enabled",
38+
return_value=True,
39+
):
40+
yield
41+
42+
3043
@pytest.fixture
3144
async def target_climates(hass: HomeAssistant) -> list[str]:
3245
"""Create multiple climate entities associated with different targets."""
3346
return await target_entities(hass, "climate")
3447

3548

49+
@pytest.mark.parametrize(
50+
"trigger_key",
51+
[
52+
"climate.turned_off",
53+
"climate.turned_on",
54+
"climate.started_heating",
55+
],
56+
)
57+
async def test_climate_triggers_gated_by_labs_flag(
58+
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
59+
) -> None:
60+
"""Test the climate triggers are gated by the labs flag."""
61+
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
62+
assert (
63+
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
64+
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
65+
"feature to be enabled in Home Assistant Labs settings (feature flag: "
66+
"'new_triggers_conditions')"
67+
) in caplog.text
68+
69+
70+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
3671
@pytest.mark.parametrize(
3772
("trigger_target_config", "entity_id", "entities_in_target"),
3873
parametrize_target_entities("climate"),
@@ -99,6 +134,7 @@ async def test_climate_state_trigger_behavior_any(
99134
service_calls.clear()
100135

101136

137+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
102138
@pytest.mark.parametrize(
103139
("trigger_target_config", "entity_id", "entities_in_target"),
104140
parametrize_target_entities("climate"),
@@ -151,6 +187,7 @@ async def test_climate_state_attribute_trigger_behavior_any(
151187
service_calls.clear()
152188

153189

190+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
154191
@pytest.mark.parametrize(
155192
("trigger_target_config", "entity_id", "entities_in_target"),
156193
parametrize_target_entities("climate"),
@@ -216,6 +253,7 @@ async def test_climate_state_trigger_behavior_first(
216253
assert len(service_calls) == 0
217254

218255

256+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
219257
@pytest.mark.parametrize(
220258
("trigger_target_config", "entity_id", "entities_in_target"),
221259
parametrize_target_entities("climate"),
@@ -267,6 +305,7 @@ async def test_climate_state_attribute_trigger_behavior_first(
267305
assert len(service_calls) == 0
268306

269307

308+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
270309
@pytest.mark.parametrize(
271310
("trigger_target_config", "entity_id", "entities_in_target"),
272311
parametrize_target_entities("climate"),
@@ -331,6 +370,7 @@ async def test_climate_state_trigger_behavior_last(
331370
service_calls.clear()
332371

333372

373+
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
334374
@pytest.mark.parametrize(
335375
("trigger_target_config", "entity_id", "entities_in_target"),
336376
parametrize_target_entities("climate"),

0 commit comments

Comments
 (0)