1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Use relative condition keys (#150021)

This commit is contained in:
Artur Pragacz
2025-08-06 18:20:30 +02:00
committed by GitHub
parent 4e2fe63182
commit 06130219b4
9 changed files with 65 additions and 52 deletions

View File

@@ -80,7 +80,7 @@ class DeviceCondition(Condition):
CONDITIONS: dict[str, type[Condition]] = {
"device": DeviceCondition,
"_device": DeviceCondition,
}

View File

@@ -153,7 +153,7 @@ class SunCondition(Condition):
CONDITIONS: dict[str, type[Condition]] = {
"sun": SunCondition,
"_": SunCondition,
}

View File

@@ -147,7 +147,7 @@ class ZoneCondition(Condition):
CONDITIONS: dict[str, type[Condition]] = {
"zone": ZoneCondition,
"_": ZoneCondition,
}

View File

@@ -58,9 +58,9 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.yaml import load_yaml_dict
from homeassistant.util.yaml.loader import JSON_TYPE
from . import config_validation as cv, entity_registry as er
from .automation import get_absolute_description_key, get_relative_description_key
from .integration_platform import async_process_integration_platforms
from .template import Template, render_complex
from .trace import (
@@ -132,7 +132,7 @@ def starts_with_dot(key: str) -> str:
_CONDITIONS_SCHEMA = vol.Schema(
{
vol.Remove(vol.All(str, starts_with_dot)): object,
cv.slug: vol.Any(None, _CONDITION_SCHEMA),
cv.underscore_slug: vol.Any(None, _CONDITION_SCHEMA),
}
)
@@ -171,6 +171,9 @@ async def _register_condition_platform(
if hasattr(platform, "async_get_conditions"):
for condition_key in await platform.async_get_conditions(hass):
condition_key = get_absolute_description_key(
integration_domain, condition_key
)
hass.data[CONDITIONS][condition_key] = integration_domain
new_conditions.add(condition_key)
else:
@@ -288,22 +291,21 @@ def trace_condition_function(condition: ConditionCheckerType) -> ConditionChecke
async def _async_get_condition_platform(
hass: HomeAssistant, config: ConfigType
) -> ConditionProtocol | None:
condition_key: str = config[CONF_CONDITION]
platform_and_sub_type = condition_key.partition(".")
hass: HomeAssistant, condition_key: str
) -> tuple[str, ConditionProtocol | None]:
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
return "", None
try:
integration = await async_get_integration(hass, platform)
except IntegrationNotFound:
raise HomeAssistantError(
f'Invalid condition "{condition_key}" specified {config}'
f'Invalid condition "{condition_key}" specified'
) from None
try:
return await integration.async_get_platform("condition")
return platform, await integration.async_get_platform("condition")
except ImportError:
raise HomeAssistantError(
f"Integration '{platform}' does not provide condition support"
@@ -339,17 +341,20 @@ async def async_from_config(
return disabled_condition
condition: str = config[CONF_CONDITION]
condition_key: str = config[CONF_CONDITION]
factory: Any = None
platform = await _async_get_condition_platform(hass, config)
platform_domain, platform = await _async_get_condition_platform(hass, condition_key)
if platform is not None:
condition_descriptors = await platform.async_get_conditions(hass)
condition_instance = condition_descriptors[condition](hass, config)
relative_condition_key = get_relative_description_key(
platform_domain, condition_key
)
condition_instance = condition_descriptors[relative_condition_key](hass, config)
return await condition_instance.async_get_checker()
for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT):
factory = getattr(sys.modules[__name__], fmt.format(condition), None)
factory = getattr(sys.modules[__name__], fmt.format(condition_key), None)
if factory:
break
@@ -960,8 +965,9 @@ async def async_validate_condition_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
condition: str = config[CONF_CONDITION]
if condition in ("and", "not", "or"):
condition_key: str = config[CONF_CONDITION]
if condition_key in ("and", "not", "or"):
conditions = []
for sub_cond in config["conditions"]:
sub_cond = await async_validate_condition_config(hass, sub_cond)
@@ -969,16 +975,23 @@ async def async_validate_condition_config(
config["conditions"] = conditions
return config
platform = await _async_get_condition_platform(hass, config)
platform_domain, platform = await _async_get_condition_platform(hass, condition_key)
if platform is not None:
condition_descriptors = await platform.async_get_conditions(hass)
if not (condition_class := condition_descriptors.get(condition)):
raise vol.Invalid(f"Invalid condition '{condition}' specified")
relative_condition_key = get_relative_description_key(
platform_domain, condition_key
)
if not (condition_class := condition_descriptors.get(relative_condition_key)):
raise vol.Invalid(f"Invalid condition '{condition_key}' specified")
return await condition_class.async_validate_config(hass, config)
if platform is None and condition in ("numeric_state", "state"):
if platform is None and condition_key in ("numeric_state", "state"):
validator = cast(
Callable[[HomeAssistant, ConfigType], ConfigType],
getattr(sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition)),
getattr(
sys.modules[__name__], VALIDATE_CONFIG_FORMAT.format(condition_key)
),
)
return validator(hass, config)
@@ -1088,11 +1101,11 @@ def async_extract_devices(config: ConfigType | Template) -> set[str]:
return referenced
def _load_conditions_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
def _load_conditions_file(integration: Integration) -> dict[str, Any]:
"""Load conditions file for an integration."""
try:
return cast(
JSON_TYPE,
dict[str, Any],
_CONDITIONS_SCHEMA(
load_yaml_dict(str(integration.file_path / "conditions.yaml"))
),
@@ -1112,11 +1125,14 @@ def _load_conditions_file(hass: HomeAssistant, integration: Integration) -> JSON
def _load_conditions_files(
hass: HomeAssistant, integrations: Iterable[Integration]
) -> dict[str, JSON_TYPE]:
integrations: Iterable[Integration],
) -> dict[str, dict[str, Any]]:
"""Load condition files for multiple integrations."""
return {
integration.domain: _load_conditions_file(hass, integration)
integration.domain: {
get_absolute_description_key(integration.domain, key): value
for key, value in _load_conditions_file(integration).items()
}
for integration in integrations
}
@@ -1137,7 +1153,7 @@ async def async_get_all_descriptions(
return descriptions_cache
# Files we loaded for missing descriptions
new_conditions_descriptions: dict[str, JSON_TYPE] = {}
new_conditions_descriptions: dict[str, dict[str, Any]] = {}
# We try to avoid making a copy in the event the cache is good,
# but now we must make a copy in case new conditions get added
# while we are loading the missing ones so we do not
@@ -1166,7 +1182,7 @@ async def async_get_all_descriptions(
if integrations:
new_conditions_descriptions = await hass.async_add_executor_job(
_load_conditions_files, hass, integrations
_load_conditions_files, integrations
)
# Make a copy of the old cache and add missing descriptions to it
@@ -1175,7 +1191,7 @@ async def async_get_all_descriptions(
domain = conditions[missing_condition]
if (
yaml_description := new_conditions_descriptions.get(domain, {}).get( # type: ignore[union-attr]
yaml_description := new_conditions_descriptions.get(domain, {}).get(
missing_condition
)
) is None:

View File

@@ -47,7 +47,7 @@ CONDITION_SCHEMA = vol.Any(
CONDITIONS_SCHEMA = vol.Schema(
{
vol.Remove(vol.All(str, condition.starts_with_dot)): object,
cv.slug: CONDITION_SCHEMA,
cv.underscore_slug: CONDITION_SCHEMA,
}
)

View File

@@ -126,7 +126,7 @@ CONDITION_ICONS_SCHEMA = cv.schema_with_slug_keys(
vol.Optional("condition"): icon_value_validator,
}
),
slug_validator=translation_key_validator,
slug_validator=cv.underscore_slug,
)

View File

@@ -434,7 +434,7 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
slug_validator=translation_key_validator,
),
},
slug_validator=translation_key_validator,
slug_validator=cv.underscore_slug,
),
vol.Optional("triggers"): cv.schema_with_slug_keys(
{

View File

@@ -721,10 +721,10 @@ async def test_subscribe_conditions(
) -> None:
"""Test condition_platforms/subscribe command."""
sun_condition_descriptions = """
sun: {}
_: {}
"""
device_automation_condition_descriptions = """
device: {}
_device: {}
"""
def _load_yaml(fname, secrets=None):
@@ -2738,10 +2738,7 @@ async def test_validate_config_works(
"entity_id": "hello.world",
"state": "paulus",
},
(
"Invalid condition \"non_existing\" specified {'condition': "
"'non_existing', 'entity_id': 'hello.world', 'state': 'paulus'}"
),
'Invalid condition "non_existing" specified',
),
# Raises HomeAssistantError
(

View File

@@ -2073,7 +2073,7 @@ async def test_platform_async_get_conditions(hass: HomeAssistant) -> None:
config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test", CONF_CONDITION: "device"}
with patch(
"homeassistant.components.device_automation.condition.async_get_conditions",
AsyncMock(return_value={"device": AsyncMock()}),
AsyncMock(return_value={"_device": AsyncMock()}),
) as device_automation_async_get_conditions_mock:
await condition.async_validate_condition_config(hass, config)
device_automation_async_get_conditions_mock.assert_awaited()
@@ -2113,8 +2113,8 @@ async def test_platform_multiple_conditions(hass: HomeAssistant) -> None:
hass: HomeAssistant,
) -> dict[str, type[condition.Condition]]:
return {
"test": MockCondition1,
"test.cond_2": MockCondition2,
"_": MockCondition1,
"cond_2": MockCondition2,
}
mock_integration(hass, MockModule("test"))
@@ -2337,7 +2337,7 @@ async def test_or_condition_with_disabled_condition(hass: HomeAssistant) -> None
"sun_condition_descriptions",
[
"""
sun:
_:
fields:
after:
example: sunrise
@@ -2371,7 +2371,7 @@ async def test_or_condition_with_disabled_condition(hass: HomeAssistant) -> None
.offset_selector: &offset_selector
selector:
time: null
sun:
_:
fields:
after: *sunrise_sunset_selector
after_offset: *offset_selector
@@ -2385,7 +2385,7 @@ async def test_async_get_all_descriptions(
) -> None:
"""Test async_get_all_descriptions."""
device_automation_condition_descriptions = """
device: {}
_device: {}
"""
assert await async_setup_component(hass, DOMAIN_SUN, {})
@@ -2415,7 +2415,7 @@ async def test_async_get_all_descriptions(
# Test we only load conditions.yaml for integrations with conditions,
# system_health has no conditions
assert proxy_load_conditions_files.mock_calls[0][1][1] == unordered(
assert proxy_load_conditions_files.mock_calls[0][1][0] == unordered(
[
await async_get_integration(hass, DOMAIN_SUN),
]
@@ -2423,7 +2423,7 @@ async def test_async_get_all_descriptions(
# system_health does not have conditions and should not be in descriptions
assert descriptions == {
DOMAIN_SUN: {
"sun": {
"fields": {
"after": {
"example": "sunrise",
@@ -2459,7 +2459,7 @@ async def test_async_get_all_descriptions(
"device": {
"fields": {},
},
DOMAIN_SUN: {
"sun": {
"fields": {
"after": {
"example": "sunrise",
@@ -2525,7 +2525,7 @@ async def test_async_get_all_descriptions_with_bad_description(
) -> None:
"""Test async_get_all_descriptions."""
sun_service_descriptions = """
sun:
_:
fields: not_a_dict
"""
@@ -2545,11 +2545,11 @@ async def test_async_get_all_descriptions_with_bad_description(
):
descriptions = await condition.async_get_all_descriptions(hass)
assert descriptions == {DOMAIN_SUN: None}
assert descriptions == {"sun": None}
assert (
"Unable to parse conditions.yaml for the sun integration: "
"expected a dictionary for dictionary value @ data['sun']['fields']"
"expected a dictionary for dictionary value @ data['_']['fields']"
) in caplog.text