Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support to allow pieces of code to request configuration from the user.
2 
3 Initiate a request by calling the `request_config` method with a callback.
4 This will return a request id that has to be used for future calls.
5 A callback has to be provided to `request_config` which will be called when
6 the user has submitted configuration information.
7 """
8 
9 from __future__ import annotations
10 
11 from collections.abc import Callable
12 from contextlib import suppress
13 from datetime import datetime
14 import functools as ft
15 from typing import Any
16 
17 from homeassistant.const import ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME
18 from homeassistant.core import (
19  HassJob,
20  HomeAssistant,
21  ServiceCall,
22  callback as async_callback,
23 )
24 from homeassistant.helpers import config_validation as cv
25 from homeassistant.helpers.entity import async_generate_entity_id
26 from homeassistant.helpers.event import async_call_later
27 from homeassistant.helpers.typing import ConfigType
28 from homeassistant.loader import bind_hass
29 from homeassistant.util.async_ import run_callback_threadsafe
30 
31 _KEY_INSTANCE = "configurator"
32 
33 DATA_REQUESTS = "configurator_requests"
34 
35 ATTR_CONFIGURE_ID = "configure_id"
36 ATTR_DESCRIPTION = "description"
37 ATTR_DESCRIPTION_IMAGE = "description_image"
38 ATTR_ERRORS = "errors"
39 ATTR_FIELDS = "fields"
40 ATTR_LINK_NAME = "link_name"
41 ATTR_LINK_URL = "link_url"
42 ATTR_SUBMIT_CAPTION = "submit_caption"
43 
44 DOMAIN = "configurator"
45 
46 ENTITY_ID_FORMAT = DOMAIN + ".{}"
47 
48 SERVICE_CONFIGURE = "configure"
49 STATE_CONFIGURE = "configure"
50 STATE_CONFIGURED = "configured"
51 
52 type ConfiguratorCallback = Callable[[list[dict[str, str]]], None]
53 
54 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
55 
56 
57 @bind_hass
58 @async_callback
60  hass: HomeAssistant,
61  name: str,
62  callback: ConfiguratorCallback | None = None,
63  description: str | None = None,
64  description_image: str | None = None,
65  submit_caption: str | None = None,
66  fields: list[dict[str, str]] | None = None,
67  link_name: str | None = None,
68  link_url: str | None = None,
69  entity_picture: str | None = None,
70 ) -> str:
71  """Create a new request for configuration.
72 
73  Will return an ID to be used for sequent calls.
74  """
75  if description and link_name is not None and link_url is not None:
76  description += f"\n\n[{link_name}]({link_url})"
77 
78  if description and description_image is not None:
79  description += f"\n\n![Description image]({description_image})"
80 
81  if (instance := hass.data.get(_KEY_INSTANCE)) is None:
82  instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
83 
84  request_id = instance.async_request_config(
85  name, callback, description, submit_caption, fields, entity_picture
86  )
87 
88  if DATA_REQUESTS not in hass.data:
89  hass.data[DATA_REQUESTS] = {}
90 
91  _get_requests(hass)[request_id] = instance
92 
93  return request_id
94 
95 
96 @bind_hass
97 def request_config(hass: HomeAssistant, *args: Any, **kwargs: Any) -> str:
98  """Create a new request for configuration.
99 
100  Will return an ID to be used for sequent calls.
101  """
102  return run_callback_threadsafe(
103  hass.loop, ft.partial(async_request_config, hass, *args, **kwargs)
104  ).result()
105 
106 
107 @bind_hass
108 @async_callback
109 def async_notify_errors(hass: HomeAssistant, request_id: str, error: str) -> None:
110  """Add errors to a config request."""
111  with suppress(KeyError): # If request_id does not exist
112  _get_requests(hass)[request_id].async_notify_errors(request_id, error)
113 
114 
115 @bind_hass
116 def notify_errors(hass: HomeAssistant, request_id: str, error: str) -> None:
117  """Add errors to a config request."""
118  return run_callback_threadsafe(
119  hass.loop, async_notify_errors, hass, request_id, error
120  ).result()
121 
122 
123 @bind_hass
124 @async_callback
125 def async_request_done(hass: HomeAssistant, request_id: str) -> None:
126  """Mark a configuration request as done."""
127  with suppress(KeyError): # If request_id does not exist
128  _get_requests(hass).pop(request_id).async_request_done(request_id)
129 
130 
131 @bind_hass
132 def request_done(hass: HomeAssistant, request_id: str) -> None:
133  """Mark a configuration request as done."""
134  return run_callback_threadsafe(
135  hass.loop, async_request_done, hass, request_id
136  ).result()
137 
138 
139 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
140  """Set up the configurator component."""
141  return True
142 
143 
144 def _get_requests(hass: HomeAssistant) -> dict[str, Configurator]:
145  """Return typed configurator_requests data."""
146  return hass.data[DATA_REQUESTS] # type: ignore[no-any-return]
147 
148 
150  """The class to keep track of current configuration requests."""
151 
152  def __init__(self, hass: HomeAssistant) -> None:
153  """Initialize the configurator."""
154  self.hasshass = hass
155  self._cur_id_cur_id = 0
156  self._requests: dict[
157  str, tuple[str, list[dict[str, str]], ConfiguratorCallback | None]
158  ] = {}
159  hass.services.async_register(
160  DOMAIN, SERVICE_CONFIGURE, self.async_handle_service_callasync_handle_service_call
161  )
162 
163  @async_callback
165  self,
166  name: str,
167  callback: ConfiguratorCallback | None,
168  description: str | None,
169  submit_caption: str | None,
170  fields: list[dict[str, str]] | None,
171  entity_picture: str | None,
172  ) -> str:
173  """Set up a request for configuration."""
174  entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hasshass)
175 
176  if fields is None:
177  fields = []
178 
179  request_id = self._generate_unique_id_generate_unique_id()
180 
181  self._requests[request_id] = (entity_id, fields, callback)
182 
183  data = {
184  ATTR_CONFIGURE_ID: request_id,
185  ATTR_FIELDS: fields,
186  ATTR_FRIENDLY_NAME: name,
187  ATTR_ENTITY_PICTURE: entity_picture,
188  }
189 
190  data.update(
191  {
192  key: value
193  for key, value in (
194  (ATTR_DESCRIPTION, description),
195  (ATTR_SUBMIT_CAPTION, submit_caption),
196  )
197  if value is not None
198  }
199  )
200 
201  self.hasshass.states.async_set(entity_id, STATE_CONFIGURE, data)
202 
203  return request_id
204 
205  @async_callback
206  def async_notify_errors(self, request_id: str, error: str) -> None:
207  """Update the state with errors."""
208  if not self._validate_request_id_validate_request_id(request_id):
209  return
210 
211  entity_id = self._requests[request_id][0]
212 
213  if (state := self.hasshass.states.get(entity_id)) is None:
214  return
215 
216  new_data = dict(state.attributes)
217  new_data[ATTR_ERRORS] = error
218 
219  self.hasshass.states.async_set(entity_id, STATE_CONFIGURE, new_data)
220 
221  @async_callback
222  def async_request_done(self, request_id: str) -> None:
223  """Remove the configuration request."""
224  if not self._validate_request_id_validate_request_id(request_id):
225  return
226 
227  entity_id = self._requests.pop(request_id)[0]
228 
229  # If we remove the state right away, it will not be included with
230  # the result of the service call (current design limitation).
231  # Instead, we will set it to configured to give as feedback but delete
232  # it shortly after so that it is deleted when the client updates.
233  self.hasshass.states.async_set(entity_id, STATE_CONFIGURED)
234 
235  @async_callback
236  def deferred_remove(now: datetime) -> None:
237  """Remove the request state."""
238  self.hasshass.states.async_remove(entity_id)
239 
240  async_call_later(self.hasshass, 1, deferred_remove)
241 
242  async def async_handle_service_call(self, call: ServiceCall) -> None:
243  """Handle a configure service call."""
244  request_id: str | None = call.data.get(ATTR_CONFIGURE_ID)
245 
246  if not request_id or not self._validate_request_id_validate_request_id(request_id):
247  return
248 
249  _, _, callback = self._requests[request_id]
250 
251  # field validation goes here?
252  if callback and (
253  job := self.hasshass.async_run_hass_job(
254  HassJob(callback), call.data.get(ATTR_FIELDS, {})
255  )
256  ):
257  await job
258 
259  def _generate_unique_id(self) -> str:
260  """Generate a unique configurator ID."""
261  self._cur_id_cur_id += 1
262  return f"{id(self)}-{self._cur_id}"
263 
264  def _validate_request_id(self, request_id: str) -> bool:
265  """Validate that the request belongs to this instance."""
266  return request_id in self._requests
None async_handle_service_call(self, ServiceCall call)
Definition: __init__.py:242
None __init__(self, HomeAssistant hass)
Definition: __init__.py:152
None async_notify_errors(self, str request_id, str error)
Definition: __init__.py:206
str async_request_config(self, str name, ConfiguratorCallback|None callback, str|None description, str|None submit_caption, list[dict[str, str]]|None fields, str|None entity_picture)
Definition: __init__.py:172
None async_request_done(self, str request_id)
Definition: __init__.py:222
bool _validate_request_id(self, str request_id)
Definition: __init__.py:264
dict[str, Configurator] _get_requests(HomeAssistant hass)
Definition: __init__.py:144
str request_config(HomeAssistant hass, *Any args, **Any kwargs)
Definition: __init__.py:97
None request_done(HomeAssistant hass, str request_id)
Definition: __init__.py:132
str async_request_config(HomeAssistant hass, str name, ConfiguratorCallback|None callback=None, str|None description=None, str|None description_image=None, str|None submit_caption=None, list[dict[str, str]]|None fields=None, str|None link_name=None, str|None link_url=None, str|None entity_picture=None)
Definition: __init__.py:70
None notify_errors(HomeAssistant hass, str request_id, str error)
Definition: __init__.py:116
None async_request_done(HomeAssistant hass, str request_id)
Definition: __init__.py:125
None async_notify_errors(HomeAssistant hass, str request_id, str error)
Definition: __init__.py:109
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:139
str async_generate_entity_id(str entity_id_format, str|None name, Iterable[str]|None current_ids=None, HomeAssistant|None hass=None)
Definition: entity.py:119
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597