Home Assistant Unofficial Reference 2024.12.1
legacy.py
Go to the documentation of this file.
1 """Handle legacy notification platforms."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Coroutine, Mapping
7 from functools import partial
8 from typing import Any, Protocol, cast
9 
10 from homeassistant.config import config_per_platform
11 from homeassistant.const import CONF_DESCRIPTION, CONF_NAME
12 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback
13 from homeassistant.exceptions import HomeAssistantError
14 from homeassistant.helpers import discovery
15 from homeassistant.helpers.service import async_set_service_schema
16 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
17 from homeassistant.loader import async_get_integration, bind_hass
18 from homeassistant.setup import (
19  SetupPhases,
20  async_prepare_setup_platform,
21  async_start_setup,
22 )
23 from homeassistant.util import slugify
24 from homeassistant.util.hass_dict import HassKey
25 from homeassistant.util.yaml import load_yaml_dict
26 
27 from .const import (
28  ATTR_DATA,
29  ATTR_MESSAGE,
30  ATTR_TARGET,
31  ATTR_TITLE,
32  DOMAIN,
33  LOGGER,
34  NOTIFY_SERVICE_SCHEMA,
35  SERVICE_NOTIFY,
36 )
37 
38 CONF_FIELDS = "fields"
39 NOTIFY_SERVICES: HassKey[dict[str, list[BaseNotificationService]]] = HassKey(
40  f"{DOMAIN}_services"
41 )
42 NOTIFY_DISCOVERY_DISPATCHER: HassKey[CALLBACK_TYPE | None] = HassKey(
43  f"{DOMAIN}_discovery_dispatcher"
44 )
45 
46 
47 class LegacyNotifyPlatform(Protocol):
48  """Define the format of legacy notify platforms."""
49 
50  async def async_get_service(
51  self,
52  hass: HomeAssistant,
53  config: ConfigType,
54  discovery_info: DiscoveryInfoType | None = ...,
55  ) -> BaseNotificationService | None:
56  """Set up notification service."""
57 
58  def get_service(
59  self,
60  hass: HomeAssistant,
61  config: ConfigType,
62  discovery_info: DiscoveryInfoType | None = ...,
63  ) -> BaseNotificationService | None:
64  """Set up notification service."""
65 
66 
67 @callback
69  hass: HomeAssistant, config: ConfigType
70 ) -> list[Coroutine[Any, Any, None]]:
71  """Set up legacy notify services."""
72  hass.data.setdefault(NOTIFY_SERVICES, {})
73  hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None)
74 
75  async def async_setup_platform(
76  integration_name: str,
77  p_config: ConfigType | None = None,
78  discovery_info: DiscoveryInfoType | None = None,
79  ) -> None:
80  """Set up a notify platform."""
81  if p_config is None:
82  p_config = {}
83 
84  platform = cast(
85  LegacyNotifyPlatform | None,
86  await async_prepare_setup_platform(hass, config, DOMAIN, integration_name),
87  )
88 
89  if platform is None:
90  LOGGER.error("Unknown notification service specified")
91  return
92 
93  full_name = f"{DOMAIN}.{integration_name}"
94  LOGGER.info("Setting up %s", full_name)
95  with async_start_setup(
96  hass,
97  integration=integration_name,
98  group=str(id(p_config)),
99  phase=SetupPhases.PLATFORM_SETUP,
100  ):
101  notify_service: BaseNotificationService | None = None
102  try:
103  if hasattr(platform, "async_get_service"):
104  notify_service = await platform.async_get_service(
105  hass, p_config, discovery_info
106  )
107  elif hasattr(platform, "get_service"):
108  notify_service = await hass.async_add_executor_job(
109  platform.get_service, hass, p_config, discovery_info
110  )
111  else:
112  raise HomeAssistantError("Invalid notify platform.") # noqa: TRY301
113 
114  if notify_service is None:
115  # Platforms can decide not to create a service based
116  # on discovery data.
117  if discovery_info is None:
118  LOGGER.error(
119  "Failed to initialize notification service %s",
120  integration_name,
121  )
122  return
123 
124  except Exception: # noqa: BLE001
125  LOGGER.exception("Error setting up platform %s", integration_name)
126  return
127 
128  if discovery_info is None:
129  discovery_info = {}
130 
131  conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME)
132  target_service_name_prefix = conf_name or integration_name
133  service_name = slugify(conf_name or SERVICE_NOTIFY)
134 
135  await notify_service.async_setup(
136  hass, service_name, target_service_name_prefix
137  )
138  await notify_service.async_register_services()
139 
140  hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append(
141  notify_service
142  )
143  hass.config.components.add(f"{integration_name}.{DOMAIN}")
144 
145  async def async_platform_discovered(
146  platform: str, info: DiscoveryInfoType | None
147  ) -> None:
148  """Handle for discovered platform."""
149  await async_setup_platform(platform, discovery_info=info)
150 
151  hass.data[NOTIFY_DISCOVERY_DISPATCHER] = discovery.async_listen_platform(
152  hass, DOMAIN, async_platform_discovered
153  )
154 
155  return [
156  async_setup_platform(integration_name, p_config)
157  for integration_name, p_config in config_per_platform(config, DOMAIN)
158  if integration_name is not None
159  ]
160 
161 
162 @bind_hass
163 async def async_reload(hass: HomeAssistant, integration_name: str) -> None:
164  """Register notify services for an integration."""
165  if not _async_integration_has_notify_services(hass, integration_name):
166  return
167 
168  tasks = [
169  notify_service.async_register_services()
170  for notify_service in hass.data[NOTIFY_SERVICES][integration_name]
171  ]
172 
173  await asyncio.gather(*tasks)
174 
175 
176 @bind_hass
177 async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None:
178  """Unregister notify services for an integration."""
179  notify_discovery_dispatcher = hass.data.get(NOTIFY_DISCOVERY_DISPATCHER)
180  if notify_discovery_dispatcher:
181  notify_discovery_dispatcher()
182  hass.data[NOTIFY_DISCOVERY_DISPATCHER] = None
183  if not _async_integration_has_notify_services(hass, integration_name):
184  return
185 
186  tasks = [
187  notify_service.async_unregister_services()
188  for notify_service in hass.data[NOTIFY_SERVICES][integration_name]
189  ]
190 
191  await asyncio.gather(*tasks)
192 
193  del hass.data[NOTIFY_SERVICES][integration_name]
194 
195 
197  hass: HomeAssistant, integration_name: str
198 ) -> bool:
199  """Determine if an integration has notify services registered."""
200  if (
201  NOTIFY_SERVICES not in hass.data
202  or integration_name not in hass.data[NOTIFY_SERVICES]
203  ):
204  return False
205 
206  return True
207 
208 
210  """An abstract class for notification services."""
211 
212  # While not purely typed, it makes typehinting more useful for us
213  # and removes the need for constant None checks or asserts.
214  hass: HomeAssistant = None # type: ignore[assignment]
215 
216  # Name => target
217  registered_targets: dict[str, Any]
218 
219  @property
220  def targets(self) -> Mapping[str, Any] | None:
221  """Return a dictionary of registered targets."""
222  return None
223 
224  def send_message(self, message: str, **kwargs: Any) -> None:
225  """Send a message.
226 
227  kwargs can contain ATTR_TITLE to specify a title.
228  """
229  raise NotImplementedError
230 
231  async def async_send_message(self, message: str, **kwargs: Any) -> None:
232  """Send a message.
233 
234  kwargs can contain ATTR_TITLE to specify a title.
235  """
236  await self.hasshass.async_add_executor_job(
237  partial(self.send_messagesend_message, message, **kwargs)
238  )
239 
240  async def _async_notify_message_service(self, service: ServiceCall) -> None:
241  """Handle sending notification message service calls."""
242  kwargs = {}
243  message: str = service.data[ATTR_MESSAGE]
244  title: str | None
245  if title := service.data.get(ATTR_TITLE):
246  kwargs[ATTR_TITLE] = title
247 
248  if self.registered_targetsregistered_targets.get(service.service) is not None:
249  kwargs[ATTR_TARGET] = [self.registered_targetsregistered_targets[service.service]]
250  elif service.data.get(ATTR_TARGET) is not None:
251  kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET)
252 
253  kwargs[ATTR_MESSAGE] = message
254  kwargs[ATTR_DATA] = service.data.get(ATTR_DATA)
255 
256  await self.async_send_messageasync_send_message(**kwargs)
257 
258  async def async_setup(
259  self,
260  hass: HomeAssistant,
261  service_name: str,
262  target_service_name_prefix: str,
263  ) -> None:
264  """Store the data for the notify service."""
265  # pylint: disable=attribute-defined-outside-init
266  self.hasshass = hass
267  self._service_name_service_name = service_name
268  self._target_service_name_prefix_target_service_name_prefix = target_service_name_prefix
269  self.registered_targetsregistered_targets = {}
270 
271  # Load service descriptions from notify/services.yaml
272  integration = await async_get_integration(hass, DOMAIN)
273  services_yaml = integration.file_path / "services.yaml"
274  self.services_dictservices_dict = await hass.async_add_executor_job(
275  load_yaml_dict, str(services_yaml)
276  )
277 
278  async def async_register_services(self) -> None:
279  """Create or update the notify services."""
280  if self.targetstargets is not None:
281  stale_targets = set(self.registered_targetsregistered_targets)
282 
283  for name, target in self.targetstargets.items():
284  target_name = slugify(f"{self._target_service_name_prefix}_{name}")
285  if target_name in stale_targets:
286  stale_targets.remove(target_name)
287  if (
288  target_name in self.registered_targetsregistered_targets
289  and target == self.registered_targetsregistered_targets[target_name]
290  ):
291  continue
292  self.registered_targetsregistered_targets[target_name] = target
293  self.hasshass.services.async_register(
294  DOMAIN,
295  target_name,
296  self._async_notify_message_service_async_notify_message_service,
297  schema=NOTIFY_SERVICE_SCHEMA,
298  )
299  # Register the service description
300  service_desc = {
301  CONF_NAME: f"Send a notification via {target_name}",
302  CONF_DESCRIPTION: (
303  "Sends a notification message using the"
304  f" {target_name} integration."
305  ),
306  CONF_FIELDS: self.services_dictservices_dict[SERVICE_NOTIFY][CONF_FIELDS],
307  }
308  async_set_service_schema(self.hasshass, DOMAIN, target_name, service_desc)
309 
310  for stale_target_name in stale_targets:
311  del self.registered_targetsregistered_targets[stale_target_name]
312  self.hasshass.services.async_remove(
313  DOMAIN,
314  stale_target_name,
315  )
316 
317  if self.hasshass.services.has_service(DOMAIN, self._service_name_service_name):
318  return
319 
320  self.hasshass.services.async_register(
321  DOMAIN,
322  self._service_name_service_name,
323  self._async_notify_message_service_async_notify_message_service,
324  schema=NOTIFY_SERVICE_SCHEMA,
325  )
326 
327  # Register the service description
328  service_desc = {
329  CONF_NAME: f"Send a notification with {self._service_name}",
330  CONF_DESCRIPTION: (
331  f"Sends a notification message using the {self._service_name} service."
332  ),
333  CONF_FIELDS: self.services_dictservices_dict[SERVICE_NOTIFY][CONF_FIELDS],
334  }
335  async_set_service_schema(self.hasshass, DOMAIN, self._service_name_service_name, service_desc)
336 
337  async def async_unregister_services(self) -> None:
338  """Unregister the notify services."""
339  if self.registered_targetsregistered_targets:
340  remove_targets = set(self.registered_targetsregistered_targets)
341  for remove_target_name in remove_targets:
342  del self.registered_targetsregistered_targets[remove_target_name]
343  self.hasshass.services.async_remove(
344  DOMAIN,
345  remove_target_name,
346  )
347 
348  if not self.hasshass.services.has_service(DOMAIN, self._service_name_service_name):
349  return
350 
351  self.hasshass.services.async_remove(
352  DOMAIN,
353  self._service_name_service_name,
354  )
None async_send_message(self, str message, **Any kwargs)
Definition: legacy.py:231
None _async_notify_message_service(self, ServiceCall service)
Definition: legacy.py:240
None send_message(self, str message, **Any kwargs)
Definition: legacy.py:224
None async_setup(self, HomeAssistant hass, str service_name, str target_service_name_prefix)
Definition: legacy.py:263
BaseNotificationService|None async_get_service(self, HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=...)
Definition: legacy.py:55
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
AppriseNotificationService|None get_service(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
Definition: notify.py:39
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_reload(HomeAssistant hass, str integration_name)
Definition: legacy.py:163
list[Coroutine[Any, Any, None]] async_setup_legacy(HomeAssistant hass, ConfigType config)
Definition: legacy.py:70
bool _async_integration_has_notify_services(HomeAssistant hass, str integration_name)
Definition: legacy.py:198
None async_reset_platform(HomeAssistant hass, str integration_name)
Definition: legacy.py:177
Iterable[tuple[str|None, ConfigType]] config_per_platform(ConfigType config, str domain)
Definition: config.py:969
None async_set_service_schema(HomeAssistant hass, str domain, str service, dict[str, Any] schema)
Definition: service.py:844
Integration async_get_integration(HomeAssistant hass, str domain)
Definition: loader.py:1354
ModuleType|None async_prepare_setup_platform(core.HomeAssistant hass, ConfigType hass_config, str domain, str platform_name)
Definition: setup.py:487
Generator[None] async_start_setup(core.HomeAssistant hass, str integration, SetupPhases phase, str|None group=None)
Definition: setup.py:739