From 2cb0249f0a5bc08fac2da1309d4c1598e7f60f38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Feb 2024 12:13:23 -0600 Subject: [PATCH] Add filter to translation event listeners to avoid creating tasks (#110732) --- homeassistant/helpers/translation.py | 60 ++++++++++++++++++++++------ tests/helpers/test_translation.py | 11 ++++- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index d231b479dab..3334dc9a62f 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Iterable, Mapping import logging import string -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.const import ( EVENT_COMPONENT_LOADED, @@ -205,6 +205,11 @@ class _TranslationCache: self.cache: dict[str, dict[str, dict[str, dict[str, str]]]] = {} self.lock = asyncio.Lock() + @callback + def async_is_loaded(self, language: str, components: set[str]) -> bool: + """Return if the given components are loaded for the language.""" + return components.issubset(self.loaded.get(language, set())) + async def async_load( self, language: str, @@ -465,20 +470,41 @@ def async_setup(hass: HomeAssistant) -> None: Listeners load translations for every loaded component and after config change. """ + cache = _TranslationCache(hass) + current_language = hass.config.language + hass.data[TRANSLATION_FLATTEN_CACHE] = cache - hass.data[TRANSLATION_FLATTEN_CACHE] = _TranslationCache(hass) + @callback + def _async_load_translations_filter(event: Event) -> bool: + """Filter out unwanted events.""" + nonlocal current_language + if ( + new_language := event.data.get("language") + ) and new_language != current_language: + current_language = new_language + return True + return False - async def load_translations(event: Event) -> None: - if "language" in event.data: - language = hass.config.language - _LOGGER.debug("Loading translations for language: %s", language) - await _async_load_state_translations_to_cache(hass, language, None) + async def _async_load_translations(event: Event) -> None: + new_language = event.data["language"] + _LOGGER.debug("Loading translations for language: %s", new_language) + await _async_load_state_translations_to_cache(hass, new_language, None) - async def load_translations_for_component(event: Event) -> None: - component = event.data.get("component") + @callback + def _async_load_translations_for_component_filter(event: Event) -> bool: + """Filter out unwanted events.""" + component: str | None = event.data.get("component") # Platforms don't have their own translations, skip them - if component is None or "." in str(component): - return + return bool( + component + and "." not in component + and not cache.async_is_loaded(hass.config.language, {component}) + ) + + async def _async_load_translations_for_component(event: Event) -> None: + component: str | None = event.data.get("component") + if TYPE_CHECKING: + assert component is not None language = hass.config.language _LOGGER.debug( "Loading translations for language: %s and component: %s", @@ -487,8 +513,16 @@ def async_setup(hass: HomeAssistant) -> None: ) await _async_load_state_translations_to_cache(hass, language, component) - hass.bus.async_listen(EVENT_COMPONENT_LOADED, load_translations_for_component) - hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, load_translations) + hass.bus.async_listen( + EVENT_COMPONENT_LOADED, + _async_load_translations_for_component, + event_filter=_async_load_translations_for_component_filter, + ) + hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, + _async_load_translations, + event_filter=_async_load_translations_filter, + ) @callback diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index c4cd615239c..22bb23f7130 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -602,12 +602,21 @@ async def test_setup(hass: HomeAssistant): await hass.async_block_till_done() mock.assert_not_called() + # Should not be called if the language is the current language with patch( "homeassistant.helpers.translation._async_load_state_translations_to_cache", ) as mock: hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {"language": "en"}) await hass.async_block_till_done() - mock.assert_called_once_with(hass, hass.config.language, None) + mock.assert_not_called() + + # Should be called if the language is different + with patch( + "homeassistant.helpers.translation._async_load_state_translations_to_cache", + ) as mock: + hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {"language": "es"}) + await hass.async_block_till_done() + mock.assert_called_once_with(hass, "es", None) with patch( "homeassistant.helpers.translation._async_load_state_translations_to_cache",