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,4 +1,184 @@
# Describes the format for available light services # 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
.color_support: &color_support
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
.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"
- "aqua"
- "aquamarine"
- "azure"
- "beige"
- "bisque"
# Black is omitted from this list as nonsensical for lights
- "blanchedalmond"
- "blue"
- "blueviolet"
- "brown"
- "burlywood"
- "cadetblue"
- "chartreuse"
- "chocolate"
- "coral"
- "cornflowerblue"
- "cornsilk"
- "crimson"
- "cyan"
- "darkblue"
- "darkcyan"
- "darkgoldenrod"
- "darkgray"
- "darkgreen"
- "darkgrey"
- "darkkhaki"
- "darkmagenta"
- "darkolivegreen"
- "darkorange"
- "darkorchid"
- "darkred"
- "darksalmon"
- "darkseagreen"
- "darkslateblue"
- "darkslategray"
- "darkslategrey"
- "darkturquoise"
- "darkviolet"
- "deeppink"
- "deepskyblue"
- "dimgray"
- "dimgrey"
- "dodgerblue"
- "firebrick"
- "floralwhite"
- "forestgreen"
- "fuchsia"
- "gainsboro"
- "ghostwhite"
- "gold"
- "goldenrod"
- "gray"
- "green"
- "greenyellow"
- "grey"
- "honeydew"
- "hotpink"
- "indianred"
- "indigo"
- "ivory"
- "khaki"
- "lavender"
- "lavenderblush"
- "lawngreen"
- "lemonchiffon"
- "lightblue"
- "lightcoral"
- "lightcyan"
- "lightgoldenrodyellow"
- "lightgray"
- "lightgreen"
- "lightgrey"
- "lightpink"
- "lightsalmon"
- "lightseagreen"
- "lightskyblue"
- "lightslategray"
- "lightslategrey"
- "lightsteelblue"
- "lightyellow"
- "lime"
- "limegreen"
- "linen"
- "magenta"
- "maroon"
- "mediumaquamarine"
- "mediumblue"
- "mediumorchid"
- "mediumpurple"
- "mediumseagreen"
- "mediumslateblue"
- "mediumspringgreen"
- "mediumturquoise"
- "mediumvioletred"
- "midnightblue"
- "mintcream"
- "mistyrose"
- "moccasin"
- "navajowhite"
- "navy"
- "navyblue"
- "oldlace"
- "olive"
- "olivedrab"
- "orange"
- "orangered"
- "orchid"
- "palegoldenrod"
- "palegreen"
- "paleturquoise"
- "palevioletred"
- "papayawhip"
- "peachpuff"
- "peru"
- "pink"
- "plum"
- "powderblue"
- "purple"
- "red"
- "rosybrown"
- "royalblue"
- "saddlebrown"
- "salmon"
- "sandybrown"
- "seagreen"
- "seashell"
- "sienna"
- "silver"
- "skyblue"
- "slateblue"
- "slategray"
- "slategrey"
- "snow"
- "springgreen"
- "steelblue"
- "tan"
- "teal"
- "thistle"
- "tomato"
- "turquoise"
- "violet"
- "wheat"
- "white"
- "whitesmoke"
- "yellow"
- "yellowgreen"
turn_on: turn_on:
target: target:
@ -15,14 +195,7 @@ turn_on:
max: 300 max: 300
unit_of_measurement: seconds unit_of_measurement: seconds
rgb_color: &rgb_color rgb_color: &rgb_color
filter: &color_support filter: *color_support
attribute:
supported_color_modes:
- light.ColorMode.HS
- light.ColorMode.XY
- light.ColorMode.RGB
- light.ColorMode.RGBW
- light.ColorMode.RGBWW
example: "[255, 100, 100]" example: "[255, 100, 100]"
selector: selector:
color_rgb: color_rgb:
@ -44,156 +217,7 @@ turn_on:
selector: selector:
select: select:
translation_key: color_name translation_key: color_name
options: &named_colors options: *named_colors
- "homeassistant"
- "aliceblue"
- "antiquewhite"
- "aqua"
- "aquamarine"
- "azure"
- "beige"
- "bisque"
# Black is omitted from this list as nonsensical for lights
- "blanchedalmond"
- "blue"
- "blueviolet"
- "brown"
- "burlywood"
- "cadetblue"
- "chartreuse"
- "chocolate"
- "coral"
- "cornflowerblue"
- "cornsilk"
- "crimson"
- "cyan"
- "darkblue"
- "darkcyan"
- "darkgoldenrod"
- "darkgray"
- "darkgreen"
- "darkgrey"
- "darkkhaki"
- "darkmagenta"
- "darkolivegreen"
- "darkorange"
- "darkorchid"
- "darkred"
- "darksalmon"
- "darkseagreen"
- "darkslateblue"
- "darkslategray"
- "darkslategrey"
- "darkturquoise"
- "darkviolet"
- "deeppink"
- "deepskyblue"
- "dimgray"
- "dimgrey"
- "dodgerblue"
- "firebrick"
- "floralwhite"
- "forestgreen"
- "fuchsia"
- "gainsboro"
- "ghostwhite"
- "gold"
- "goldenrod"
- "gray"
- "green"
- "greenyellow"
- "grey"
- "honeydew"
- "hotpink"
- "indianred"
- "indigo"
- "ivory"
- "khaki"
- "lavender"
- "lavenderblush"
- "lawngreen"
- "lemonchiffon"
- "lightblue"
- "lightcoral"
- "lightcyan"
- "lightgoldenrodyellow"
- "lightgray"
- "lightgreen"
- "lightgrey"
- "lightpink"
- "lightsalmon"
- "lightseagreen"
- "lightskyblue"
- "lightslategray"
- "lightslategrey"
- "lightsteelblue"
- "lightyellow"
- "lime"
- "limegreen"
- "linen"
- "magenta"
- "maroon"
- "mediumaquamarine"
- "mediumblue"
- "mediumorchid"
- "mediumpurple"
- "mediumseagreen"
- "mediumslateblue"
- "mediumspringgreen"
- "mediumturquoise"
- "mediumvioletred"
- "midnightblue"
- "mintcream"
- "mistyrose"
- "moccasin"
- "navajowhite"
- "navy"
- "navyblue"
- "oldlace"
- "olive"
- "olivedrab"
- "orange"
- "orangered"
- "orchid"
- "palegoldenrod"
- "palegreen"
- "paleturquoise"
- "palevioletred"
- "papayawhip"
- "peachpuff"
- "peru"
- "pink"
- "plum"
- "powderblue"
- "purple"
- "red"
- "rosybrown"
- "royalblue"
- "saddlebrown"
- "salmon"
- "sandybrown"
- "seagreen"
- "seashell"
- "sienna"
- "silver"
- "skyblue"
- "slateblue"
- "slategray"
- "slategrey"
- "snow"
- "springgreen"
- "steelblue"
- "tan"
- "teal"
- "thistle"
- "tomato"
- "turquoise"
- "violet"
- "wheat"
- "white"
- "whitesmoke"
- "yellow"
- "yellowgreen"
hs_color: &hs_color hs_color: &hs_color
filter: *color_support filter: *color_support
advanced: true advanced: true
@ -207,15 +231,7 @@ turn_on:
selector: selector:
object: object:
color_temp: &color_temp color_temp: &color_temp
filter: &color_temp_support 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
advanced: true advanced: true
selector: selector:
color_temp: color_temp:
@ -230,16 +246,7 @@ turn_on:
min: 2000 min: 2000
max: 6500 max: 6500
brightness: &brightness brightness: &brightness
filter: &brightness_support 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
advanced: true advanced: true
selector: selector:
number: number:

