Support shared keys starting with period in services.yaml (#118789)

This commit is contained in:
Erik Montnemery 2024-06-11 16:31:19 +02:00 committed by GitHub
parent ea571a6997
commit 8620bef5b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 262 additions and 181 deletions

View File

@ -1,21 +1,16 @@
# Describes the format for available light services
.brightness_support: &brightness_support
attribute:
supported_color_modes:
- light.ColorMode.BRIGHTNESS
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
turn_on:
target:
entity:
domain: light
fields:
transition: &transition
filter:
supported_features:
- light.LightEntityFeature.TRANSITION
selector:
number:
min: 0
max: 300
unit_of_measurement: seconds
rgb_color: &rgb_color
filter: &color_support
.color_support: &color_support
attribute:
supported_color_modes:
- light.ColorMode.HS
@ -23,28 +18,18 @@ turn_on:
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
example: "[255, 100, 100]"
selector:
color_rgb:
rgbw_color: &rgbw_color
filter: *color_support
advanced: true
example: "[255, 100, 100, 50]"
selector:
object:
rgbww_color: &rgbww_color
filter: *color_support
advanced: true
example: "[255, 100, 100, 50, 70]"
selector:
object:
color_name: &color_name
filter: *color_support
advanced: true
selector:
select:
translation_key: color_name
options: &named_colors
.color_temp_support: &color_temp_support
attribute:
supported_color_modes:
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
.named_colors: &named_colors
- "homeassistant"
- "aliceblue"
- "antiquewhite"
@ -194,6 +179,45 @@ turn_on:
- "whitesmoke"
- "yellow"
- "yellowgreen"
turn_on:
target:
entity:
domain: light
fields:
transition: &transition
filter:
supported_features:
- light.LightEntityFeature.TRANSITION
selector:
number:
min: 0
max: 300
unit_of_measurement: seconds
rgb_color: &rgb_color
filter: *color_support
example: "[255, 100, 100]"
selector:
color_rgb:
rgbw_color: &rgbw_color
filter: *color_support
advanced: true
example: "[255, 100, 100, 50]"
selector:
object:
rgbww_color: &rgbww_color
filter: *color_support
advanced: true
example: "[255, 100, 100, 50, 70]"
selector:
object:
color_name: &color_name
filter: *color_support
advanced: true
selector:
select:
translation_key: color_name
options: *named_colors
hs_color: &hs_color
filter: *color_support
advanced: true
@ -207,15 +231,7 @@ turn_on:
selector:
object:
color_temp: &color_temp
filter: &color_temp_support
attribute:
supported_color_modes:
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
filter: *color_temp_support
advanced: true
selector:
color_temp:
@ -230,16 +246,7 @@ turn_on:
min: 2000
max: 6500
brightness: &brightness
filter: &brightness_support
attribute:
supported_color_modes:
- light.ColorMode.BRIGHTNESS
- light.ColorMode.COLOR_TEMP
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
filter: *brightness_support
advanced: true
selector:
number:

View File

@ -187,7 +187,20 @@ _SERVICE_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
_SERVICES_SCHEMA = vol.Schema({cv.slug: vol.Any(None, _SERVICE_SCHEMA)})
def starts_with_dot(key: str) -> str:
"""Check if key starts with dot."""
if not key.startswith("."):
raise vol.Invalid("Key does not start with .")
return key
_SERVICES_SCHEMA = vol.Schema(
{
vol.Remove(vol.All(str, starts_with_dot)): object,
cv.slug: vol.Any(None, _SERVICE_SCHEMA),
}
)
class ServiceParams(TypedDict):

View File

@ -78,7 +78,10 @@ CUSTOM_INTEGRATION_SERVICE_SCHEMA = vol.Any(
)
CORE_INTEGRATION_SERVICES_SCHEMA = vol.Schema(
{cv.slug: CORE_INTEGRATION_SERVICE_SCHEMA}
{
vol.Remove(vol.All(str, service.starts_with_dot)): object,
cv.slug: CORE_INTEGRATION_SERVICE_SCHEMA,
}
)
CUSTOM_INTEGRATION_SERVICES_SCHEMA = vol.Schema(
{cv.slug: CUSTOM_INTEGRATION_SERVICE_SCHEMA}

View File

@ -1432,7 +1432,10 @@ def mock_config_flow(domain: str, config_flow: type[ConfigFlow]) -> None:
def mock_integration(
hass: HomeAssistant, module: MockModule, built_in: bool = True
hass: HomeAssistant,
module: MockModule,
built_in: bool = True,
top_level_files: set[str] | None = None,
) -> loader.Integration:
"""Mock an integration."""
integration = loader.Integration(
@ -1442,7 +1445,7 @@ def mock_integration(
else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}",
pathlib.Path(""),
module.mock_manifest(),
set(),
top_level_files,
)
def mock_import_platform(platform_name: str) -> NoReturn:

View File

@ -3,6 +3,7 @@
import asyncio
from collections.abc import Iterable
from copy import deepcopy
import io
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
@ -43,13 +44,16 @@ from homeassistant.helpers import (
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import async_get_integration
from homeassistant.setup import async_setup_component
from homeassistant.util.yaml.loader import parse_yaml
from tests.common import (
MockEntity,
MockModule,
MockUser,
async_mock_service,
mock_area_registry,
mock_device_registry,
mock_integration,
mock_registry,
)
@ -916,6 +920,57 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
assert await service.async_get_all_descriptions(hass) is descriptions
async def test_async_get_all_descriptions_dot_keys(hass: HomeAssistant) -> None:
"""Test async_get_all_descriptions with keys starting with a period."""
service_descriptions = """
.anchor: &anchor
selector:
text:
test_service:
fields:
test: *anchor
"""
domain = "test_domain"
hass.services.async_register(domain, "test_service", lambda call: None)
mock_integration(hass, MockModule(domain), top_level_files={"services.yaml"})
assert await async_setup_component(hass, domain, {})
def load_yaml(fname, secrets=None):
with io.StringIO(service_descriptions) as file:
return parse_yaml(file)
with (
patch(
"homeassistant.helpers.service._load_services_files",
side_effect=service._load_services_files,
) as proxy_load_services_files,
patch(
"homeassistant.util.yaml.loader.load_yaml",
side_effect=load_yaml,
) as mock_load_yaml,
):
descriptions = await service.async_get_all_descriptions(hass)
mock_load_yaml.assert_called_once_with("services.yaml", None)
assert proxy_load_services_files.mock_calls[0][1][1] == unordered(
[
await async_get_integration(hass, domain),
]
)
assert descriptions == {
"test_domain": {
"test_service": {
"description": "",
"fields": {"test": {"selector": {"text": None}}},
"name": "",
}
}
}
async def test_async_get_all_descriptions_failing_integration(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: