Remove translations from get_services WS command

This commit is contained in:
Erik 2025-06-19 10:05:34 +02:00
parent be53ad5449
commit 717d550e1f
2 changed files with 73 additions and 114 deletions

View File

@ -61,7 +61,6 @@ from . import (
floor_registry, floor_registry,
label_registry, label_registry,
template, template,
translation,
) )
from .group import expand_entity_ids from .group import expand_entity_ids
from .selector import TargetSelector from .selector import TargetSelector
@ -751,11 +750,6 @@ async def async_get_all_descriptions(
_load_services_files, hass, integrations _load_services_files, hass, integrations
) )
# Load translations for all service domains
translations = await translation.async_get_translations(
hass, "en", "services", services
)
# Build response # Build response
descriptions: dict[str, dict[str, Any]] = {} descriptions: dict[str, dict[str, Any]] = {}
for domain, services_map in services.items(): for domain, services_map in services.items():
@ -782,40 +776,11 @@ async def async_get_all_descriptions(
# Don't warn for missing services, because it triggers false # Don't warn for missing services, because it triggers false
# positives for things like scripts, that register as a service # positives for things like scripts, that register as a service
# description = {"fields": yaml_description.get("fields", {})}
# 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 item in ("description", "name", "target"):
for field_name, field_schema in description["fields"].items(): if item in yaml_description:
if name := translations.get( description[item] = yaml_description[item]
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 example := translations.get(
f"component.{domain}.services.{service_name}.fields.{field_name}.example"
):
field_schema["example"] = example
if "target" in yaml_description:
description["target"] = yaml_description["target"]
response = service.supports_response response = service.supports_response
if response is not SupportsResponse.NONE: if response is not SupportsResponse.NONE:

View File

@ -4,7 +4,7 @@ import asyncio
from collections.abc import Iterable from collections.abc import Iterable
from copy import deepcopy from copy import deepcopy
import io import io
from typing import Any import threading
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import pytest import pytest
@ -43,14 +43,13 @@ from homeassistant.helpers import (
entity_registry as er, entity_registry as er,
service, service,
) )
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import ( from homeassistant.loader import (
Integration, Integration,
async_get_integration, async_get_integration,
async_get_integrations, async_get_integrations,
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.yaml.loader import parse_yaml from homeassistant.util.yaml.loader import JSON_TYPE, parse_yaml
from tests.common import ( from tests.common import (
MockEntity, MockEntity,
@ -831,7 +830,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
assert len(descriptions) == 1 assert len(descriptions) == 1
assert DOMAIN_GROUP in descriptions assert DOMAIN_GROUP in descriptions
assert "description" in descriptions[DOMAIN_GROUP]["reload"] assert "description" not in descriptions[DOMAIN_GROUP]["reload"]
assert "fields" in descriptions[DOMAIN_GROUP]["reload"] assert "fields" in descriptions[DOMAIN_GROUP]["reload"]
# Does not have services # Does not have services
@ -839,26 +838,39 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
logger_config = {DOMAIN_LOGGER: {}} logger_config = {DOMAIN_LOGGER: {}}
async def async_get_translations( # Test legacy service with translations in services.yaml
hass: HomeAssistant, def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
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.{DOMAIN_LOGGER}.services.set_default_level"
return { return {
f"{translation_key_prefix}.name": "Translated name", "set_default_level": {
f"{translation_key_prefix}.description": "Translated description", "description": "Translated description",
f"{translation_key_prefix}.fields.level.name": "Field name", "fields": {
f"{translation_key_prefix}.fields.level.description": "Field description", "level": {
f"{translation_key_prefix}.fields.level.example": "Field example", "description": "Field description",
"example": "Field example",
"name": "Field name",
"selector": {
"select": {
"options": [
"debug",
"info",
"warning",
"error",
"fatal",
"critical",
],
"translation_key": "level",
}
},
}
},
"name": "Translated name",
},
"set_level": None,
} }
with patch( with patch(
"homeassistant.helpers.service.translation.async_get_translations", "homeassistant.helpers.service._load_services_file",
side_effect=async_get_translations, side_effect=_load_services_file,
): ):
await async_setup_component(hass, DOMAIN_LOGGER, logger_config) await async_setup_component(hass, DOMAIN_LOGGER, logger_config)
descriptions = await service.async_get_all_descriptions(hass) descriptions = await service.async_get_all_descriptions(hass)
@ -985,9 +997,7 @@ async def test_async_get_all_descriptions_dot_keys(hass: HomeAssistant) -> None:
assert descriptions == { assert descriptions == {
"test_domain": { "test_domain": {
"test_service": { "test_service": {
"description": "",
"fields": {"test": {"selector": {"text": None}}}, "fields": {"test": {"selector": {"text": None}}},
"name": "",
} }
} }
} }
@ -1055,7 +1065,6 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None:
) )
test_service_schema = { test_service_schema = {
"description": "",
"fields": { "fields": {
"advanced_stuff": { "advanced_stuff": {
"fields": { "fields": {
@ -1076,7 +1085,6 @@ async def test_async_get_all_descriptions_filter(hass: HomeAssistant) -> None:
"selector": {"number": None}, "selector": {"number": None},
}, },
}, },
"name": "",
"target": { "target": {
"entity": [ "entity": [
{ {
@ -1112,31 +1120,11 @@ async def test_async_get_all_descriptions_failing_integration(
integrations[DOMAIN_LOGGER] = ImportError("Failed to load services.yaml") integrations[DOMAIN_LOGGER] = ImportError("Failed to load services.yaml")
return integrations return integrations
async def wrap_get_translations(
hass: HomeAssistant,
language: str,
category: str,
integrations: Iterable[str] | None = None,
config_flow: bool | None = None,
) -> dict[str, str]:
translations = await async_get_translations(
hass, language, category, integrations, config_flow
)
return {
key: value
for key, value in translations.items()
if not key.startswith("component.logger.services.")
}
with ( with (
patch( patch(
"homeassistant.helpers.service.async_get_integrations", "homeassistant.helpers.service.async_get_integrations",
wraps=wrap_get_integrations, wraps=wrap_get_integrations,
), ),
patch(
"homeassistant.helpers.service.translation.async_get_translations",
wrap_get_translations,
),
): ):
descriptions = await service.async_get_all_descriptions(hass) descriptions = await service.async_get_all_descriptions(hass)
@ -1145,16 +1133,12 @@ async def test_async_get_all_descriptions_failing_integration(
# Services are empty defaults if the load fails but should # Services are empty defaults if the load fails but should
# not raise # not raise
assert descriptions[DOMAIN_GROUP]["remove"]["description"] assert "description" not in descriptions[DOMAIN_GROUP]["remove"]
assert descriptions[DOMAIN_GROUP]["remove"]["fields"] assert descriptions[DOMAIN_GROUP]["remove"]["fields"]
assert descriptions[DOMAIN_LOGGER]["set_level"] == { assert descriptions[DOMAIN_LOGGER]["set_level"] == {"fields": {}}
"description": "",
"fields": {},
"name": "",
}
assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["description"] assert "description" not in descriptions[DOMAIN_INPUT_BUTTON]["press"]
assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["fields"] == {} assert descriptions[DOMAIN_INPUT_BUTTON]["press"]["fields"] == {}
assert "target" in descriptions[DOMAIN_INPUT_BUTTON]["press"] assert "target" in descriptions[DOMAIN_INPUT_BUTTON]["press"]
@ -1209,7 +1193,7 @@ async def test_async_get_all_descriptions_dynamically_created_services(
assert len(descriptions) == 1 assert len(descriptions) == 1
assert "description" in descriptions["group"]["reload"] assert "description" not in descriptions["group"]["reload"]
assert "fields" in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"]
shell_command_config = {DOMAIN_SHELL_COMMAND: {"test_service": "ls /bin"}} shell_command_config = {DOMAIN_SHELL_COMMAND: {"test_service": "ls /bin"}}
@ -1218,9 +1202,7 @@ async def test_async_get_all_descriptions_dynamically_created_services(
assert len(descriptions) == 2 assert len(descriptions) == 2
assert descriptions[DOMAIN_SHELL_COMMAND]["test_service"] == { assert descriptions[DOMAIN_SHELL_COMMAND]["test_service"] == {
"description": "",
"fields": {}, "fields": {},
"name": "",
"response": {"optional": True}, "response": {"optional": True},
} }
@ -1235,41 +1217,53 @@ async def test_async_get_all_descriptions_new_service_added_while_loading(
assert len(descriptions) == 1 assert len(descriptions) == 1
assert "description" in descriptions["group"]["reload"] assert "description" not in descriptions["group"]["reload"]
assert "fields" in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"]
logger_domain = DOMAIN_LOGGER logger_domain = DOMAIN_LOGGER
logger_config = {logger_domain: {}} logger_config = {logger_domain: {}}
translations_called = asyncio.Event() translations_called = threading.Event()
translations_wait = asyncio.Event() translations_wait = threading.Event()
async def async_get_translations( def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
hass: HomeAssistant,
language: str,
category: str,
integrations: Iterable[str] | None = None,
config_flow: bool | None = None,
) -> dict[str, Any]:
"""Return all backend translations."""
translations_called.set() translations_called.set()
await translations_wait.wait() translations_wait.wait()
translation_key_prefix = f"component.{logger_domain}.services.set_default_level"
return { return {
f"{translation_key_prefix}.name": "Translated name", "set_default_level": {
f"{translation_key_prefix}.description": "Translated description", "description": "Translated description",
f"{translation_key_prefix}.fields.level.name": "Field name", "fields": {
f"{translation_key_prefix}.fields.level.description": "Field description", "level": {
f"{translation_key_prefix}.fields.level.example": "Field example", "description": "Field description",
"example": "Field example",
"name": "Field name",
"selector": {
"select": {
"options": [
"debug",
"info",
"warning",
"error",
"fatal",
"critical",
],
"translation_key": "level",
}
},
}
},
"name": "Translated name",
},
"set_level": None,
} }
with patch( with patch(
"homeassistant.helpers.service.translation.async_get_translations", "homeassistant.helpers.service._load_services_file",
side_effect=async_get_translations, side_effect=_load_services_file,
): ):
await async_setup_component(hass, logger_domain, logger_config) await async_setup_component(hass, logger_domain, logger_config)
task = asyncio.create_task(service.async_get_all_descriptions(hass)) task = asyncio.create_task(service.async_get_all_descriptions(hass))
await translations_called.wait() await hass.async_add_executor_job(translations_called.wait)
# Now register a new service while translations are being loaded # Now register a new service while translations are being loaded
hass.services.async_register(logger_domain, "new_service", lambda x: None, None) hass.services.async_register(logger_domain, "new_service", lambda x: None, None)
service.async_set_service_schema( service.async_set_service_schema(