Add ServiceValidationError and translation support (#102592)

* Add ServiceValidationError

* Add translation support

* Extend translation support to HomeAssistantError

* Add translation support for ServiceNotFound exc

* Frontend translation & translation_key from caller

* Improve fallback message

* Set websocket_api as default translation_domain

* Add MQTT ServiceValidationError exception

* Follow up comments

* Revert removing gueard on translation_key

* Revert test changes to fix CI test

* Follow up comments

* Fix CI test

* Follow up

* Improve language

* Follow up comment
This commit is contained in:
Jan Bouwhuis
2023-11-06 15:45:04 +01:00
committed by GitHub
parent 5cd61a0cf4
commit 54cf7010cd
12 changed files with 206 additions and 18 deletions
@@ -22,6 +22,7 @@ from homeassistant.core import Context, Event, HomeAssistant, State, callback
from homeassistant.exceptions import (
HomeAssistantError,
ServiceNotFound,
ServiceValidationError,
TemplateError,
Unauthorized,
)
@@ -238,14 +239,53 @@ async def handle_call_service(
connection.send_result(msg["id"], {"context": context})
except ServiceNotFound as err:
if err.domain == msg["domain"] and err.service == msg["service"]:
connection.send_error(msg["id"], const.ERR_NOT_FOUND, "Service not found.")
connection.send_error(
msg["id"],
const.ERR_NOT_FOUND,
f"Service {err.domain}.{err.service} not found.",
translation_domain=err.translation_domain,
translation_key=err.translation_key,
translation_placeholders=err.translation_placeholders,
)
else:
connection.send_error(msg["id"], const.ERR_HOME_ASSISTANT_ERROR, str(err))
# The called service called another service which does not exist
connection.send_error(
msg["id"],
const.ERR_HOME_ASSISTANT_ERROR,
f"Service {err.domain}.{err.service} called service "
f"{msg['domain']}.{msg['service']} which was not found.",
translation_domain=const.DOMAIN,
translation_key="child_service_not_found",
translation_placeholders={
"domain": err.domain,
"service": err.service,
"child_domain": msg["domain"],
"child_service": msg["service"],
},
)
except vol.Invalid as err:
connection.send_error(msg["id"], const.ERR_INVALID_FORMAT, str(err))
except ServiceValidationError as err:
connection.logger.error(err)
connection.logger.debug("", exc_info=err)
connection.send_error(
msg["id"],
const.ERR_SERVICE_VALIDATION_ERROR,
f"Validation error: {err}",
translation_domain=err.translation_domain,
translation_key=err.translation_key,
translation_placeholders=err.translation_placeholders,
)
except HomeAssistantError as err:
connection.logger.exception(err)
connection.send_error(msg["id"], const.ERR_HOME_ASSISTANT_ERROR, str(err))
connection.send_error(
msg["id"],
const.ERR_HOME_ASSISTANT_ERROR,
str(err),
translation_domain=err.translation_domain,
translation_key=err.translation_key,
translation_placeholders=err.translation_placeholders,
)
except Exception as err: # pylint: disable=broad-except
connection.logger.exception(err)
connection.send_error(msg["id"], const.ERR_UNKNOWN_ERROR, str(err))
@@ -134,9 +134,26 @@ class ActiveConnection:
self.send_message(messages.event_message(msg_id, event))
@callback
def send_error(self, msg_id: int, code: str, message: str) -> None:
"""Send a error message."""
self.send_message(messages.error_message(msg_id, code, message))
def send_error(
self,
msg_id: int,
code: str,
message: str,
translation_key: str | None = None,
translation_domain: str | None = None,
translation_placeholders: dict[str, Any] | None = None,
) -> None:
"""Send an error message."""
self.send_message(
messages.error_message(
msg_id,
code,
message,
translation_key=translation_key,
translation_domain=translation_domain,
translation_placeholders=translation_placeholders,
)
)
@callback
def async_handle_binary(self, handler_id: int, payload: bytes) -> None:
@@ -32,6 +32,7 @@ ERR_NOT_ALLOWED: Final = "not_allowed"
ERR_NOT_FOUND: Final = "not_found"
ERR_NOT_SUPPORTED: Final = "not_supported"
ERR_HOME_ASSISTANT_ERROR: Final = "home_assistant_error"
ERR_SERVICE_VALIDATION_ERROR: Final = "service_validation_error"
ERR_UNKNOWN_COMMAND: Final = "unknown_command"
ERR_UNKNOWN_ERROR: Final = "unknown_error"
ERR_UNAUTHORIZED: Final = "unauthorized"
@@ -65,12 +65,29 @@ def construct_result_message(iden: int, payload: str) -> str:
return f'{{"id":{iden},"type":"result","success":true,"result":{payload}}}'
def error_message(iden: int | None, code: str, message: str) -> dict[str, Any]:
def error_message(
iden: int | None,
code: str,
message: str,
translation_key: str | None = None,
translation_domain: str | None = None,
translation_placeholders: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Return an error result message."""
error_payload: dict[str, Any] = {
"code": code,
"message": message,
}
# In case `translation_key` is `None` we do not set it, nor the
# `translation`_placeholders` and `translation_domain`.
if translation_key is not None:
error_payload["translation_key"] = translation_key
error_payload["translation_placeholders"] = translation_placeholders
error_payload["translation_domain"] = translation_domain
return {
"id": iden,
**BASE_ERROR_MESSAGE,
"error": {"code": code, "message": message},
"error": error_payload,
}
@@ -0,0 +1,7 @@
{
"exceptions": {
"child_service_not_found": {
"message": "Service {domain}.{service} called service {child_domain}.{child_service} which was not found."
}
}
}