View File

@ -187,7 +187,20 @@ _SERVICE_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, 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): class ServiceParams(TypedDict):

View File

@ -78,7 +78,10 @@ CUSTOM_INTEGRATION_SERVICE_SCHEMA = vol.Any(
) )
CORE_INTEGRATION_SERVICES_SCHEMA = vol.Schema( 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( CUSTOM_INTEGRATION_SERVICES_SCHEMA = vol.Schema(
{cv.slug: CUSTOM_INTEGRATION_SERVICE_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( 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: ) -> loader.Integration:
"""Mock an integration.""" """Mock an integration."""
integration = loader.Integration( integration = loader.Integration(
@ -1442,7 +1445,7 @@ def mock_integration(
else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}", else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}",
pathlib.Path(""), pathlib.Path(""),
module.mock_manifest(), module.mock_manifest(),
set(), top_level_files,
) )
def mock_import_platform(platform_name: str) -> NoReturn: def mock_import_platform(platform_name: str) -> NoReturn:

View File

@ -3,6 +3,7 @@
import asyncio import asyncio
from collections.abc import Iterable from collections.abc import Iterable
from copy import deepcopy from copy import deepcopy
import io
from typing import Any from typing import Any
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
@ -43,13 +44,16 @@ from homeassistant.helpers import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.loader import async_get_integration from homeassistant.loader import async_get_integration
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.yaml.loader import parse_yaml
from tests.common import ( from tests.common import (
MockEntity, MockEntity,
MockModule,
MockUser, MockUser,
async_mock_service, async_mock_service,
mock_area_registry, mock_area_registry,
mock_device_registry, mock_device_registry,
mock_integration,
mock_registry, 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 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( async def test_async_get_all_descriptions_failing_integration(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: ) -> None: