"""Icon helper methods.""" from __future__ import annotations import asyncio from collections.abc import Iterable from functools import lru_cache import logging import pathlib from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.loader import Integration, async_get_integrations from homeassistant.util.hass_dict import HassKey from homeassistant.util.json import load_json_object from .translation import build_resources ICON_CACHE: HassKey[_IconsCache] = HassKey("icon_cache") _LOGGER = logging.getLogger(__name__) def _load_icons_files( icons_files: dict[str, pathlib.Path], ) -> dict[str, dict[str, Any]]: """Load and parse icons.json files.""" return { component: load_json_object(icons_file) for component, icons_file in icons_files.items() } async def _async_get_component_icons( hass: HomeAssistant, components: set[str], integrations: dict[str, Integration], ) -> dict[str, Any]: """Load icons.""" icons: dict[str, Any] = {} # Determine files to load files_to_load = { comp: integrations[comp].file_path / "icons.json" for comp in components } # Load files if files_to_load: icons.update( await hass.async_add_executor_job(_load_icons_files, files_to_load) ) return icons class _IconsCache: """Cache for icons.""" __slots__ = ("_hass", "_loaded", "_cache", "_lock") def __init__(self, hass: HomeAssistant) -> None: """Initialize the cache.""" self._hass = hass self._loaded: set[str] = set() self._cache: dict[str, dict[str, Any]] = {} self._lock = asyncio.Lock() async def async_fetch( self, category: str, components: set[str], ) -> dict[str, dict[str, Any]]: """Load resources into the cache.""" if components_to_load := components - self._loaded: # Icons are never unloaded so if there are no components to load # we can skip the lock which reduces contention async with self._lock: # Check components to load again, as another task might have loaded # them while we were waiting for the lock. if components_to_load := components - self._loaded: await self._async_load(components_to_load) return { component: result for component in components if (result := self._cache.get(category, {}).get(component)) } async def _async_load(self, components: set[str]) -> None: """Populate the cache for a given set of components.""" _LOGGER.debug("Cache miss for: %s", components) integrations: dict[str, Integration] = {} ints_or_excs = await async_get_integrations(self._hass, components) for domain, int_or_exc in ints_or_excs.items(): if isinstance(int_or_exc, Exception): raise int_or_exc integrations[domain] = int_or_exc icons = await _async_get_component_icons(self._hass, components, integrations) self._build_category_cache(components, icons) self._loaded.update(components) @callback def _build_category_cache( self, components: set[str], icons: dict[str, dict[str, Any]], ) -> None: """Extract resources into the cache.""" categories = { category for component in icons.values() for category in component } for category in categories: self._cache.setdefault(category, {}).update( build_resources(icons, components, category) ) async def async_get_icons( hass: HomeAssistant, category: str, integrations: Iterable[str] | None = None, ) -> dict[str, Any]: """Return all icons of integrations. If integration specified, load it for that one; otherwise default to loaded integrations. """ if integrations: components = set(integrations) else: components = hass.config.top_level_components if ICON_CACHE in hass.data: cache = hass.data[ICON_CACHE] else: cache = hass.data[ICON_CACHE] = _IconsCache(hass) return await cache.async_fetch(category, components) @lru_cache def icon_for_battery_level( battery_level: int | None = None, charging: bool = False ) -> str: """Return a battery icon valid identifier.""" icon = "mdi:battery" if battery_level is None: return f"{icon}-unknown" if charging and battery_level > 10: icon += f"-charging-{int(round(battery_level / 20 - 0.01)) * 20}" elif charging: icon += "-outline" elif battery_level <= 5: icon += "-alert" elif 5 < battery_level < 95: icon += f"-{int(round(battery_level / 10 - 0.01)) * 10}" return icon def icon_for_signal_level(signal_level: int | None = None) -> str: """Return a signal icon valid identifier.""" if signal_level is None or signal_level == 0: return "mdi:signal-cellular-outline" if signal_level > 70: return "mdi:signal-cellular-3" if signal_level > 30: return "mdi:signal-cellular-2" return "mdi:signal-cellular-1"