mirror of
https://github.com/home-assistant/core.git
synced 2025-12-27 14:31:13 +00:00
Disable experimental conditions according to labs flag setting (#157345)
This commit is contained in:
@@ -117,6 +117,10 @@ SERVICE_TRIGGER = "trigger"
|
||||
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||
|
||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"light",
|
||||
}
|
||||
|
||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
@@ -131,6 +135,19 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def is_disabled_experimental_condition(hass: HomeAssistant, platform: str) -> bool:
|
||||
"""Check if the platform is a disabled experimental condition platform."""
|
||||
return (
|
||||
platform in _EXPERIMENTAL_CONDITION_PLATFORMS
|
||||
and not labs.async_is_preview_feature_enabled(
|
||||
hass,
|
||||
DOMAIN,
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def is_disabled_experimental_trigger(hass: HomeAssistant, platform: str) -> bool:
|
||||
"""Check if the platform is a disabled experimental trigger platform."""
|
||||
|
||||
@@ -110,6 +110,9 @@ INPUT_ENTITY_ID = re.compile(
|
||||
CONDITION_DESCRIPTION_CACHE: HassKey[dict[str, dict[str, Any] | None]] = HassKey(
|
||||
"condition_description_cache"
|
||||
)
|
||||
CONDITION_DISABLED_CONDITIONS: HassKey[set[str]] = HassKey(
|
||||
"condition_disabled_conditions"
|
||||
)
|
||||
CONDITION_PLATFORM_SUBSCRIPTIONS: HassKey[
|
||||
list[Callable[[set[str]], Coroutine[Any, Any, None]]]
|
||||
] = HassKey("condition_platform_subscriptions")
|
||||
@@ -151,9 +154,27 @@ _CONDITIONS_DESCRIPTION_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the condition helper."""
|
||||
from homeassistant.components import automation, labs # noqa: PLC0415
|
||||
|
||||
hass.data[CONDITION_DESCRIPTION_CACHE] = {}
|
||||
hass.data[CONDITION_DISABLED_CONDITIONS] = set()
|
||||
hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS] = []
|
||||
hass.data[CONDITIONS] = {}
|
||||
|
||||
@callback
|
||||
def new_triggers_conditions_listener() -> None:
|
||||
"""Handle new_triggers_conditions flag change."""
|
||||
# Invalidate the cache
|
||||
hass.data[CONDITION_DESCRIPTION_CACHE] = {}
|
||||
hass.data[CONDITION_DISABLED_CONDITIONS] = set()
|
||||
|
||||
labs.async_listen(
|
||||
hass,
|
||||
automation.DOMAIN,
|
||||
automation.NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG,
|
||||
new_triggers_conditions_listener,
|
||||
)
|
||||
|
||||
await async_process_integration_platforms(
|
||||
hass, "condition", _register_condition_platform, wait_for_platforms=True
|
||||
)
|
||||
@@ -352,11 +373,21 @@ def trace_condition_function(condition: ConditionCheckerType) -> ConditionChecke
|
||||
async def _async_get_condition_platform(
|
||||
hass: HomeAssistant, condition_key: str
|
||||
) -> tuple[str, ConditionProtocol | None]:
|
||||
from homeassistant.components import automation # noqa: PLC0415
|
||||
|
||||
platform_and_sub_type = condition_key.split(".")
|
||||
platform: str | None = platform_and_sub_type[0]
|
||||
platform = _PLATFORM_ALIASES.get(platform, platform)
|
||||
if platform is None:
|
||||
return "", None
|
||||
|
||||
if automation.is_disabled_experimental_condition(hass, platform):
|
||||
raise vol.Invalid(
|
||||
f"Condition '{condition_key}' requires the experimental 'New triggers and "
|
||||
"conditions' feature to be enabled in Home Assistant Labs settings "
|
||||
f"(feature flag: '{automation.NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG}')"
|
||||
)
|
||||
|
||||
try:
|
||||
integration = await async_get_integration(hass, platform)
|
||||
except IntegrationNotFound:
|
||||
@@ -1209,6 +1240,8 @@ async def async_get_all_descriptions(
|
||||
hass: HomeAssistant,
|
||||
) -> dict[str, dict[str, Any] | None]:
|
||||
"""Return descriptions (i.e. user documentation) for all conditions."""
|
||||
from homeassistant.components import automation # noqa: PLC0415
|
||||
|
||||
descriptions_cache = hass.data[CONDITION_DESCRIPTION_CACHE]
|
||||
|
||||
conditions = hass.data[CONDITIONS]
|
||||
@@ -1217,7 +1250,12 @@ async def async_get_all_descriptions(
|
||||
all_conditions = set(conditions)
|
||||
previous_all_conditions = set(descriptions_cache)
|
||||
# If the conditions are the same, we can return the cache
|
||||
if previous_all_conditions == all_conditions:
|
||||
|
||||
# mypy complains: Invalid index type "HassKey[set[str]]" for "HassDict"
|
||||
if (
|
||||
previous_all_conditions | hass.data[CONDITION_DISABLED_CONDITIONS] # type: ignore[index]
|
||||
== all_conditions
|
||||
):
|
||||
return descriptions_cache
|
||||
|
||||
# Files we loaded for missing descriptions
|
||||
@@ -1257,6 +1295,9 @@ async def async_get_all_descriptions(
|
||||
new_descriptions_cache = descriptions_cache.copy()
|
||||
for missing_condition in missing_conditions:
|
||||
domain = conditions[missing_condition]
|
||||
if automation.is_disabled_experimental_condition(hass, domain):
|
||||
hass.data[CONDITION_DISABLED_CONDITIONS].add(missing_condition)
|
||||
continue
|
||||
|
||||
if (
|
||||
yaml_description := new_conditions_descriptions.get(domain, {}).get(
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
"""Test light conditions."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.const import (
|
||||
ATTR_LABEL_ID,
|
||||
CONF_CONDITION,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
@@ -83,6 +87,39 @@ async def has_call_after_trigger(
|
||||
return has_calls
|
||||
|
||||
|
||||
@pytest.fixture(name="enable_experimental_triggers_conditions")
|
||||
def enable_experimental_triggers_conditions() -> Generator[None]:
|
||||
"""Enable experimental triggers and conditions."""
|
||||
with patch(
|
||||
"homeassistant.components.labs.async_is_preview_feature_enabled",
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
"light.is_off",
|
||||
"light.is_on",
|
||||
],
|
||||
)
|
||||
async def test_light_conditions_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str
|
||||
) -> None:
|
||||
"""Test the light conditions are gated by the labs flag."""
|
||||
await setup_automation_with_light_condition(
|
||||
hass, condition=condition, target={ATTR_LABEL_ID: "test_label"}, behavior="any"
|
||||
)
|
||||
assert (
|
||||
"Unnamed automation failed to setup conditions and has been disabled: "
|
||||
f"Condition '{condition}' requires the experimental 'New triggers and "
|
||||
"conditions' feature to be enabled in Home Assistant Labs settings "
|
||||
"(feature flag: 'new_triggers_conditions')"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("light"),
|
||||
@@ -166,6 +203,7 @@ async def test_light_state_condition_behavior_any(
|
||||
assert not await has_call_after_trigger(hass, service_calls)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("light"),
|
||||
|
||||
@@ -13,6 +13,7 @@ import voluptuous as vol
|
||||
from homeassistant.components.device_automation import (
|
||||
DOMAIN as DOMAIN_DEVICE_AUTOMATION,
|
||||
)
|
||||
from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sun import DOMAIN as DOMAIN_SUN
|
||||
from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH
|
||||
@@ -47,6 +48,7 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.yaml.loader import parse_yaml
|
||||
|
||||
from tests.common import MockModule, MockPlatform, mock_integration, mock_platform
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
def assert_element(trace_element, expected_element, path):
|
||||
@@ -2500,7 +2502,9 @@ async def test_or_condition_with_disabled_condition(hass: HomeAssistant) -> None
|
||||
],
|
||||
)
|
||||
async def test_async_get_all_descriptions(
|
||||
hass: HomeAssistant, sun_condition_descriptions: str
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
sun_condition_descriptions: str,
|
||||
) -> None:
|
||||
"""Test async_get_all_descriptions."""
|
||||
device_automation_condition_descriptions = """
|
||||
@@ -2514,6 +2518,18 @@ async def test_async_get_all_descriptions(
|
||||
supported_features:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
|
||||
"""
|
||||
light_condition_descriptions = """
|
||||
is_off:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
is_on:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
"""
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN_SUN, {})
|
||||
assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {})
|
||||
@@ -2522,6 +2538,8 @@ async def test_async_get_all_descriptions(
|
||||
def _load_yaml(fname, secrets=None):
|
||||
if fname.endswith("device_automation/conditions.yaml"):
|
||||
condition_descriptions = device_automation_condition_descriptions
|
||||
elif fname.endswith("light/conditions.yaml"):
|
||||
condition_descriptions = light_condition_descriptions
|
||||
elif fname.endswith("sun/conditions.yaml"):
|
||||
condition_descriptions = sun_condition_descriptions
|
||||
with io.StringIO(condition_descriptions) as file:
|
||||
@@ -2549,7 +2567,7 @@ async def test_async_get_all_descriptions(
|
||||
)
|
||||
|
||||
# system_health does not have conditions and should not be in descriptions
|
||||
assert descriptions == {
|
||||
expected_descriptions = {
|
||||
"sun": {
|
||||
"fields": {
|
||||
"after": {
|
||||
@@ -2579,6 +2597,7 @@ async def test_async_get_all_descriptions(
|
||||
}
|
||||
}
|
||||
}
|
||||
assert descriptions == expected_descriptions
|
||||
|
||||
# Verify the cache returns the same object
|
||||
assert await condition.async_get_all_descriptions(hass) is descriptions
|
||||
@@ -2596,35 +2615,8 @@ async def test_async_get_all_descriptions(
|
||||
):
|
||||
new_descriptions = await condition.async_get_all_descriptions(hass)
|
||||
assert new_descriptions is not descriptions
|
||||
assert new_descriptions == {
|
||||
"sun": {
|
||||
"fields": {
|
||||
"after": {
|
||||
"example": "sunrise",
|
||||
"selector": {
|
||||
"select": {
|
||||
"custom_value": False,
|
||||
"multiple": False,
|
||||
"options": ["sunrise", "sunset"],
|
||||
"sort": False,
|
||||
}
|
||||
},
|
||||
},
|
||||
"after_offset": {"selector": {"time": {}}},
|
||||
"before": {
|
||||
"example": "sunrise",
|
||||
"selector": {
|
||||
"select": {
|
||||
"custom_value": False,
|
||||
"multiple": False,
|
||||
"options": ["sunrise", "sunset"],
|
||||
"sort": False,
|
||||
}
|
||||
},
|
||||
},
|
||||
"before_offset": {"selector": {"time": {}}},
|
||||
}
|
||||
},
|
||||
# The device automation conditions should now be present
|
||||
expected_descriptions |= {
|
||||
"device": {
|
||||
"fields": {
|
||||
"entity": {
|
||||
@@ -2644,6 +2636,114 @@ async def test_async_get_all_descriptions(
|
||||
}
|
||||
},
|
||||
}
|
||||
assert new_descriptions == expected_descriptions
|
||||
|
||||
# Verify the cache returns the same object
|
||||
assert await condition.async_get_all_descriptions(hass) is new_descriptions
|
||||
|
||||
# Load the light integration and check a new cache object is created
|
||||
assert await async_setup_component(hass, DOMAIN_LIGHT, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"annotatedyaml.loader.load_yaml",
|
||||
side_effect=_load_yaml,
|
||||
),
|
||||
patch.object(Integration, "has_conditions", return_value=True),
|
||||
):
|
||||
new_descriptions = await condition.async_get_all_descriptions(hass)
|
||||
assert new_descriptions is not descriptions
|
||||
# No light conditions added, they are gated by the automation.new_triggers_conditions
|
||||
# labs flag
|
||||
assert new_descriptions == expected_descriptions
|
||||
|
||||
# Verify the cache returns the same object
|
||||
assert await condition.async_get_all_descriptions(hass) is new_descriptions
|
||||
|
||||
# Enable the new_triggers_conditions flag and verify light conditions are loaded
|
||||
assert await async_setup_component(hass, "labs", {})
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "labs/update",
|
||||
"domain": "automation",
|
||||
"preview_feature": "new_triggers_conditions",
|
||||
"enabled": True,
|
||||
}
|
||||
)
|
||||
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"annotatedyaml.loader.load_yaml",
|
||||
side_effect=_load_yaml,
|
||||
),
|
||||
patch.object(Integration, "has_conditions", return_value=True),
|
||||
):
|
||||
new_descriptions = await condition.async_get_all_descriptions(hass)
|
||||
assert new_descriptions is not descriptions
|
||||
# The light conditions should now be present
|
||||
assert new_descriptions == expected_descriptions | {
|
||||
"light.is_off": {
|
||||
"fields": {},
|
||||
"target": {
|
||||
"entity": [
|
||||
{
|
||||
"domain": [
|
||||
"light",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"light.is_on": {
|
||||
"fields": {},
|
||||
"target": {
|
||||
"entity": [
|
||||
{
|
||||
"domain": [
|
||||
"light",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Verify the cache returns the same object
|
||||
assert await condition.async_get_all_descriptions(hass) is new_descriptions
|
||||
|
||||
# Disable the new_triggers_conditions flag and verify light conditions are removed
|
||||
assert await async_setup_component(hass, "labs", {})
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "labs/update",
|
||||
"domain": "automation",
|
||||
"preview_feature": "new_triggers_conditions",
|
||||
"enabled": False,
|
||||
}
|
||||
)
|
||||
|
||||
msg = await ws_client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"annotatedyaml.loader.load_yaml",
|
||||
side_effect=_load_yaml,
|
||||
),
|
||||
patch.object(Integration, "has_conditions", return_value=True),
|
||||
):
|
||||
new_descriptions = await condition.async_get_all_descriptions(hass)
|
||||
assert new_descriptions is not descriptions
|
||||
# The light conditions should no longer be present
|
||||
assert new_descriptions == expected_descriptions
|
||||
|
||||
# Verify the cache returns the same object
|
||||
assert await condition.async_get_all_descriptions(hass) is new_descriptions
|
||||
|
||||
Reference in New Issue
Block a user