Home Assistant Unofficial Reference 2024.12.1
icon.py
Go to the documentation of this file.
1 """Icon helper methods."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Iterable
7 from functools import lru_cache
8 import logging
9 import pathlib
10 from typing import Any, cast
11 
12 from homeassistant.core import HomeAssistant, callback
13 from homeassistant.loader import Integration, async_get_integrations
14 from homeassistant.util.hass_dict import HassKey
15 from homeassistant.util.json import load_json_object
16 
17 from .translation import build_resources
18 
19 ICON_CACHE: HassKey[_IconsCache] = HassKey("icon_cache")
20 
21 _LOGGER = logging.getLogger(__name__)
22 
23 
25  value: str | dict[str, str | dict[str, str]],
26 ) -> dict[str, str | dict[str, str]]:
27  """Convert shorthand service icon to dict."""
28  if isinstance(value, str):
29  return {"service": value}
30  return value
31 
32 
34  icons_file: pathlib.Path,
35 ) -> dict[str, Any]:
36  """Load and parse an icons.json file."""
37  icons = load_json_object(icons_file)
38  if "services" not in icons:
39  return icons
40  services = cast(dict[str, str | dict[str, str | dict[str, str]]], icons["services"])
41  for service, service_icons in services.items():
42  services[service] = convert_shorthand_service_icon(service_icons)
43  return icons
44 
45 
47  icons_files: dict[str, pathlib.Path],
48 ) -> dict[str, dict[str, Any]]:
49  """Load and parse icons.json files."""
50  return {
51  component: _load_icons_file(icons_file)
52  for component, icons_file in icons_files.items()
53  }
54 
55 
57  hass: HomeAssistant,
58  components: set[str],
59  integrations: dict[str, Integration],
60 ) -> dict[str, Any]:
61  """Load icons."""
62  icons: dict[str, Any] = {}
63 
64  # Determine files to load
65  files_to_load = {
66  comp: integrations[comp].file_path / "icons.json" for comp in components
67  }
68 
69  # Load files
70  if files_to_load:
71  icons.update(
72  await hass.async_add_executor_job(_load_icons_files, files_to_load)
73  )
74 
75  return icons
76 
77 
79  """Cache for icons."""
80 
81  __slots__ = ("_hass", "_loaded", "_cache", "_lock")
82 
83  def __init__(self, hass: HomeAssistant) -> None:
84  """Initialize the cache."""
85  self._hass_hass = hass
86  self._loaded: set[str] = set()
87  self._cache: dict[str, dict[str, Any]] = {}
88  self._lock_lock = asyncio.Lock()
89 
90  async def async_fetch(
91  self,
92  category: str,
93  components: set[str],
94  ) -> dict[str, dict[str, Any]]:
95  """Load resources into the cache."""
96  if components_to_load := components - self._loaded:
97  # Icons are never unloaded so if there are no components to load
98  # we can skip the lock which reduces contention
99  async with self._lock_lock:
100  # Check components to load again, as another task might have loaded
101  # them while we were waiting for the lock.
102  if components_to_load := components - self._loaded:
103  await self._async_load_async_load(components_to_load)
104 
105  return {
106  component: result
107  for component in components
108  if (result := self._cache.get(category, {}).get(component))
109  }
110 
111  async def _async_load(self, components: set[str]) -> None:
112  """Populate the cache for a given set of components."""
113  _LOGGER.debug("Cache miss for: %s", components)
114 
115  integrations: dict[str, Integration] = {}
116  ints_or_excs = await async_get_integrations(self._hass_hass, components)
117  for domain, int_or_exc in ints_or_excs.items():
118  if isinstance(int_or_exc, Exception):
119  raise int_or_exc
120  integrations[domain] = int_or_exc
121 
122  icons = await _async_get_component_icons(self._hass_hass, components, integrations)
123 
124  self._build_category_cache_build_category_cache(components, icons)
125  self._loaded.update(components)
126 
127  @callback
129  self,
130  components: set[str],
131  icons: dict[str, dict[str, Any]],
132  ) -> None:
133  """Extract resources into the cache."""
134  categories = {
135  category for component in icons.values() for category in component
136  }
137  for category in categories:
138  self._cache.setdefault(category, {}).update(
139  build_resources(icons, components, category)
140  )
141 
142 
143 async def async_get_icons(
144  hass: HomeAssistant,
145  category: str,
146  integrations: Iterable[str] | None = None,
147 ) -> dict[str, Any]:
148  """Return all icons of integrations.
149 
150  If integration specified, load it for that one; otherwise default to loaded
151  integrations.
152  """
153  if integrations:
154  components = set(integrations)
155  else:
156  components = hass.config.top_level_components
157 
158  if ICON_CACHE in hass.data:
159  cache = hass.data[ICON_CACHE]
160  else:
161  cache = hass.data[ICON_CACHE] = _IconsCache(hass)
162 
163  return await cache.async_fetch(category, components)
164 
165 
166 @lru_cache
168  battery_level: int | None = None, charging: bool = False
169 ) -> str:
170  """Return a battery icon valid identifier."""
171  icon = "mdi:battery"
172  if battery_level is None:
173  return f"{icon}-unknown"
174  if charging and battery_level > 10:
175  icon += f"-charging-{int(round(battery_level / 20 - 0.01)) * 20}"
176  elif charging:
177  icon += "-outline"
178  elif battery_level <= 5:
179  icon += "-alert"
180  elif 5 < battery_level < 95:
181  icon += f"-{int(round(battery_level / 10 - 0.01)) * 10}"
182  return icon
183 
184 
185 def icon_for_signal_level(signal_level: int | None = None) -> str:
186  """Return a signal icon valid identifier."""
187  if signal_level is None or signal_level == 0:
188  return "mdi:signal-cellular-outline"
189  if signal_level > 70:
190  return "mdi:signal-cellular-3"
191  if signal_level > 30:
192  return "mdi:signal-cellular-2"
193  return "mdi:signal-cellular-1"
None _async_load(self, set[str] components)
Definition: icon.py:111
None __init__(self, HomeAssistant hass)
Definition: icon.py:83
None _build_category_cache(self, set[str] components, dict[str, dict[str, Any]] icons)
Definition: icon.py:132
dict[str, dict[str, Any]] async_fetch(self, str category, set[str] components)
Definition: icon.py:94
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
dict[str, Any] async_get_icons(HomeAssistant hass, str category, Iterable[str]|None integrations=None)
Definition: icon.py:147
dict[str, Any] _async_get_component_icons(HomeAssistant hass, set[str] components, dict[str, Integration] integrations)
Definition: icon.py:60
dict[str, str|dict[str, str]] convert_shorthand_service_icon(str|dict[str, str|dict[str, str]] value)
Definition: icon.py:26
str icon_for_battery_level(int|None battery_level=None, bool charging=False)
Definition: icon.py:169
dict[str, Any] _load_icons_file(pathlib.Path icons_file)
Definition: icon.py:35
str icon_for_signal_level(int|None signal_level=None)
Definition: icon.py:185
dict[str, dict[str, Any]] _load_icons_files(dict[str, pathlib.Path] icons_files)
Definition: icon.py:48
dict[str, dict[str, Any]|str] build_resources(dict[str, dict[str, dict[str, Any]|str]] translation_strings, set[str] components, str category)
Definition: translation.py:78
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)
Definition: loader.py:1368
JsonObjectType load_json_object(str|PathLike[str] filename, JsonObjectType default=_SENTINEL)
Definition: json.py:109