1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 21:06:19 +00:00

Ensure backwards compatibility for new-style configs in old triggers and conditions (#156446)

This commit is contained in:
Artur Pragacz
2025-11-22 12:37:48 +01:00
committed by GitHub
parent 71c665ed49
commit f6b9a0eb29
6 changed files with 184 additions and 6 deletions

View File

@@ -34,7 +34,8 @@ def move_top_level_schema_fields_to_options(
) -> ConfigType:
"""Move top-level fields to options.
This function is used to help migrating old-style configs to new-style configs.
This function is used to help migrating old-style configs to new-style configs
for triggers and conditions.
If options is already present, the config is returned as-is.
"""
if CONF_OPTIONS in config:
@@ -50,3 +51,38 @@ def move_top_level_schema_fields_to_options(
options[key] = config.pop(key)
return config
def move_options_fields_to_top_level(
config: ConfigType, base_schema: vol.Schema
) -> ConfigType:
"""Move options fields to top-level.
This function is used to provide backwards compatibility for new-style configs
for triggers and conditions.
The config is returned as-is, if any of the following is true:
- options is not present
- options is not a dict
- the config with options field removed fails the base_schema validation (most
likely due to additional keys being present)
Those conditions are checked to make it so that only configs that have the structure
of the new-style are modified, whereas valid old-style configs are preserved.
"""
options = config.get(CONF_OPTIONS)
if not isinstance(options, dict):
return config
new_config: ConfigType = config.copy()
new_config.pop(CONF_OPTIONS)
try:
new_config = base_schema(new_config)
except vol.Invalid:
return config
new_config.update(options)
return new_config

View File

@@ -64,7 +64,11 @@ from homeassistant.util.hass_dict import HassKey
from homeassistant.util.yaml import load_yaml_dict
from . import config_validation as cv, entity_registry as er, selector
from .automation import get_absolute_description_key, get_relative_description_key
from .automation import (
get_absolute_description_key,
get_relative_description_key,
move_options_fields_to_top_level,
)
from .integration_platform import async_process_integration_platforms
from .selector import TargetSelector
from .template import Template, render_complex
@@ -202,10 +206,14 @@ async def _register_condition_platform(
_LOGGER.exception("Error while notifying condition platform listener")
_CONDITION_SCHEMA = vol.Schema(
_CONDITION_BASE_SCHEMA = vol.Schema(
{
**cv.CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): str,
}
)
_CONDITION_SCHEMA = _CONDITION_BASE_SCHEMA.extend(
{
vol.Optional(CONF_OPTIONS): object,
vol.Optional(CONF_TARGET): cv.TARGET_FIELDS,
}
@@ -1044,6 +1052,8 @@ async def async_validate_condition_config(
raise vol.Invalid(f"Invalid condition '{condition_key}' specified")
return await condition_class.async_validate_complete_config(hass, config)
config = move_options_fields_to_top_level(config, _CONDITION_BASE_SCHEMA)
if condition_key in ("numeric_state", "state"):
validator = cast(
Callable[[HomeAssistant, ConfigType], ConfigType],

View File

@@ -46,7 +46,11 @@ from homeassistant.util.hass_dict import HassKey
from homeassistant.util.yaml import load_yaml_dict
from . import config_validation as cv, selector
from .automation import get_absolute_description_key, get_relative_description_key
from .automation import (
get_absolute_description_key,
get_relative_description_key,
move_options_fields_to_top_level,
)
from .integration_platform import async_process_integration_platforms
from .selector import TargetSelector
from .template import Template
@@ -497,8 +501,10 @@ async def async_validate_trigger_config(
raise vol.Invalid(f"Invalid trigger '{trigger_key}' specified")
conf = await trigger.async_validate_complete_config(hass, conf)
elif hasattr(platform, "async_validate_trigger_config"):
conf = move_options_fields_to_top_level(conf, cv.TRIGGER_BASE_SCHEMA)
conf = await platform.async_validate_trigger_config(hass, conf)
else:
conf = move_options_fields_to_top_level(conf, cv.TRIGGER_BASE_SCHEMA)
conf = platform.TRIGGER_SCHEMA(conf)
config.append(conf)
return config

View File

@@ -6,6 +6,7 @@ import voluptuous as vol
from homeassistant.helpers.automation import (
get_absolute_description_key,
get_relative_description_key,
move_options_fields_to_top_level,
move_top_level_schema_fields_to_options,
)
@@ -106,3 +107,76 @@ async def test_move_schema_fields_to_options(
assert (
move_top_level_schema_fields_to_options(config, schema_dict) == expected_config
)
@pytest.mark.parametrize(
("config", "expected_config"),
[
(
{
"platform": "test",
"options": {
"entity": "sensor.test",
"from": "open",
"to": "closed",
"for": {"hours": 1},
},
},
{
"platform": "test",
"entity": "sensor.test",
"from": "open",
"to": "closed",
"for": {"hours": 1},
},
),
(
{
"platform": "test",
"entity": "sensor.test",
"from": "open",
"to": "closed",
"for": {"hours": 1},
},
{
"platform": "test",
"entity": "sensor.test",
"from": "open",
"to": "closed",
"for": {"hours": 1},
},
),
(
{
"platform": "test",
"options": 456,
},
{
"platform": "test",
"options": 456,
},
),
(
{
"platform": "test",
"options": {
"entity": "sensor.test",
},
"extra_field": "extra_value",
},
{
"platform": "test",
"options": {
"entity": "sensor.test",
},
"extra_field": "extra_value",
},
),
],
)
async def test_move_options_fields_to_top_level(config, expected_config) -> None:
"""Test moving options fields to top-level."""
base_schema = vol.Schema({vol.Required("platform"): str})
original_config = config.copy()
assert move_options_fields_to_top_level(config, base_schema) == expected_config
assert config == original_config # Ensure original config is not modified

View File

@@ -2170,7 +2170,7 @@ async def test_platform_multiple_conditions(hass: HomeAssistant) -> None:
await condition.async_from_config(hass, config_3)
async def test_platform_migrate_trigger(hass: HomeAssistant) -> None:
async def test_platform_migrate_condition(hass: HomeAssistant) -> None:
"""Test a condition platform with a migration."""
OPTIONS_SCHEMA_DICT = {
@@ -2238,6 +2238,29 @@ async def test_platform_migrate_trigger(hass: HomeAssistant) -> None:
)
async def test_platform_backwards_compatibility_for_new_style_configs(
hass: HomeAssistant,
) -> None:
"""Test backwards compatibility for old-style conditions with new-style configs."""
config_old_style = {
"condition": "numeric_state",
"entity_id": ["sensor.test"],
"above": 50,
}
result = await async_validate_condition_config(hass, config_old_style)
assert result == config_old_style
config_new_style = {
"condition": "numeric_state",
"options": {
"entity_id": ["sensor.test"],
"above": 50,
},
}
result = await async_validate_condition_config(hass, config_new_style)
assert result == config_old_style
@pytest.mark.parametrize("enabled_value", [True, "{{ 1 == 1 }}"])
async def test_enabled_condition(
hass: HomeAssistant, enabled_value: bool | str

View File

@@ -18,7 +18,7 @@ from homeassistant.core import (
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import trigger
from homeassistant.helpers import config_validation as cv, trigger
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
from homeassistant.helpers.trigger import (
DATA_PLUGGABLE_ACTIONS,
@@ -607,6 +607,35 @@ async def test_platform_migrate_trigger(hass: HomeAssistant) -> None:
assert await async_validate_trigger_config(hass, config_4) == config_4
async def test_platform_backwards_compatibility_for_new_style_configs(
hass: HomeAssistant,
) -> None:
"""Test backwards compatibility for old-style triggers with new-style configs."""
class MockTriggerPlatform:
"""Mock trigger platform."""
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
vol.Required("option_1"): str,
vol.Optional("option_2"): int,
}
)
mock_integration(hass, MockModule("test"))
mock_platform(hass, "test.trigger", MockTriggerPlatform())
config_old_style = [{"platform": "test", "option_1": "value_1", "option_2": 2}]
result = await async_validate_trigger_config(hass, config_old_style)
assert result == config_old_style
config_new_style = [
{"platform": "test", "options": {"option_1": "value_1", "option_2": 2}}
]
result = await async_validate_trigger_config(hass, config_new_style)
assert result == config_old_style
@pytest.mark.parametrize(
"sun_trigger_descriptions",
[