Add support for service translations (#95984)

This commit is contained in:
Franck Nijhof 2023-07-11 15:52:12 +02:00 committed by GitHub
parent f12f8bca03
commit f054de0ad5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 483 additions and 119 deletions

View File

@ -1,17 +1,11 @@
# Describes the format for available light services
turn_on:
name: Turn on
description: >
Turn on one or more lights and adjust properties of the light, even when
they are turned on already.
target:
entity:
domain: light
fields:
transition:
name: Transition
description: Duration it takes to get to next state.
filter:
supported_features:
- light.LightEntityFeature.TRANSITION
@ -21,8 +15,6 @@ turn_on:
max: 300
unit_of_measurement: seconds
rgb_color:
name: Color
description: The color for the light (based on RGB - red, green, blue).
filter:
attribute:
supported_color_modes:
@ -34,8 +26,6 @@ turn_on:
selector:
color_rgb:
rgbw_color:
name: RGBW-color
description: A list containing four integers between 0 and 255 representing the RGBW (red, green, blue, white) color for the light.
filter:
attribute:
supported_color_modes:
@ -49,8 +39,6 @@ turn_on:
selector:
object:
rgbww_color:
name: RGBWW-color
description: A list containing five integers between 0 and 255 representing the RGBWW (red, green, blue, cold white, warm white) color for the light.
filter:
attribute:
supported_color_modes:
@ -64,8 +52,6 @@ turn_on:
selector:
object:
color_name:
name: Color name
description: A human readable color name.
filter:
attribute:
supported_color_modes:
@ -77,6 +63,7 @@ turn_on:
advanced: true
selector:
select:
translation_key: color_name
options:
- "homeassistant"
- "aliceblue"
@ -228,8 +215,6 @@ turn_on:
- "yellow"
- "yellowgreen"
hs_color:
name: Hue/Sat color
description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
filter:
attribute:
supported_color_modes:
@ -243,8 +228,6 @@ turn_on:
selector:
object:
xy_color:
name: XY-color
description: Color for the light in XY-format.
filter:
attribute:
supported_color_modes:
@ -258,8 +241,6 @@ turn_on:
selector:
object:
color_temp:
name: Color temperature
description: Color temperature for the light in mireds.
filter:
attribute:
supported_color_modes:
@ -274,8 +255,6 @@ turn_on:
min_mireds: 153
max_mireds: 500
kelvin:
name: Color temperature (Kelvin)
description: Color temperature for the light in Kelvin.
filter:
attribute:
supported_color_modes:
@ -293,10 +272,6 @@ turn_on:
step: 100
unit_of_measurement: K
brightness:
name: Brightness value
description: Number indicating brightness, where 0 turns the light
off, 1 is the minimum brightness and 255 is the maximum brightness
supported by the light.
filter:
attribute:
supported_color_modes:
@ -313,10 +288,6 @@ turn_on:
min: 0
max: 255
brightness_pct:
name: Brightness
description: Number indicating percentage of full brightness, where 0
turns the light off, 1 is the minimum brightness and 100 is the maximum
brightness supported by the light.
filter:
attribute:
supported_color_modes:
@ -333,8 +304,6 @@ turn_on:
max: 100
unit_of_measurement: "%"
brightness_step:
name: Brightness step value
description: Change brightness by an amount.
filter:
attribute:
supported_color_modes:
@ -351,8 +320,6 @@ turn_on:
min: -225
max: 255
brightness_step_pct:
name: Brightness step
description: Change brightness by a percentage.
filter:
attribute:
supported_color_modes:
@ -369,8 +336,6 @@ turn_on:
max: 100
unit_of_measurement: "%"
white:
name: White
description: Set the light to white mode.
filter:
attribute:
supported_color_modes:
@ -381,15 +346,11 @@ turn_on:
value: true
label: Enabled
profile:
name: Profile
description: Name of a light profile to use.
advanced: true
example: relax
selector:
text:
flash:
name: Flash
description: If the light should flash.
filter:
supported_features:
- light.LightEntityFeature.FLASH
@ -402,8 +363,6 @@ turn_on:
- label: "Short"
value: "short"
effect:
name: Effect
description: Light effect.
filter:
supported_features:
- light.LightEntityFeature.EFFECT
@ -411,15 +370,11 @@ turn_on:
text:
turn_off:
name: Turn off
description: Turns off one or more lights.
target:
entity:
domain: light
fields:
transition:
name: Transition
description: Duration it takes to get to next state.
filter:
supported_features:
- light.LightEntityFeature.TRANSITION
@ -429,8 +384,6 @@ turn_off:
max: 300
unit_of_measurement: seconds
flash:
name: Flash
description: If the light should flash.
filter:
supported_features:
- light.LightEntityFeature.FLASH
@ -444,17 +397,11 @@ turn_off:
value: "short"
toggle:
name: Toggle
description: >
Toggles one or more lights, from on to off, or, off to on, based on their
current state.
target:
entity:
domain: light
fields:
transition:
name: Transition
description: Duration it takes to get to next state.
filter:
supported_features:
- light.LightEntityFeature.TRANSITION
@ -464,8 +411,6 @@ toggle:
max: 300
unit_of_measurement: seconds
rgb_color:
name: RGB-color
description: Color for the light in RGB-format.
filter:
attribute:
supported_color_modes:
@ -479,8 +424,6 @@ toggle:
selector:
object:
color_name:
name: Color name
description: A human readable color name.
filter:
attribute:
supported_color_modes:
@ -492,6 +435,7 @@ toggle:
advanced: true
selector:
select:
translation_key: color_name
options:
- "homeassistant"
- "aliceblue"
@ -643,8 +587,6 @@ toggle:
- "yellow"
- "yellowgreen"
hs_color:
name: Hue/Sat color
description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
filter:
attribute:
supported_color_modes:
@ -658,8 +600,6 @@ toggle:
selector:
object:
xy_color:
name: XY-color
description: Color for the light in XY-format.
filter:
attribute:
supported_color_modes:
@ -673,8 +613,6 @@ toggle:
selector:
object:
color_temp:
name: Color temperature (mireds)
description: Color temperature for the light in mireds.
filter:
attribute:
supported_color_modes:
@ -688,8 +626,6 @@ toggle:
selector:
color_temp:
kelvin:
name: Color temperature (Kelvin)
description: Color temperature for the light in Kelvin.
filter:
attribute:
supported_color_modes:
@ -707,10 +643,6 @@ toggle:
step: 100
unit_of_measurement: K
brightness:
name: Brightness value
description: Number indicating brightness, where 0 turns the light
off, 1 is the minimum brightness and 255 is the maximum brightness
supported by the light.
filter:
attribute:
supported_color_modes:
@ -727,10 +659,6 @@ toggle:
min: 0
max: 255
brightness_pct:
name: Brightness
description: Number indicating percentage of full brightness, where 0
turns the light off, 1 is the minimum brightness and 100 is the maximum
brightness supported by the light.
filter:
attribute:
supported_color_modes:
@ -747,8 +675,6 @@ toggle:
max: 100
unit_of_measurement: "%"
white:
name: White
description: Set the light to white mode.
filter:
attribute:
supported_color_modes:
@ -759,15 +685,11 @@ toggle:
value: true
label: Enabled
profile:
name: Profile
description: Name of a light profile to use.
advanced: true
example: relax
selector:
text:
flash:
name: Flash
description: If the light should flash.
filter:
supported_features:
- light.LightEntityFeature.FLASH
@ -780,8 +702,6 @@ toggle:
- label: "Short"
value: "short"
effect:
name: Effect
description: Light effect.
filter:
supported_features:
- light.LightEntityFeature.EFFECT

View File

@ -86,5 +86,307 @@
}
}
}
},
"selector": {
"color_name": {
"options": {
"homeassistant": "Home Assistant",
"aliceblue": "Alice blue",
"antiquewhite": "Antique white",
"aqua": "Aqua",
"aquamarine": "Aquamarine",
"azure": "Azure",
"beige": "Beige",
"bisque": "Bisque",
"blanchedalmond": "Blanched almond",
"blue": "Blue",
"blueviolet": "Blue violet",
"brown": "Brown",
"burlywood": "Burlywood",
"cadetblue": "Cadet blue",
"chartreuse": "Chartreuse",
"chocolate": "Chocolate",
"coral": "Coral",
"cornflowerblue": "Cornflower blue",
"cornsilk": "Cornsilk",
"crimson": "Crimson",
"cyan": "Cyan",
"darkblue": "Dark blue",
"darkcyan": "Dark cyan",
"darkgoldenrod": "Dark goldenrod",
"darkgray": "Dark gray",
"darkgreen": "Dark green",
"darkgrey": "Dark grey",
"darkkhaki": "Dark khaki",
"darkmagenta": "Dark magenta",
"darkolivegreen": "Dark olive green",
"darkorange": "Dark orange",
"darkorchid": "Dark orchid",
"darkred": "Dark red",
"darksalmon": "Dark salmon",
"darkseagreen": "Dark sea green",
"darkslateblue": "Dark slate blue",
"darkslategray": "Dark slate gray",
"darkslategrey": "Dark slate grey",
"darkturquoise": "Dark turquoise",
"darkviolet": "Dark violet",
"deeppink": "Deep pink",
"deepskyblue": "Deep sky blue",
"dimgray": "Dim gray",
"dimgrey": "Dim grey",
"dodgerblue": "Dodger blue",
"firebrick": "Fire brick",
"floralwhite": "Floral white",
"forestgreen": "Forest green",
"fuchsia": "Fuchsia",
"gainsboro": "Gainsboro",
"ghostwhite": "Ghost white",
"gold": "Gold",
"goldenrod": "Goldenrod",
"gray": "Gray",
"green": "Green",
"greenyellow": "Green yellow",
"grey": "Grey",
"honeydew": "Honeydew",
"hotpink": "Hot pink",
"indianred": "Indian red",
"indigo": "Indigo",
"ivory": "Ivory",
"khaki": "Khaki",
"lavender": "Lavender",
"lavenderblush": "Lavender blush",
"lawngreen": "Lawn green",
"lemonchiffon": "Lemon chiffon",
"lightblue": "Light blue",
"lightcoral": "Light coral",
"lightcyan": "Light cyan",
"lightgoldenrodyellow": "Light goldenrod yellow",
"lightgray": "Light gray",
"lightgreen": "Light green",
"lightgrey": "Light grey",
"lightpink": "Light pink",
"lightsalmon": "Light salmon",
"lightseagreen": "Light sea green",
"lightskyblue": "Light sky blue",
"lightslategray": "Light slate gray",
"lightslategrey": "Light slate grey",
"lightsteelblue": "Light steel blue",
"lightyellow": "Light yellow",
"lime": "Lime",
"limegreen": "Lime green",
"linen": "Linen",
"magenta": "Magenta",
"maroon": "Maroon",
"mediumaquamarine": "Medium aquamarine",
"mediumblue": "Medium blue",
"mediumorchid": "Medium orchid",
"mediumpurple": "Medium purple",
"mediumseagreen": "Medium sea green",
"mediumslateblue": "Medium slate blue",
"mediumspringgreen": "Medium spring green",
"mediumturquoise": "Medium turquoise",
"mediumvioletred": "Medium violet red",
"midnightblue": "Midnight blue",
"mintcream": "Mint cream",
"mistyrose": "Misty rose",
"moccasin": "Moccasin",
"navajowhite": "Navajo white",
"navy": "Navy",
"navyblue": "Navy blue",
"oldlace": "Old lace",
"olive": "Olive",
"olivedrab": "Olive drab",
"orange": "Orange",
"orangered": "Orange red",
"orchid": "Orchid",
"palegoldenrod": "Pale goldenrod",
"palegreen": "Pale green",
"paleturquoise": "Pale turquoise",
"palevioletred": "Pale violet red",
"papayawhip": "Papaya whip",
"peachpuff": "Peach puff",
"peru": "Peru",
"pink": "Pink",
"plum": "Plum",
"powderblue": "Powder blue",
"purple": "Purple",
"red": "Red",
"rosybrown": "Rosy brown",
"royalblue": "Royal blue",
"saddlebrown": "Saddle brown",
"salmon": "Salmon",
"sandybrown": "Sandy brown",
"seagreen": "Sea green",
"seashell": "Seashell",
"sienna": "Sienna",
"silver": "Silver",
"skyblue": "Sky blue",
"slateblue": "Slate blue",
"slategray": "Slate gray",
"slategrey": "Slate grey",
"snow": "Snow",
"springgreen": "Spring green",
"steelblue": "Steel blue",
"tan": "Tan",
"teal": "Teal",
"thistle": "Thistle",
"tomato": "Tomato",
"turquoise": "Turquoise",
"violet": "Violet",
"wheat": "Wheat",
"white": "White",
"whitesmoke": "White smoke",
"yellow": "Yellow",
"yellowgreen": "Yellow green"
}
}
},
"services": {
"turn_on": {
"name": "Turn on",
"description": "Turn on one or more lights and adjust properties of the light, even when they are turned on already.",
"fields": {
"transition": {
"name": "Transition",
"description": "Duration it takes to get to next state."
},
"rgb_color": {
"name": "Color",
"description": "The color in RGB format. A list of three integers between 0 and 255 representing the values of red, green, and blue."
},
"rgbw_color": {
"name": "RGBW-color",
"description": "The color in RGBW format. A list of four integers between 0 and 255 representing the values of red, green, blue, and white."
},
"rgbww_color": {
"name": "RGBWW-color",
"description": "The color in RGBWW format. A list of five integers between 0 and 255 representing the values of red, green, blue, cold white, and warm white."
},
"color_name": {
"name": "Color name",
"description": "A human readable color name."
},
"hs_color": {
"name": "Hue/Sat color",
"description": "Color in hue/sat format. A list of two integers. Hue is 0-360 and Sat is 0-100."
},
"xy_color": {
"name": "XY-color",
"description": "Color in XY-format. A list of two decimal numbers between 0 and 1."
},
"color_temp": {
"name": "Color temperature",
"description": "Color temperature in mireds."
},
"kelvin": {
"name": "Color temperature",
"description": "Color temperature in Kelvin."
},
"brightness": {
"name": "Brightness value",
"description": "Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness, and 255 is the maximum brightness."
},
"brightness_pct": {
"name": "Brightness",
"description": "Number indicating the percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness, and 100 is the maximum brightness."
},
"brightness_step": {
"name": "Brightness step value",
"description": "Change brightness by an amount."
},
"brightness_step_pct": {
"name": "Brightness step",
"description": "Change brightness by a percentage."
},
"white": {
"name": "White",
"description": "Set the light to white mode."
},
"profile": {
"name": "Profile",
"description": "Name of a light profile to use."
},
"flash": {
"name": "Flash",
"description": "If the light should flash."
},
"effect": {
"name": "Effect",
"description": "Light effect."
}
}
},
"turn_off": {
"name": "Turn off",
"description": "Turn off one or more lights.",
"fields": {
"transition": {
"name": "[%key:component::light::services::turn_on::fields::transition::name%]",
"description": "[%key:component::light::services::turn_on::fields::transition::description%]"
},
"flash": {
"name": "[%key:component::light::services::turn_on::fields::flash::name%]",
"description": "[%key:component::light::services::turn_on::fields::flash::description%]"
}
}
},
"toggle": {
"name": "Toggle",
"description": "Toggles one or more lights, from on to off, or, off to on, based on their current state.",
"fields": {
"transition": {
"name": "[%key:component::light::services::turn_on::fields::transition::name%]",
"description": "[%key:component::light::services::turn_on::fields::transition::description%]"
},
"rgb_color": {
"name": "[%key:component::light::services::turn_on::fields::rgb_color::name%]",
"description": "[%key:component::light::services::turn_on::fields::rgb_color::description%]"
},
"color_name": {
"name": "[%key:component::light::services::turn_on::fields::color_name::name%]",
"description": "[%key:component::light::services::turn_on::fields::color_name::description%]"
},
"hs_color": {
"name": "[%key:component::light::services::turn_on::fields::hs_color::name%]",
"description": "[%key:component::light::services::turn_on::fields::hs_color::description%]"
},
"xy_color": {
"name": "[%key:component::light::services::turn_on::fields::xy_color::name%]",
"description": "[%key:component::light::services::turn_on::fields::xy_color::description%]"
},
"color_temp": {
"name": "[%key:component::light::services::turn_on::fields::color_temp::name%]",
"description": "[%key:component::light::services::turn_on::fields::color_temp::description%]"
},
"kelvin": {
"name": "[%key:component::light::services::turn_on::fields::kelvin::name%]",
"description": "[%key:component::light::services::turn_on::fields::kelvin::description%]"
},
"brightness": {
"name": "[%key:component::light::services::turn_on::fields::brightness::name%]",
"description": "[%key:component::light::services::turn_on::fields::brightness::description%]"
},
"brightness_pct": {
"name": "[%key:component::light::services::turn_on::fields::brightness_pct::name%]",
"description": "[%key:component::light::services::turn_on::fields::brightness_pct::description%]"
},
"white": {
"name": "[%key:component::light::services::turn_on::fields::white::name%]",
"description": "[%key:component::light::services::turn_on::fields::white::description%]"
},
"profile": {
"name": "[%key:component::light::services::turn_on::fields::profile::name%]",
"description": "[%key:component::light::services::turn_on::fields::profile::description%]"
},
"flash": {
"name": "[%key:component::light::services::turn_on::fields::flash::name%]",
"description": "[%key:component::light::services::turn_on::fields::flash::description%]"
},
"effect": {
"name": "[%key:component::light::services::turn_on::fields::effect::name%]",
"description": "[%key:component::light::services::turn_on::fields::effect::description%]"
}
}
}
}
}

