Home Assistant Unofficial Reference 2024.12.1
notify.py
Go to the documentation of this file.
1 """Signal Messenger for notify component."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from pysignalclirestapi import SignalCliRestApi, SignalCliRestApiError
9 import requests
10 import voluptuous as vol
11 
13  ATTR_DATA,
14  PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA,
15  BaseNotificationService,
16 )
17 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
20 
21 _LOGGER = logging.getLogger(__name__)
22 
23 CONF_SENDER_NR = "number"
24 CONF_RECP_NR = "recipients"
25 CONF_SIGNAL_CLI_REST_API = "url"
26 CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES = 52428800
27 ATTR_FILENAMES = "attachments"
28 ATTR_URLS = "urls"
29 ATTR_VERIFY_SSL = "verify_ssl"
30 ATTR_TEXTMODE = "text_mode"
31 
32 TEXTMODE_OPTIONS = ["normal", "styled"]
33 
34 DATA_FILENAMES_SCHEMA = vol.Schema(
35  {
36  vol.Required(ATTR_FILENAMES): [cv.string],
37  vol.Optional(ATTR_TEXTMODE, default="normal"): vol.In(TEXTMODE_OPTIONS),
38  }
39 )
40 
41 DATA_URLS_SCHEMA = vol.Schema(
42  {
43  vol.Required(ATTR_URLS): [cv.url],
44  vol.Optional(ATTR_VERIFY_SSL, default=True): cv.boolean,
45  vol.Optional(ATTR_TEXTMODE, default="normal"): vol.In(TEXTMODE_OPTIONS),
46  }
47 )
48 
49 DATA_SCHEMA = vol.Any(
50  None,
51  vol.Schema(
52  {
53  vol.Optional(ATTR_TEXTMODE, default="normal"): vol.In(TEXTMODE_OPTIONS),
54  }
55  ),
56  DATA_FILENAMES_SCHEMA,
57  DATA_URLS_SCHEMA,
58 )
59 
60 PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
61  {
62  vol.Required(CONF_SENDER_NR): cv.string,
63  vol.Required(CONF_SIGNAL_CLI_REST_API): cv.string,
64  vol.Required(CONF_RECP_NR): vol.All(cv.ensure_list, [cv.string]),
65  }
66 )
67 
68 
70  hass: HomeAssistant,
71  config: ConfigType,
72  discovery_info: DiscoveryInfoType | None = None,
73 ) -> SignalNotificationService:
74  """Get the SignalMessenger notification service."""
75 
76  sender_nr = config[CONF_SENDER_NR]
77  recp_nrs = config[CONF_RECP_NR]
78  signal_cli_rest_api_url = config[CONF_SIGNAL_CLI_REST_API]
79 
80  signal_cli_rest_api = SignalCliRestApi(signal_cli_rest_api_url, sender_nr)
81 
82  return SignalNotificationService(hass, recp_nrs, signal_cli_rest_api)
83 
84 
85 class SignalNotificationService(BaseNotificationService):
86  """Implement the notification service for SignalMessenger."""
87 
88  def __init__(
89  self,
90  hass: HomeAssistant,
91  recp_nrs: list[str],
92  signal_cli_rest_api: SignalCliRestApi,
93  ) -> None:
94  """Initialize the service."""
95 
96  self._hass_hass = hass
97  self._recp_nrs_recp_nrs = recp_nrs
98  self._signal_cli_rest_api_signal_cli_rest_api = signal_cli_rest_api
99 
100  def send_message(self, message: str = "", **kwargs: Any) -> None:
101  """Send a message to a one or more recipients. Additionally a file can be attached."""
102 
103  _LOGGER.debug("Sending signal message")
104 
105  data = kwargs.get(ATTR_DATA)
106 
107  try:
108  data = DATA_SCHEMA(data)
109  except vol.Invalid as ex:
110  _LOGGER.error("Invalid message data: %s", ex)
111  raise
112 
113  filenames = self.get_filenamesget_filenames(data)
114  attachments_as_bytes = self.get_attachments_as_bytesget_attachments_as_bytes(
115  data, CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES, self._hass_hass
116  )
117  try:
118  self._signal_cli_rest_api_signal_cli_rest_api.send_message(
119  message,
120  self._recp_nrs_recp_nrs,
121  filenames,
122  attachments_as_bytes,
123  text_mode="normal" if data is None else data.get(ATTR_TEXTMODE),
124  )
125  except SignalCliRestApiError as ex:
126  _LOGGER.error("%s", ex)
127  raise
128 
129  @staticmethod
130  def get_filenames(data: Any) -> list[str] | None:
131  """Extract attachment filenames from data."""
132  try:
133  data = DATA_FILENAMES_SCHEMA(data)
134  except vol.Invalid:
135  return None
136  return data[ATTR_FILENAMES]
137 
138  @staticmethod
140  data: Any,
141  attachment_size_limit: int,
142  hass: HomeAssistant,
143  ) -> list[bytearray] | None:
144  """Retrieve attachments from URLs defined in data."""
145  try:
146  data = DATA_URLS_SCHEMA(data)
147  except vol.Invalid:
148  return None
149  urls = data[ATTR_URLS]
150 
151  attachments_as_bytes: list[bytearray] = []
152 
153  for url in urls:
154  try:
155  if not hass.config.is_allowed_external_url(url):
156  _LOGGER.error("URL '%s' not in allow list", url)
157  continue
158 
159  resp = requests.get(
160  url, verify=data[ATTR_VERIFY_SSL], timeout=10, stream=True
161  )
162  resp.raise_for_status()
163 
164  if (
165  resp.headers.get("Content-Length") is not None
166  and int(str(resp.headers.get("Content-Length")))
167  > attachment_size_limit
168  ):
169  content_length = int(str(resp.headers.get("Content-Length")))
170  raise ValueError( # noqa: TRY301
171  "Attachment too large (Content-Length reports "
172  f"{content_length}). Max size: "
173  f"{CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES} bytes"
174  )
175 
176  size = 0
177  chunks = bytearray()
178  for chunk in resp.iter_content(1024):
179  size += len(chunk)
180  if size > attachment_size_limit:
181  raise ValueError( # noqa: TRY301
182  f"Attachment too large (Stream reports {size}). "
183  f"Max size: {CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES} bytes"
184  )
185 
186  chunks.extend(chunk)
187 
188  attachments_as_bytes.append(chunks)
189  except Exception as ex:
190  _LOGGER.error("%s", ex)
191  raise
192 
193  if not attachments_as_bytes:
194  return None
195 
196  return attachments_as_bytes
None __init__(self, HomeAssistant hass, list[str] recp_nrs, SignalCliRestApi signal_cli_rest_api)
Definition: notify.py:93
list[bytearray]|None get_attachments_as_bytes(Any data, int attachment_size_limit, HomeAssistant hass)
Definition: notify.py:143
SignalNotificationService get_service(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
Definition: notify.py:73