From 8620bef5b041db29f1658963ac26e7d344c8cbe9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery <erik@montnemery.com> Date: Tue, 11 Jun 2024 16:31:19 +0200 Subject: [PATCH] Support shared keys starting with period in services.yaml (#118789) --- homeassistant/components/light/services.yaml | 361 ++++++++++--------- homeassistant/helpers/service.py | 15 +- script/hassfest/services.py | 5 +- tests/common.py | 7 +- tests/helpers/test_service.py | 55 +++ 5 files changed, 262 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 4f9f4e03b89..6183d2a49df 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -1,4 +1,184 @@ # 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: target: @@ -15,14 +195,7 @@ turn_on: max: 300 unit_of_measurement: seconds rgb_color: &rgb_color - filter: &color_support - attribute: - supported_color_modes: - - light.ColorMode.HS - - light.ColorMode.XY - - light.ColorMode.RGB - - light.ColorMode.RGBW - - light.ColorMode.RGBWW + filter: *color_support example: "[255, 100, 100]" selector: color_rgb: @@ -44,156 +217,7 @@ turn_on: selector: select: translation_key: color_name - 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" + 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: diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 3a828ada9c2..a9959902084 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -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): diff --git a/script/hassfest/services.py b/script/hassfest/services.py index c962d84e6e1..ea4503d5410 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -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} diff --git a/tests/common.py b/tests/common.py index 3f1dea4b720..cf5469e1cd2 100644 --- a/tests/common.py +++ b/tests/common.py @@ -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: diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index c9d92c2f25a..60fe87db9d2 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -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: -- GitLab