View File

@ -50,6 +50,7 @@ from . import (
device_registry,
entity_registry,
template,
translation,
)
from .selector import TargetSelector
from .typing import ConfigType, TemplateVarsType
@ -607,6 +608,11 @@ async def async_get_all_descriptions(
)
loaded = dict(zip(missing, contents))
# Load translations for all service domains
translations = await translation.async_get_translations(
hass, "en", "services", list(services)
)
# Build response
descriptions: dict[str, dict[str, Any]] = {}
for domain, services_map in services.items():
@ -616,37 +622,62 @@ async def async_get_all_descriptions(
for service_name in services_map:
cache_key = (domain, service_name)
description = descriptions_cache.get(cache_key)
if description is not None:
domain_descriptions[service_name] = description
continue
# Cache missing descriptions
if description is None:
domain_yaml = loaded.get(domain) or {}
# The YAML may be empty for dynamically defined
# services (ie shell_command) that never call
# service.async_set_service_schema for the dynamic
# service
domain_yaml = loaded.get(domain) or {}
# The YAML may be empty for dynamically defined
# services (ie shell_command) that never call
# service.async_set_service_schema for the dynamic
# service
yaml_description = domain_yaml.get( # type: ignore[union-attr]
service_name, {}
)
yaml_description = domain_yaml.get( # type: ignore[union-attr]
service_name, {}
)
# Don't warn for missing services, because it triggers false
# positives for things like scripts, that register as a service
description = {
"name": yaml_description.get("name", ""),
"description": yaml_description.get("description", ""),
"fields": yaml_description.get("fields", {}),
# Don't warn for missing services, because it triggers false
# positives for things like scripts, that register as a service
#
# When name & description are in the translations use those;
# otherwise fallback to backwards compatible behavior from
# the time when we didn't have translations for descriptions yet.
# This mimics the behavior of the frontend.
description = {
"name": translations.get(
f"component.{domain}.services.{service_name}.name",
yaml_description.get("name", ""),
),
"description": translations.get(
f"component.{domain}.services.{service_name}.description",
yaml_description.get("description", ""),
),
"fields": dict(yaml_description.get("fields", {})),
}
# Translate fields names & descriptions as well
for field_name, field_schema in description["fields"].items():
if name := translations.get(
f"component.{domain}.services.{service_name}.fields.{field_name}.name"
):
field_schema["name"] = name
if desc := translations.get(
f"component.{domain}.services.{service_name}.fields.{field_name}.description"
):
field_schema["description"] = desc
if "target" in yaml_description:
description["target"] = yaml_description["target"]
if (
response := hass.services.supports_response(domain, service_name)
) != SupportsResponse.NONE:
description["response"] = {
"optional": response == SupportsResponse.OPTIONAL,
}
if "target" in yaml_description:
description["target"] = yaml_description["target"]
if (
response := hass.services.supports_response(domain, service_name)
) != SupportsResponse.NONE:
description["response"] = {
"optional": response == SupportsResponse.OPTIONAL,
}
descriptions_cache[cache_key] = description
descriptions_cache[cache_key] = description
domain_descriptions[service_name] = description

View File

@ -302,7 +302,7 @@ async def async_get_translations(
components = set(integrations)
elif config_flow:
components = (await async_get_config_flows(hass)) - hass.config.components
elif category in ("state", "entity_component"):
elif category in ("state", "entity_component", "services"):
components = set(hass.config.components)
else:
# Only 'state' supports merging, so remove platforms from selection

View File

@ -1,6 +1,8 @@
"""Validate dependencies."""
from __future__ import annotations
import contextlib
import json
import pathlib
import re
from typing import Any
@ -25,7 +27,7 @@ def exists(value: Any) -> Any:
FIELD_SCHEMA = vol.Schema(
{
vol.Required("description"): str,
vol.Optional("description"): str,
vol.Optional("name"): str,
vol.Optional("example"): exists,
vol.Optional("default"): exists,
@ -46,7 +48,7 @@ FIELD_SCHEMA = vol.Schema(
SERVICE_SCHEMA = vol.Schema(
{
vol.Required("description"): str,
vol.Optional("description"): str,
vol.Optional("name"): str,
vol.Optional("target"): vol.Any(selector.TargetSelector.CONFIG_SCHEMA, None),
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
@ -70,7 +72,7 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool
return False
def validate_services(integration: Integration) -> None:
def validate_services(config: Config, integration: Integration) -> None:
"""Validate services."""
try:
data = load_yaml(str(integration.path / "services.yaml"))
@ -92,15 +94,75 @@ def validate_services(integration: Integration) -> None:
return
try:
SERVICES_SCHEMA(data)
services = SERVICES_SCHEMA(data)
except vol.Invalid as err:
integration.add_error(
"services", f"Invalid services.yaml: {humanize_error(data, err)}"
)
# Try loading translation strings
if integration.core:
strings_file = integration.path / "strings.json"
else:
# For custom integrations, use the en.json file
strings_file = integration.path / "translations/en.json"
strings = {}
if strings_file.is_file():
with contextlib.suppress(ValueError):
strings = json.loads(strings_file.read_text())
# For each service in the integration, check if the description if set,
# if not, check if it's in the strings file. If not, add an error.
for service_name, service_schema in services.items():
if "name" not in service_schema:
try:
strings["services"][service_name]["name"]
except KeyError:
integration.add_error(
"services",
f"Service {service_name} has no name and is not in the translations file",
)
if "description" not in service_schema:
try:
strings["services"][service_name]["description"]
except KeyError:
integration.add_error(
"services",
f"Service {service_name} has no description and is not in the translations file",
)
# The same check is done for the description in each of the fields of the
# service schema.
for field_name, field_schema in service_schema.get("fields", {}).items():
if "description" not in field_schema:
try:
strings["services"][service_name]["fields"][field_name][
"description"
]
except KeyError:
integration.add_error(
"services",
f"Service {service_name} has a field {field_name} with no description and is not in the translations file",
)
if "selector" in field_schema:
with contextlib.suppress(KeyError):
translation_key = field_schema["selector"]["select"][
"translation_key"
]
try:
strings["selector"][translation_key]
except KeyError:
integration.add_error(
"services",
f"Service {service_name} has a field {field_name} with a selector with a translation key {translation_key} that is not in the translations file",
)
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Handle dependencies for integrations."""
# check services.yaml is cool
for integration in integrations.values():
validate_services(integration)
validate_services(config, integration)

View File

@ -326,6 +326,20 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
),
slug_validator=cv.slug,
),
vol.Optional("services"): cv.schema_with_slug_keys(
{
vol.Required("name"): translation_value_validator,
vol.Required("description"): translation_value_validator,
vol.Optional("fields"): cv.schema_with_slug_keys(
{
vol.Required("name"): str,
vol.Required("description"): translation_value_validator,
},
slug_validator=translation_key_validator,
),
},
slug_validator=translation_key_validator,
),
}
)

View File

@ -1,6 +1,8 @@
"""Test service helpers."""
from collections import OrderedDict
from collections.abc import Iterable
from copy import deepcopy
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
import pytest
@ -556,13 +558,47 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
logger = hass.components.logger
logger_config = {logger.DOMAIN: {}}
await async_setup_component(hass, logger.DOMAIN, logger_config)
descriptions = await service.async_get_all_descriptions(hass)
async def async_get_translations(
hass: HomeAssistant,
language: str,
category: str,
integrations: Iterable[str] | None = None,
config_flow: bool | None = None,
) -> dict[str, Any]:
"""Return all backend translations."""
translation_key_prefix = f"component.{logger.DOMAIN}.services.set_default_level"
return {
f"{translation_key_prefix}.name": "Translated name",
f"{translation_key_prefix}.description": "Translated description",
f"{translation_key_prefix}.fields.level.name": "Field name",
f"{translation_key_prefix}.fields.level.description": "Field description",
}
with patch(
"homeassistant.helpers.service.translation.async_get_translations",
side_effect=async_get_translations,
):
await async_setup_component(hass, logger.DOMAIN, logger_config)
descriptions = await service.async_get_all_descriptions(hass)
assert len(descriptions) == 2
assert "description" in descriptions[logger.DOMAIN]["set_level"]
assert "fields" in descriptions[logger.DOMAIN]["set_level"]
assert descriptions[logger.DOMAIN]["set_default_level"]["name"] == "Translated name"
assert (
descriptions[logger.DOMAIN]["set_default_level"]["description"]
== "Translated description"
)
assert (
descriptions[logger.DOMAIN]["set_default_level"]["fields"]["level"]["name"]
== "Field name"
)
assert (
descriptions[logger.DOMAIN]["set_default_level"]["fields"]["level"][
"description"
]
== "Field description"
)
hass.services.async_register(logger.DOMAIN, "new_service", lambda x: None, None)
service.async_set_service_schema(
@ -602,7 +638,6 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
"another_service_with_response",
{"description": "response service"},
)
descriptions = await service.async_get_all_descriptions(hass)
assert "another_new_service" in descriptions[logger.DOMAIN]
assert "service_with_optional_response" in descriptions[logger.DOMAIN]