Home Assistant Unofficial Reference 2024.12.1
websocket_api.py
Go to the documentation of this file.
1 """Websocket API for blueprint."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable, Coroutine
7 import functools
8 from typing import Any, cast
9 
10 import voluptuous as vol
11 
12 from homeassistant.components import websocket_api
13 from homeassistant.core import HomeAssistant, callback
14 from homeassistant.exceptions import HomeAssistantError
15 from homeassistant.helpers import config_validation as cv
16 from homeassistant.util import yaml
17 
18 from . import importer, models
19 from .const import DOMAIN
20 from .errors import BlueprintException, FailedToLoad, FileAlreadyExists
21 from .schemas import BLUEPRINT_SCHEMA
22 
23 
24 @callback
25 def async_setup(hass: HomeAssistant) -> None:
26  """Set up the websocket API."""
27  websocket_api.async_register_command(hass, ws_delete_blueprint)
28  websocket_api.async_register_command(hass, ws_import_blueprint)
29  websocket_api.async_register_command(hass, ws_list_blueprints)
30  websocket_api.async_register_command(hass, ws_save_blueprint)
31  websocket_api.async_register_command(hass, ws_substitute_blueprint)
32 
33 
35  func: Callable[
36  [
37  HomeAssistant,
39  dict[str, Any],
41  ],
42  Coroutine[Any, Any, None],
43  ],
44 ) -> websocket_api.AsyncWebSocketCommandHandler:
45  """Decorate a function to pass in the domain blueprints."""
46 
47  @functools.wraps(func)
48  async def with_domain_blueprints(
49  hass: HomeAssistant,
51  msg: dict[str, Any],
52  ) -> None:
53  domain_blueprints: models.DomainBlueprints | None = hass.data.get(
54  DOMAIN, {}
55  ).get(msg["domain"])
56  if domain_blueprints is None:
57  connection.send_error(
58  msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain"
59  )
60  return
61 
62  await func(hass, connection, msg, domain_blueprints)
63 
64  return with_domain_blueprints
65 
66 
67 @websocket_api.websocket_command( { vol.Required("type"): "blueprint/list",
68  vol.Required("domain"): cv.string,
69  }
70 )
71 @websocket_api.async_response
72 async def ws_list_blueprints(
73  hass: HomeAssistant,
75  msg: dict[str, Any],
76 ) -> None:
77  """List available blueprints."""
78  domain_blueprints: dict[str, models.DomainBlueprints] = hass.data.get(DOMAIN, {})
79  results: dict[str, Any] = {}
80 
81  if msg["domain"] not in domain_blueprints:
82  connection.send_result(msg["id"], results)
83  return
84 
85  domain_results = await domain_blueprints[msg["domain"]].async_get_blueprints()
86 
87  for path, value in domain_results.items():
88  if isinstance(value, models.Blueprint):
89  results[path] = {
90  "metadata": value.metadata,
91  }
92  else:
93  results[path] = {"error": str(value)}
94 
95  connection.send_result(msg["id"], results)
96 
97 
98 @websocket_api.websocket_command( { vol.Required("type"): "blueprint/import",
99  vol.Required("url"): cv.url,
100  }
101 )
102 @websocket_api.async_response
103 async def ws_import_blueprint(
104  hass: HomeAssistant,
105  connection: websocket_api.ActiveConnection,
106  msg: dict[str, Any],
107 ) -> None:
108  """Import a blueprint."""
109  async with asyncio.timeout(10):
110  imported_blueprint = await importer.fetch_blueprint_from_url(hass, msg["url"])
111 
112  if imported_blueprint is None:
113  connection.send_error( # type: ignore[unreachable]
114  msg["id"], websocket_api.ERR_NOT_SUPPORTED, "This url is not supported"
115  )
116  return
117 
118  # Check it exists and if so, which automations are using it
119  domain = imported_blueprint.blueprint.metadata["domain"]
120  domain_blueprints: models.DomainBlueprints | None = hass.data.get(DOMAIN, {}).get(
121  domain
122  )
123  if domain_blueprints is None:
124  connection.send_error(
125  msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain"
126  )
127  return
128 
129  suggested_path = f"{imported_blueprint.suggested_filename}.yaml"
130  try:
131  exists = bool(await domain_blueprints.async_get_blueprint(suggested_path))
132  except FailedToLoad:
133  exists = False
134 
135  connection.send_result(
136  msg["id"],
137  {
138  "suggested_filename": imported_blueprint.suggested_filename,
139  "raw_data": imported_blueprint.raw_data,
140  "blueprint": {
141  "metadata": imported_blueprint.blueprint.metadata,
142  },
143  "validation_errors": imported_blueprint.blueprint.validate(),
144  "exists": exists,
145  },
146  )
147 
148 
149 @websocket_api.websocket_command( { vol.Required("type"): "blueprint/save",
150  vol.Required("domain"): cv.string,
151  vol.Required("path"): cv.path,
152  vol.Required("yaml"): cv.string,
153  vol.Optional("source_url"): cv.url,
154  vol.Optional("allow_override"): bool,
155  }
156 )
157 @websocket_api.async_response
158 @_ws_with_blueprint_domain
159 async def ws_save_blueprint(
160  hass: HomeAssistant,
161  connection: websocket_api.ActiveConnection,
162  msg: dict[str, Any],
163  domain_blueprints: models.DomainBlueprints,
164 ) -> None:
165  """Save a blueprint."""
166 
167  path = msg["path"]
168  domain = msg["domain"]
169 
170  try:
171  yaml_data = cast(dict[str, Any], yaml.parse_yaml(msg["yaml"]))
172  blueprint = models.Blueprint(
173  yaml_data, expected_domain=domain, schema=BLUEPRINT_SCHEMA
174  )
175  if "source_url" in msg:
176  blueprint.update_metadata(source_url=msg["source_url"])
177  except HomeAssistantError as err:
178  connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
179  return
180 
181  if not path.endswith(".yaml"):
182  path = f"{path}.yaml"
183 
184  try:
185  overrides_existing = await domain_blueprints.async_add_blueprint(
186  blueprint, path, allow_override=msg.get("allow_override", False)
187  )
188  except FileAlreadyExists:
189  connection.send_error(msg["id"], "already_exists", "File already exists")
190  return
191  except OSError as err:
192  connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
193  return
194 
195  connection.send_result(
196  msg["id"],
197  {
198  "overrides_existing": overrides_existing,
199  },
200  )
201 
202 
203 @websocket_api.websocket_command( { vol.Required("type"): "blueprint/delete",
204  vol.Required("domain"): cv.string,
205  vol.Required("path"): cv.path,
206  }
207 )
208 @websocket_api.async_response
209 @_ws_with_blueprint_domain
210 async def ws_delete_blueprint(
211  hass: HomeAssistant,
212  connection: websocket_api.ActiveConnection,
213  msg: dict[str, Any],
214  domain_blueprints: models.DomainBlueprints,
215 ) -> None:
216  """Delete a blueprint."""
217  try:
218  await domain_blueprints.async_remove_blueprint(msg["path"])
219  except OSError as err:
220  connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
221  return
222 
223  connection.send_result(
224  msg["id"],
225  )
226 
227 
228 @websocket_api.websocket_command( { vol.Required("type"): "blueprint/substitute",
229  vol.Required("domain"): cv.string,
230  vol.Required("path"): cv.path,
231  vol.Required("input"): dict,
232  }
233 )
234 @websocket_api.async_response
235 @_ws_with_blueprint_domain
236 async def ws_substitute_blueprint(
237  hass: HomeAssistant,
238  connection: websocket_api.ActiveConnection,
239  msg: dict[str, Any],
240  domain_blueprints: models.DomainBlueprints,
241 ) -> None:
242  """Process a blueprinted config to allow editing."""
243 
244  blueprint_config = {"use_blueprint": {"path": msg["path"], "input": msg["input"]}}
245 
246  try:
247  blueprint_inputs = await domain_blueprints.async_inputs_from_config(
248  blueprint_config
249  )
250  except BlueprintException as err:
251  connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
252  return
253 
254  try:
255  config = blueprint_inputs.async_substitute()
256  except yaml.UndefinedSubstitution as err:
257  connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
258  return
259 
260  connection.send_result(msg["id"], {"substituted_config": config})
261 
blueprint.DomainBlueprints async_get_blueprints(HomeAssistant hass)
Definition: helpers.py:29
None ws_substitute_blueprint(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg, models.DomainBlueprints domain_blueprints)
None ws_list_blueprints(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
None ws_delete_blueprint(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg, models.DomainBlueprints domain_blueprints)
None ws_import_blueprint(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
websocket_api.AsyncWebSocketCommandHandler _ws_with_blueprint_domain(Callable[[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any], models.DomainBlueprints,], Coroutine[Any, Any, None],] func)
None ws_save_blueprint(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg, models.DomainBlueprints domain_blueprints)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88