Home Assistant Unofficial Reference 2024.12.1
notify.py
Go to the documentation of this file.
1 """Support for mobile_app push notifications."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from functools import partial
7 from http import HTTPStatus
8 import logging
9 
10 import aiohttp
11 
13  ATTR_DATA,
14  ATTR_MESSAGE,
15  ATTR_TARGET,
16  ATTR_TITLE,
17  ATTR_TITLE_DEFAULT,
18  BaseNotificationService,
19 )
20 from homeassistant.core import HomeAssistant
21 from homeassistant.exceptions import HomeAssistantError
22 from homeassistant.helpers.aiohttp_client import async_get_clientsession
23 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
24 import homeassistant.util.dt as dt_util
25 
26 from .const import (
27  ATTR_APP_DATA,
28  ATTR_APP_ID,
29  ATTR_APP_VERSION,
30  ATTR_DEVICE_NAME,
31  ATTR_OS_VERSION,
32  ATTR_PUSH_RATE_LIMITS,
33  ATTR_PUSH_RATE_LIMITS_ERRORS,
34  ATTR_PUSH_RATE_LIMITS_MAXIMUM,
35  ATTR_PUSH_RATE_LIMITS_RESETS_AT,
36  ATTR_PUSH_RATE_LIMITS_SUCCESSFUL,
37  ATTR_PUSH_TOKEN,
38  ATTR_PUSH_URL,
39  ATTR_WEBHOOK_ID,
40  DATA_CONFIG_ENTRIES,
41  DATA_NOTIFY,
42  DATA_PUSH_CHANNEL,
43  DOMAIN,
44 )
45 from .util import supports_push
46 
47 _LOGGER = logging.getLogger(__name__)
48 
49 
51  """Return a dictionary of push enabled registrations."""
52  targets = {}
53 
54  for webhook_id, entry in hass.data[DOMAIN][DATA_CONFIG_ENTRIES].items():
55  if not supports_push(hass, webhook_id):
56  continue
57 
58  targets[entry.data[ATTR_DEVICE_NAME]] = webhook_id
59 
60  return targets
61 
62 
63 def log_rate_limits(hass, device_name, resp, level=logging.INFO):
64  """Output rate limit log line at given level."""
65  if ATTR_PUSH_RATE_LIMITS not in resp:
66  return
67 
68  rate_limits = resp[ATTR_PUSH_RATE_LIMITS]
69  resetsAt = rate_limits[ATTR_PUSH_RATE_LIMITS_RESETS_AT]
70  resetsAtTime = dt_util.parse_datetime(resetsAt) - dt_util.utcnow()
71  rate_limit_msg = (
72  "mobile_app push notification rate limits for %s: "
73  "%d sent, %d allowed, %d errors, "
74  "resets in %s"
75  )
76  _LOGGER.log(
77  level,
78  rate_limit_msg,
79  device_name,
80  rate_limits[ATTR_PUSH_RATE_LIMITS_SUCCESSFUL],
81  rate_limits[ATTR_PUSH_RATE_LIMITS_MAXIMUM],
82  rate_limits[ATTR_PUSH_RATE_LIMITS_ERRORS],
83  str(resetsAtTime).split(".", maxsplit=1)[0],
84  )
85 
86 
88  hass: HomeAssistant,
89  config: ConfigType,
90  discovery_info: DiscoveryInfoType | None = None,
91 ) -> MobileAppNotificationService:
92  """Get the mobile_app notification service."""
93  service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(hass)
94  return service
95 
96 
97 class MobileAppNotificationService(BaseNotificationService):
98  """Implement the notification service for mobile_app."""
99 
100  def __init__(self, hass):
101  """Initialize the service."""
102  self._hass_hass = hass
103 
104  @property
105  def targets(self):
106  """Return a dictionary of registered targets."""
107  return push_registrations(self.hass)
108 
109  async def async_send_message(self, message="", **kwargs):
110  """Send a message to the Lambda APNS gateway."""
111  data = {ATTR_MESSAGE: message}
112 
113  # Remove default title from notifications.
114  if (
115  kwargs.get(ATTR_TITLE) is not None
116  and kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT
117  ):
118  data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)
119 
120  if not (targets := kwargs.get(ATTR_TARGET)):
121  targets = push_registrations(self.hass).values()
122 
123  if kwargs.get(ATTR_DATA) is not None:
124  data[ATTR_DATA] = kwargs.get(ATTR_DATA)
125 
126  local_push_channels = self.hass.data[DOMAIN][DATA_PUSH_CHANNEL]
127 
128  for target in targets:
129  registration = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target].data
130 
131  if target in local_push_channels:
132  local_push_channels[target].async_send_notification(
133  data,
134  partial(
135  self._async_send_remote_message_target_async_send_remote_message_target, target, registration
136  ),
137  )
138  continue
139 
140  # Test if local push only.
141  if ATTR_PUSH_URL not in registration[ATTR_APP_DATA]:
142  raise HomeAssistantError(
143  "Device not connected to local push notifications"
144  )
145 
146  await self._async_send_remote_message_target_async_send_remote_message_target(target, registration, data)
147 
148  async def _async_send_remote_message_target(self, target, registration, data):
149  """Send a message to a target."""
150  app_data = registration[ATTR_APP_DATA]
151  push_token = app_data[ATTR_PUSH_TOKEN]
152  push_url = app_data[ATTR_PUSH_URL]
153 
154  target_data = dict(data)
155  target_data[ATTR_PUSH_TOKEN] = push_token
156 
157  reg_info = {
158  ATTR_APP_ID: registration[ATTR_APP_ID],
159  ATTR_APP_VERSION: registration[ATTR_APP_VERSION],
160  ATTR_WEBHOOK_ID: target,
161  }
162  if ATTR_OS_VERSION in registration:
163  reg_info[ATTR_OS_VERSION] = registration[ATTR_OS_VERSION]
164 
165  target_data["registration_info"] = reg_info
166 
167  try:
168  async with asyncio.timeout(10):
169  response = await async_get_clientsession(self._hass_hass).post(
170  push_url, json=target_data
171  )
172  result = await response.json()
173 
174  if response.status in (
175  HTTPStatus.OK,
176  HTTPStatus.CREATED,
177  HTTPStatus.ACCEPTED,
178  ):
179  log_rate_limits(self.hass, registration[ATTR_DEVICE_NAME], result)
180  return
181 
182  fallback_error = result.get("errorMessage", "Unknown error")
183  fallback_message = (
184  f"Internal server error, please try again later: {fallback_error}"
185  )
186  message = result.get("message", fallback_message)
187 
188  if "message" in result:
189  if message[-1] not in [".", "?", "!"]:
190  message += "."
191  message += " This message is generated externally to Home Assistant."
192 
193  if response.status == HTTPStatus.TOO_MANY_REQUESTS:
194  _LOGGER.warning(message)
196  self.hass, registration[ATTR_DEVICE_NAME], result, logging.WARNING
197  )
198  else:
199  _LOGGER.error(message)
200 
201  except TimeoutError:
202  _LOGGER.error("Timeout sending notification to %s", push_url)
203  except aiohttp.ClientError as err:
204  _LOGGER.error("Error sending notification to %s: %r", push_url, err)
def _async_send_remote_message_target(self, target, registration, data)
Definition: notify.py:148
web.Response post(self, web.Request request, str config_key)
Definition: view.py:101
None async_send_notification(LaMetricDataUpdateCoordinator coordinator, ServiceCall call, list[Chart|Goal|Simple] frames)
Definition: services.py:117
MobileAppNotificationService async_get_service(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
Definition: notify.py:91
def log_rate_limits(hass, device_name, resp, level=logging.INFO)
Definition: notify.py:63
bool supports_push(HomeAssistant hass, str webhook_id)
Definition: util.py:42
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)