Home Assistant Unofficial Reference 2024.12.1
notify.py
Go to the documentation of this file.
1 """Notifications for Android TV notification service."""
2 
3 from __future__ import annotations
4 
5 from io import BufferedReader
6 import logging
7 from typing import Any
8 
9 from notifications_android_tv import Notifications
10 import requests
11 from requests.auth import HTTPBasicAuth, HTTPDigestAuth
12 import voluptuous as vol
13 
15  ATTR_DATA,
16  ATTR_TITLE,
17  ATTR_TITLE_DEFAULT,
18  BaseNotificationService,
19 )
20 from homeassistant.const import CONF_HOST
21 from homeassistant.core import HomeAssistant
22 from homeassistant.exceptions import ServiceValidationError
24 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
25 
26 from .const import (
27  ATTR_COLOR,
28  ATTR_DURATION,
29  ATTR_FONTSIZE,
30  ATTR_ICON,
31  ATTR_ICON_AUTH,
32  ATTR_ICON_AUTH_DIGEST,
33  ATTR_ICON_PASSWORD,
34  ATTR_ICON_PATH,
35  ATTR_ICON_URL,
36  ATTR_ICON_USERNAME,
37  ATTR_IMAGE,
38  ATTR_IMAGE_AUTH,
39  ATTR_IMAGE_AUTH_DIGEST,
40  ATTR_IMAGE_PASSWORD,
41  ATTR_IMAGE_PATH,
42  ATTR_IMAGE_URL,
43  ATTR_IMAGE_USERNAME,
44  ATTR_INTERRUPT,
45  ATTR_POSITION,
46  ATTR_TRANSPARENCY,
47  DEFAULT_TIMEOUT,
48  DOMAIN,
49 )
50 
51 _LOGGER = logging.getLogger(__name__)
52 
53 
55  hass: HomeAssistant,
56  config: ConfigType,
57  discovery_info: DiscoveryInfoType | None = None,
58 ) -> NFAndroidTVNotificationService | None:
59  """Get the NFAndroidTV notification service."""
60  if discovery_info is None:
61  return None
62  notify = await hass.async_add_executor_job(Notifications, discovery_info[CONF_HOST])
64  notify,
65  hass.config.is_allowed_path,
66  )
67 
68 
69 class NFAndroidTVNotificationService(BaseNotificationService):
70  """Notification service for Notifications for Android TV."""
71 
72  def __init__(
73  self,
74  notify: Notifications,
75  is_allowed_path: Any,
76  ) -> None:
77  """Initialize the service."""
78  self.notifynotify = notify
79  self.is_allowed_pathis_allowed_path = is_allowed_path
80 
81  def send_message(self, message: str, **kwargs: Any) -> None:
82  """Send a message to a Android TV device."""
83  data: dict | None = kwargs.get(ATTR_DATA)
84  title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
85  duration = None
86  fontsize = None
87  position = None
88  transparency = None
89  bkgcolor = None
90  interrupt = False
91  icon = None
92  image_file = None
93  if data:
94  if ATTR_DURATION in data:
95  try:
96  duration = int(
97  data.get(ATTR_DURATION, Notifications.DEFAULT_DURATION)
98  )
99  except ValueError:
100  _LOGGER.warning(
101  "Invalid duration-value: %s", data.get(ATTR_DURATION)
102  )
103  if ATTR_FONTSIZE in data:
104  if data.get(ATTR_FONTSIZE) in Notifications.FONTSIZES:
105  fontsize = data.get(ATTR_FONTSIZE)
106  else:
107  _LOGGER.warning(
108  "Invalid fontsize-value: %s", data.get(ATTR_FONTSIZE)
109  )
110  if ATTR_POSITION in data:
111  if data.get(ATTR_POSITION) in Notifications.POSITIONS:
112  position = data.get(ATTR_POSITION)
113  else:
114  _LOGGER.warning(
115  "Invalid position-value: %s", data.get(ATTR_POSITION)
116  )
117  if ATTR_TRANSPARENCY in data:
118  if data.get(ATTR_TRANSPARENCY) in Notifications.TRANSPARENCIES:
119  transparency = data.get(ATTR_TRANSPARENCY)
120  else:
121  _LOGGER.warning(
122  "Invalid transparency-value: %s",
123  data.get(ATTR_TRANSPARENCY),
124  )
125  if ATTR_COLOR in data:
126  if data.get(ATTR_COLOR) in Notifications.BKG_COLORS:
127  bkgcolor = data.get(ATTR_COLOR)
128  else:
129  _LOGGER.warning("Invalid color-value: %s", data.get(ATTR_COLOR))
130  if ATTR_INTERRUPT in data:
131  try:
132  interrupt = cv.boolean(data.get(ATTR_INTERRUPT))
133  except vol.Invalid:
134  _LOGGER.warning(
135  "Invalid interrupt-value: %s", data.get(ATTR_INTERRUPT)
136  )
137  if imagedata := data.get(ATTR_IMAGE):
138  if isinstance(imagedata, str):
139  image_file = (
140  self.load_fileload_file(url=imagedata)
141  if imagedata.startswith("http")
142  else self.load_fileload_file(local_path=imagedata)
143  )
144  elif isinstance(imagedata, dict):
145  image_file = self.load_fileload_file(
146  url=imagedata.get(ATTR_IMAGE_URL),
147  local_path=imagedata.get(ATTR_IMAGE_PATH),
148  username=imagedata.get(ATTR_IMAGE_USERNAME),
149  password=imagedata.get(ATTR_IMAGE_PASSWORD),
150  auth=imagedata.get(ATTR_IMAGE_AUTH),
151  )
152  else:
154  "Invalid image provided",
155  translation_domain=DOMAIN,
156  translation_key="invalid_notification_image",
157  translation_placeholders={"type": type(imagedata).__name__},
158  )
159  if icondata := data.get(ATTR_ICON):
160  if isinstance(icondata, str):
161  icondata = (
162  self.load_fileload_file(url=icondata)
163  if icondata.startswith("http")
164  else self.load_fileload_file(local_path=icondata)
165  )
166  elif isinstance(icondata, dict):
167  icon = self.load_fileload_file(
168  url=icondata.get(ATTR_ICON_URL),
169  local_path=icondata.get(ATTR_ICON_PATH),
170  username=icondata.get(ATTR_ICON_USERNAME),
171  password=icondata.get(ATTR_ICON_PASSWORD),
172  auth=icondata.get(ATTR_ICON_AUTH),
173  )
174  else:
176  "Invalid Icon provided",
177  translation_domain=DOMAIN,
178  translation_key="invalid_notification_icon",
179  translation_placeholders={"type": type(icondata).__name__},
180  )
181  self.notifynotify.send(
182  message,
183  title=title,
184  duration=duration,
185  fontsize=fontsize,
186  position=position,
187  bkgcolor=bkgcolor,
188  transparency=transparency,
189  interrupt=interrupt,
190  icon=icon,
191  image_file=image_file,
192  )
193 
195  self,
196  url: str | None = None,
197  local_path: str | None = None,
198  username: str | None = None,
199  password: str | None = None,
200  auth: str | None = None,
201  ) -> BufferedReader | bytes | None:
202  """Load image/document/etc from a local path or URL."""
203  try:
204  if url is not None:
205  # Check whether authentication parameters are provided
206  if username is not None and password is not None:
207  # Use digest or basic authentication
208  auth_: HTTPDigestAuth | HTTPBasicAuth
209  if auth in (ATTR_IMAGE_AUTH_DIGEST, ATTR_ICON_AUTH_DIGEST):
210  auth_ = HTTPDigestAuth(username, password)
211  else:
212  auth_ = HTTPBasicAuth(username, password)
213  # Load file from URL with authentication
214  req = requests.get(url, auth=auth_, timeout=DEFAULT_TIMEOUT)
215  else:
216  # Load file from URL without authentication
217  req = requests.get(url, timeout=DEFAULT_TIMEOUT)
218  return req.content
219 
220  if local_path is not None:
221  # Check whether path is whitelisted in configuration.yaml
222  if self.is_allowed_pathis_allowed_path(local_path):
223  return open(local_path, "rb")
224  _LOGGER.warning("'%s' is not secure to load data from!", local_path)
225  else:
226  _LOGGER.warning("Neither URL nor local path found in params!")
227 
228  except OSError as error:
229  _LOGGER.error("Can't load from url or local path: %s", error)
230 
231  return None
BufferedReader|bytes|None load_file(self, str|None url=None, str|None local_path=None, str|None username=None, str|None password=None, str|None auth=None)
Definition: notify.py:201
None __init__(self, Notifications notify, Any is_allowed_path)
Definition: notify.py:76
NFAndroidTVNotificationService|None async_get_service(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
Definition: notify.py:58
None open(self, **Any kwargs)
Definition: lock.py:86