Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for the Lovelace UI."""
2 
3 import logging
4 
5 import voluptuous as vol
6 
7 from homeassistant.components import frontend, onboarding, websocket_api
8 from homeassistant.config import (
9  async_hass_config_yaml,
10  async_process_component_and_handle_errors,
11 )
12 from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES
13 from homeassistant.core import HomeAssistant, ServiceCall, callback
14 from homeassistant.exceptions import HomeAssistantError
15 from homeassistant.helpers import collection, config_validation as cv
16 from homeassistant.helpers.service import async_register_admin_service
17 from homeassistant.helpers.translation import async_get_translations
18 from homeassistant.helpers.typing import ConfigType
19 from homeassistant.loader import async_get_integration
20 
21 from . import dashboard, resources, websocket
22 from .const import ( # noqa: F401
23  CONF_ALLOW_SINGLE_WORD,
24  CONF_ICON,
25  CONF_REQUIRE_ADMIN,
26  CONF_SHOW_IN_SIDEBAR,
27  CONF_TITLE,
28  CONF_URL_PATH,
29  DASHBOARD_BASE_CREATE_FIELDS,
30  DEFAULT_ICON,
31  DOMAIN,
32  EVENT_LOVELACE_UPDATED,
33  MODE_STORAGE,
34  MODE_YAML,
35  RESOURCE_CREATE_FIELDS,
36  RESOURCE_RELOAD_SERVICE_SCHEMA,
37  RESOURCE_SCHEMA,
38  RESOURCE_UPDATE_FIELDS,
39  SERVICE_RELOAD_RESOURCES,
40  STORAGE_DASHBOARD_CREATE_FIELDS,
41  STORAGE_DASHBOARD_UPDATE_FIELDS,
42  url_slug,
43 )
44 from .system_health import system_health_info # noqa: F401
45 
46 _LOGGER = logging.getLogger(__name__)
47 
48 CONF_DASHBOARDS = "dashboards"
49 
50 YAML_DASHBOARD_SCHEMA = vol.Schema(
51  {
52  **DASHBOARD_BASE_CREATE_FIELDS,
53  vol.Required(CONF_MODE): MODE_YAML,
54  vol.Required(CONF_FILENAME): cv.path,
55  }
56 )
57 
58 CONFIG_SCHEMA = vol.Schema(
59  {
60  vol.Optional(DOMAIN, default={}): vol.Schema(
61  {
62  vol.Optional(CONF_MODE, default=MODE_STORAGE): vol.All(
63  vol.Lower, vol.In([MODE_YAML, MODE_STORAGE])
64  ),
65  vol.Optional(CONF_DASHBOARDS): cv.schema_with_slug_keys(
66  YAML_DASHBOARD_SCHEMA,
67  slug_validator=url_slug,
68  ),
69  vol.Optional(CONF_RESOURCES): [RESOURCE_SCHEMA],
70  }
71  )
72  },
73  extra=vol.ALLOW_EXTRA,
74 )
75 
76 
77 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
78  """Set up the Lovelace commands."""
79  mode = config[DOMAIN][CONF_MODE]
80  yaml_resources = config[DOMAIN].get(CONF_RESOURCES)
81 
82  frontend.async_register_built_in_panel(hass, DOMAIN, config={"mode": mode})
83 
84  async def reload_resources_service_handler(service_call: ServiceCall) -> None:
85  """Reload yaml resources."""
86  try:
87  conf = await async_hass_config_yaml(hass)
88  except HomeAssistantError as err:
89  _LOGGER.error(err)
90  return
91 
92  integration = await async_get_integration(hass, DOMAIN)
93 
95  hass, conf, integration
96  )
97 
98  if config is None:
99  raise HomeAssistantError("Config validation failed")
100 
101  resource_collection = await create_yaml_resource_col(
102  hass, config[DOMAIN].get(CONF_RESOURCES)
103  )
104  hass.data[DOMAIN]["resources"] = resource_collection
105 
106  default_config: dashboard.LovelaceConfig
107  if mode == MODE_YAML:
108  default_config = dashboard.LovelaceYAML(hass, None, None)
109  resource_collection = await create_yaml_resource_col(hass, yaml_resources)
110 
112  hass,
113  DOMAIN,
114  SERVICE_RELOAD_RESOURCES,
115  reload_resources_service_handler,
116  schema=RESOURCE_RELOAD_SERVICE_SCHEMA,
117  )
118  # Register lovelace/resources for backwards compatibility, remove in
119  # Home Assistant Core 2025.1
120  for command in ("lovelace/resources", "lovelace/resources/list"):
121  websocket_api.async_register_command(
122  hass,
123  command,
124  websocket.websocket_lovelace_resources,
125  websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
126  {"type": command},
127  ),
128  )
129 
130  else:
131  default_config = dashboard.LovelaceStorage(hass, None)
132 
133  if yaml_resources is not None:
134  _LOGGER.warning(
135  "Lovelace is running in storage mode. Define resources via user"
136  " interface"
137  )
138 
139  resource_collection = resources.ResourceStorageCollection(hass, default_config)
140 
142  resource_collection,
143  "lovelace/resources",
144  "resource",
145  RESOURCE_CREATE_FIELDS,
146  RESOURCE_UPDATE_FIELDS,
147  ).async_setup(hass)
148 
149  websocket_api.async_register_command(hass, websocket.websocket_lovelace_config)
150  websocket_api.async_register_command(hass, websocket.websocket_lovelace_save_config)
151  websocket_api.async_register_command(
152  hass, websocket.websocket_lovelace_delete_config
153  )
154 
155  hass.data[DOMAIN] = {
156  # We store a dictionary mapping url_path: config. None is the default.
157  "mode": mode,
158  "dashboards": {None: default_config},
159  "resources": resource_collection,
160  "yaml_dashboards": config[DOMAIN].get(CONF_DASHBOARDS, {}),
161  }
162 
163  if hass.config.recovery_mode:
164  return True
165 
166  async def storage_dashboard_changed(change_type, item_id, item):
167  """Handle a storage dashboard change."""
168  url_path = item[CONF_URL_PATH]
169 
170  if change_type == collection.CHANGE_REMOVED:
171  frontend.async_remove_panel(hass, url_path)
172  await hass.data[DOMAIN]["dashboards"].pop(url_path).async_delete()
173  return
174 
175  if change_type == collection.CHANGE_ADDED:
176  existing = hass.data[DOMAIN]["dashboards"].get(url_path)
177 
178  if existing:
179  _LOGGER.warning(
180  "Cannot register panel at %s, it is already defined in %s",
181  url_path,
182  existing,
183  )
184  return
185 
186  hass.data[DOMAIN]["dashboards"][url_path] = dashboard.LovelaceStorage(
187  hass, item
188  )
189 
190  update = False
191  else:
192  hass.data[DOMAIN]["dashboards"][url_path].config = item
193  update = True
194 
195  try:
196  _register_panel(hass, url_path, MODE_STORAGE, item, update)
197  except ValueError:
198  _LOGGER.warning("Failed to %s panel %s from storage", change_type, url_path)
199 
200  # Process YAML dashboards
201  for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items():
202  # For now always mode=yaml
203  lovelace_config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf)
204  hass.data[DOMAIN]["dashboards"][url_path] = lovelace_config
205 
206  try:
207  _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False)
208  except ValueError:
209  _LOGGER.warning("Panel url path %s is not unique", url_path)
210 
211  # Process storage dashboards
212  dashboards_collection = dashboard.DashboardsCollection(hass)
213 
214  # This can be removed when the map integration is removed
215  hass.data[DOMAIN]["dashboards_collection"] = dashboards_collection
216 
217  dashboards_collection.async_add_listener(storage_dashboard_changed)
218  await dashboards_collection.async_load()
219 
221  dashboards_collection,
222  "lovelace/dashboards",
223  "dashboard",
224  STORAGE_DASHBOARD_CREATE_FIELDS,
225  STORAGE_DASHBOARD_UPDATE_FIELDS,
226  ).async_setup(hass)
227 
228  def create_map_dashboard():
229  hass.async_create_task(_create_map_dashboard(hass))
230 
231  if not onboarding.async_is_onboarded(hass):
232  onboarding.async_add_listener(hass, create_map_dashboard)
233 
234  return True
235 
236 
237 async def create_yaml_resource_col(hass, yaml_resources):
238  """Create yaml resources collection."""
239  if yaml_resources is None:
240  default_config = dashboard.LovelaceYAML(hass, None, None)
241  try:
242  ll_conf = await default_config.async_load(False)
243  except HomeAssistantError:
244  pass
245  else:
246  if CONF_RESOURCES in ll_conf:
247  _LOGGER.warning(
248  "Resources need to be specified in your configuration.yaml. Please"
249  " see the docs"
250  )
251  yaml_resources = ll_conf[CONF_RESOURCES]
252 
253  return resources.ResourceYAMLCollection(yaml_resources or [])
254 
255 
256 @callback
257 def _register_panel(hass, url_path, mode, config, update):
258  """Register a panel."""
259  kwargs = {
260  "frontend_url_path": url_path,
261  "require_admin": config[CONF_REQUIRE_ADMIN],
262  "config": {"mode": mode},
263  "update": update,
264  }
265 
266  if config[CONF_SHOW_IN_SIDEBAR]:
267  kwargs["sidebar_title"] = config[CONF_TITLE]
268  kwargs["sidebar_icon"] = config.get(CONF_ICON, DEFAULT_ICON)
269 
270  frontend.async_register_built_in_panel(hass, DOMAIN, **kwargs)
271 
272 
273 async def _create_map_dashboard(hass: HomeAssistant):
274  translations = await async_get_translations(
275  hass, hass.config.language, "dashboard", {onboarding.DOMAIN}
276  )
277  title = translations["component.onboarding.dashboard.map.title"]
278 
279  dashboards_collection: dashboard.DashboardsCollection = hass.data[DOMAIN][
280  "dashboards_collection"
281  ]
282  await dashboards_collection.async_create_item(
283  {
284  CONF_ALLOW_SINGLE_WORD: True,
285  CONF_ICON: "mdi:map",
286  CONF_TITLE: title,
287  CONF_URL_PATH: "map",
288  }
289  )
290 
291  map_store: dashboard.LovelaceStorage = hass.data[DOMAIN]["dashboards"]["map"]
292  await map_store.async_save({"strategy": {"type": "map"}})
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
def _register_panel(hass, url_path, mode, config, update)
Definition: __init__.py:257
None reload_resources_service_handler(ServiceCall service_call)
Definition: __init__.py:84
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:77
def create_yaml_resource_col(hass, yaml_resources)
Definition: __init__.py:237
def _create_map_dashboard(HomeAssistant hass)
Definition: __init__.py:273
ConfigType|None async_process_component_and_handle_errors(HomeAssistant hass, ConfigType config, Integration integration, bool raise_on_failure=False)
Definition: config.py:862
dict async_hass_config_yaml(HomeAssistant hass)
Definition: config.py:209
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
Definition: service.py:1121
dict[str, str] async_get_translations(HomeAssistant hass, str language, str category, Iterable[str]|None integrations=None, bool|None config_flow=None)
Definition: translation.py:342
Integration async_get_integration(HomeAssistant hass, str domain)
Definition: loader.py:1354