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:
@@ -80,7 +80,7 @@ class DeviceCondition(Condition):
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"device": DeviceCondition,
|
||||
"_device": DeviceCondition,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class SunCondition(Condition):
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"sun": SunCondition,
|
||||
"_": SunCondition,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ class ZoneCondition(Condition):
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"zone": ZoneCondition,
|
||||
"_": ZoneCondition,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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
|
||||
(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user