Coverage for custom_components/supernotify/__init__.py: 99%
194 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-28 14:21 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-28 14:21 +0000
1"""The SuperNotification integration"""
3from dataclasses import dataclass, field
4from enum import StrEnum
6import voluptuous as vol
7from homeassistant.components.notify import PLATFORM_SCHEMA
8from homeassistant.const import (
9 ATTR_DOMAIN,
10 ATTR_SERVICE,
11 CONF_ACTION,
12 CONF_ALIAS,
13 CONF_CONDITION,
14 CONF_DEFAULT,
15 CONF_DESCRIPTION,
16 CONF_EMAIL,
17 CONF_ENABLED,
18 CONF_ENTITIES,
19 CONF_ICON,
20 CONF_ID,
21 CONF_NAME,
22 CONF_PLATFORM,
23 CONF_TARGET,
24 CONF_URL,
25 STATE_HOME,
26 STATE_NOT_HOME,
27 Platform,
28)
29from homeassistant.helpers import config_validation as cv
31from custom_components.supernotify.common import format_timestamp as format_timestamp
33DOMAIN = "supernotify"
35PLATFORMS = [Platform.NOTIFY]
36TEMPLATE_DIR = "/config/templates/supernotify"
37MEDIA_DIR = "supernotify/media"
39CONF_ACTIONS = "actions"
40CONF_TITLE = "title"
41CONF_URI = "uri"
42CONF_RECIPIENTS = "recipients"
43CONF_TEMPLATE_PATH = "template_path"
44CONF_MEDIA_PATH = "media_path"
45CONF_HOUSEKEEPING = "housekeeping"
46CONF_HOUSEKEEPING_TIME = "housekeeping_time"
47CONF_ARCHIVE_PATH = "archive_path"
48CONF_ARCHIVE = "archive"
49CONF_ARCHIVE_DAYS = "archive_days"
50CONF_TEMPLATE = "template"
51CONF_LINKS = "links"
52CONF_PERSON = "person"
53CONF_METHOD = "method"
54CONF_METHODS = "methods"
55CONF_DELIVERY = "delivery"
56CONF_SELECTION = "selection"
58CONF_DATA: str = "data"
59CONF_OPTIONS: str = "options"
60CONF_MOBILE: str = "mobile"
61CONF_NOTIFY: str = "notify"
62CONF_NOTIFY_ACTION: str = "notify_action"
63CONF_PHONE_NUMBER: str = "phone_number"
64CONF_PRIORITY: str = "priority"
65CONF_OCCUPANCY: str = "occupancy"
66CONF_SCENARIOS: str = "scenarios"
67CONF_MANUFACTURER: str = "manufacturer"
68CONF_DEVICE_TRACKER: str = "device_tracker"
69CONF_DEVICE_NAME: str = "device_name"
70CONF_DEVICE_LABELS: str = "device_labels"
71CONF_MODEL: str = "model"
72CONF_MESSAGE: str = "message"
73CONF_TARGETS_REQUIRED: str = "targets_required"
74CONF_MOBILE_DEVICES: str = "mobile_devices"
75CONF_MOBILE_DISCOVERY: str = "mobile_discovery"
76CONF_ACTION_TEMPLATE: str = "action_template"
77CONF_ACTION_GROUPS: str = "action_groups"
78CONF_TITLE_TEMPLATE: str = "title_template"
79CONF_DELIVERY_SELECTION: str = "delivery_selection"
80CONF_MEDIA: str = "media"
81CONF_CAMERA: str = "camera"
82CONF_CLIP_URL: str = "clip_url"
83CONF_SNAPSHOT_URL: str = "snapshot_url"
84CONF_PTZ_DELAY: str = "ptz_delay"
85CONF_PTZ_METHOD: str = "ptz_method"
86CONF_PTZ_PRESET_DEFAULT: str = "ptz_default_preset"
87CONF_ALT_CAMERA: str = "alt_camera"
88CONF_CAMERAS: str = "cameras"
90OCCUPANCY_ANY_IN = "any_in"
91OCCUPANCY_ANY_OUT = "any_out"
92OCCUPANCY_ALL_IN = "all_in"
93OCCUPANCY_ALL = "all"
94OCCUPANCY_NONE = "none"
95OCCUPANCY_ALL_OUT = "all_out"
96OCCUPANCY_ONLY_IN = "only_in"
97OCCUPANCY_ONLY_OUT = "only_out"
99ATTR_PRIORITY = "priority"
100ATTR_ACTION = "action"
101ATTR_SCENARIOS_CONSTRAIN = "require_scenarios"
102ATTR_SCENARIOS_APPLY = "apply_scenarios"
103ATTR_DELIVERY = "delivery"
104ATTR_DEFAULT = "default"
105ATTR_NOTIFICATION_ID = "notification_id"
106ATTR_DELIVERY_SELECTION = "delivery_selection"
107ATTR_RECIPIENTS = "recipients"
108ATTR_DATA = "data"
109ATTR_MEDIA = "media"
110ATTR_TITLE = "title"
111ATTR_MEDIA_SNAPSHOT_URL = "snapshot_url"
112ATTR_MEDIA_CAMERA_ENTITY_ID = "camera_entity_id"
113ATTR_MEDIA_CAMERA_DELAY = "camera_delay"
114ATTR_MEDIA_CAMERA_PTZ_PRESET = "camera_ptz_preset"
115ATTR_MEDIA_CLIP_URL = "clip_url"
116ATTR_ACTION_GROUPS = "action_groups"
117CONF_ACTION_GROUP_NAMES = "action_groups"
118ATTR_ACTION_CATEGORY = "action_category"
119ATTR_ACTION_URL = "action_url"
120ATTR_ACTION_URL_TITLE = "action_url_title"
121ATTR_MESSAGE_HTML = "message_html"
122ATTR_JPEG_FLAGS = "jpeg_flags"
123ATTR_TIMESTAMP = "timestamp"
124ATTR_DEBUG = "debug"
125ATTR_ACTIONS = "actions"
126ATTR_USER_ID = "user_id"
128DELIVERY_SELECTION_IMPLICIT = "implicit"
129DELIVERY_SELECTION_EXPLICIT = "explicit"
130DELIVERY_SELECTION_FIXED = "fixed"
132DELIVERY_SELECTION_VALUES = [DELIVERY_SELECTION_EXPLICIT, DELIVERY_SELECTION_FIXED, DELIVERY_SELECTION_IMPLICIT]
133PTZ_METHOD_ONVIF = "onvif"
134PTZ_METHOD_FRIGATE = "frigate"
135PTZ_METHOD_VALUES = [PTZ_METHOD_ONVIF, PTZ_METHOD_FRIGATE]
137SELECTION_FALLBACK_ON_ERROR = "fallback_on_error"
138SELECTION_FALLBACK = "fallback"
139SELECTION_BY_SCENARIO = "scenario"
140SELECTION_DEFAULT = "default"
141SELECTION_VALUES = [SELECTION_FALLBACK_ON_ERROR, SELECTION_BY_SCENARIO, SELECTION_DEFAULT, SELECTION_FALLBACK]
143OCCUPANCY_VALUES = [
144 OCCUPANCY_ALL_IN,
145 OCCUPANCY_ALL_OUT,
146 OCCUPANCY_ANY_IN,
147 OCCUPANCY_ANY_OUT,
148 OCCUPANCY_ONLY_IN,
149 OCCUPANCY_ONLY_OUT,
150 OCCUPANCY_ALL,
151 OCCUPANCY_NONE,
152]
154PRIORITY_CRITICAL = "critical"
155PRIORITY_HIGH = "high"
156PRIORITY_MEDIUM = "medium"
157PRIORITY_LOW = "low"
159PRIORITY_VALUES = [PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH, PRIORITY_CRITICAL]
160METHOD_SMS = "sms"
161METHOD_EMAIL = "email"
162METHOD_ALEXA = "alexa"
163METHOD_MOBILE_PUSH = "mobile_push"
164METHOD_MEDIA = "media"
165METHOD_CHIME = "chime"
166METHOD_GENERIC = "generic"
167METHOD_PERSISTENT = "persistent"
168METHOD_VALUES = [
169 METHOD_SMS,
170 METHOD_ALEXA,
171 METHOD_MOBILE_PUSH,
172 METHOD_CHIME,
173 METHOD_EMAIL,
174 METHOD_MEDIA,
175 METHOD_PERSISTENT,
176 METHOD_GENERIC,
177]
179SCENARIO_DEFAULT = "DEFAULT"
180SCENARIO_NULL = "NULL"
182RESERVED_DELIVERY_NAMES = ["ALL"]
183RESERVED_SCENARIO_NAMES = [SCENARIO_DEFAULT, SCENARIO_NULL]
184RESERVED_DATA_KEYS = [ATTR_DOMAIN, ATTR_SERVICE, "action"]
186CONF_DUPE_CHECK = "dupe_check"
187CONF_DUPE_POLICY = "dupe_policy"
188CONF_TTL = "ttl"
189CONF_SIZE = "size"
190ATTR_DUPE_POLICY_MTSLP = "dupe_policy_message_title_same_or_lower_priority"
191ATTR_DUPE_POLICY_NONE = "dupe_policy_none"
193DATA_SCHEMA = vol.Schema({vol.NotIn(RESERVED_DATA_KEYS): vol.Any(str, int, bool, float, dict, list)})
194MOBILE_DEVICE_SCHEMA = vol.Schema({
195 vol.Optional(CONF_MANUFACTURER): cv.string,
196 vol.Optional(CONF_MODEL): cv.string,
197 vol.Optional(CONF_NOTIFY_ACTION): cv.string,
198 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
199})
200NOTIFICATION_DUPE_SCHEMA = vol.Schema({
201 vol.Optional(CONF_TTL): cv.positive_int,
202 vol.Optional(CONF_SIZE, default=100): cv.positive_int, # type: ignore
203 vol.Optional(CONF_DUPE_POLICY, default=ATTR_DUPE_POLICY_MTSLP): vol.In( # type: ignore
204 [ATTR_DUPE_POLICY_MTSLP, ATTR_DUPE_POLICY_NONE]
205 ), # type: ignore
206})
207DELIVERY_CUSTOMIZE_SCHEMA = vol.Schema({
208 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
209 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
210 vol.Optional(CONF_ENABLED, default=True): cv.boolean, # type: ignore
211 vol.Optional(CONF_DATA): DATA_SCHEMA,
212})
213LINK_SCHEMA = vol.Schema({
214 vol.Optional(CONF_ID): cv.string,
215 vol.Required(CONF_URL): cv.url,
216 vol.Optional(CONF_ICON): cv.icon,
217 vol.Required(CONF_DESCRIPTION): cv.string,
218 vol.Optional(CONF_NAME): cv.string,
219})
220METHOD_DEFAULTS_SCHEMA = vol.Schema({
221 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
222 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
223 vol.Optional(CONF_ACTION): cv.service,
224 vol.Optional(CONF_TARGETS_REQUIRED): cv.boolean,
225 vol.Optional(CONF_OPTIONS, default=dict): dict, # type: ignore
226 vol.Optional(CONF_DATA): DATA_SCHEMA,
227})
228RECIPIENT_SCHEMA = vol.Schema({
229 vol.Required(CONF_PERSON): cv.entity_id,
230 vol.Optional(CONF_ALIAS): cv.string,
231 vol.Optional(CONF_EMAIL): cv.string,
232 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
233 vol.Optional(CONF_PHONE_NUMBER): cv.string,
234 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean, # type: ignore
235 vol.Optional(CONF_MOBILE_DEVICES, default=list): vol.All(cv.ensure_list, [MOBILE_DEVICE_SCHEMA]), # type: ignore
236 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_CUSTOMIZE_SCHEMA}, # type: ignore
237})
238CAMERA_SCHEMA = vol.Schema({
239 vol.Required(CONF_CAMERA): cv.entity_id,
240 vol.Optional(CONF_ALT_CAMERA): vol.All(cv.ensure_list, [cv.entity_id]),
241 vol.Optional(CONF_ALIAS): cv.string,
242 vol.Optional(CONF_URL): cv.url,
243 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
244 vol.Optional(CONF_PTZ_PRESET_DEFAULT, default=1): vol.Any(cv.positive_int, cv.string), # type: ignore
245 vol.Optional(CONF_PTZ_DELAY, default=0): int, # type: ignore
246 vol.Optional(CONF_PTZ_METHOD, default=PTZ_METHOD_ONVIF): vol.In(PTZ_METHOD_VALUES), # type: ignore
247})
248MEDIA_SCHEMA = vol.Schema({
249 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id,
250 vol.Optional(ATTR_MEDIA_CAMERA_DELAY, default=0): int, # type: ignore
251 vol.Optional(ATTR_MEDIA_CAMERA_PTZ_PRESET): vol.Any(cv.positive_int, cv.string),
252 # URL fragments allowed
253 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string),
254 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string),
255 vol.Optional(ATTR_JPEG_FLAGS): dict,
256})
257DELIVERY_SCHEMA = vol.Schema({
258 vol.Optional(CONF_ALIAS): cv.string,
259 vol.Required(CONF_METHOD): vol.In(METHOD_VALUES),
260 vol.Optional(CONF_ACTION): cv.service, # previously 'service:'
261 vol.Optional(CONF_PLATFORM): cv.string,
262 vol.Optional(CONF_TEMPLATE): cv.string,
263 vol.Optional(CONF_DEFAULT, default=False): cv.boolean, # type: ignore
264 vol.Optional(CONF_SELECTION, default=[SELECTION_DEFAULT]): vol.All( # type: ignore
265 cv.ensure_list, [vol.In(SELECTION_VALUES)]
266 ),
267 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
268 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
269 vol.Optional(CONF_MESSAGE): vol.Any(None, cv.string),
270 vol.Optional(CONF_TITLE): vol.Any(None, cv.string),
271 vol.Optional(CONF_DATA): DATA_SCHEMA,
272 vol.Optional(CONF_ENABLED, default=True): cv.boolean, # type: ignore # type: ignore
273 vol.Optional(CONF_OPTIONS, default=dict): dict, # type: ignore
274 vol.Optional(CONF_PRIORITY, default=PRIORITY_VALUES): vol.All( # type: ignore
275 cv.ensure_list, [vol.In(PRIORITY_VALUES)]
276 ),
277 vol.Optional(CONF_OCCUPANCY, default=OCCUPANCY_ALL): vol.In(OCCUPANCY_VALUES), # type: ignore
278 vol.Optional(CONF_CONDITION): cv.CONDITION_SCHEMA,
279})
281SCENARIO_SCHEMA = vol.Schema({
282 vol.Optional(CONF_ALIAS): cv.string,
283 vol.Optional(CONF_CONDITION): cv.CONDITION_SCHEMA,
284 vol.Optional(CONF_MEDIA): MEDIA_SCHEMA,
285 vol.Optional(CONF_ACTION_GROUP_NAMES, default=[]): vol.All(cv.ensure_list, [cv.string]), # type: ignore
286 vol.Optional(CONF_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
287 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}, # type: ignore
288})
289ACTION_CALL_SCHEMA = vol.Schema(
290 {
291 vol.Optional(ATTR_ACTION): cv.string,
292 vol.Optional(ATTR_TITLE): cv.string,
293 vol.Optional(ATTR_ACTION_CATEGORY): cv.string,
294 vol.Optional(ATTR_ACTION_URL): cv.url,
295 vol.Optional(ATTR_ACTION_URL_TITLE): cv.string,
296 },
297 extra=vol.ALLOW_EXTRA,
298)
299ACTION_SCHEMA = vol.Schema(
300 {
301 vol.Exclusive(CONF_ACTION, CONF_ACTION_TEMPLATE): cv.string,
302 vol.Exclusive(CONF_TITLE, CONF_TITLE_TEMPLATE): cv.string,
303 vol.Optional(CONF_URI): cv.url,
304 vol.Optional(CONF_ICON): cv.string,
305 },
306 extra=vol.ALLOW_EXTRA,
307)
310ARCHIVE_SCHEMA = vol.Schema({
311 vol.Optional(CONF_ARCHIVE_PATH): cv.path,
312 vol.Optional(CONF_ENABLED, default=False): cv.boolean, # type: ignore
313 vol.Optional(CONF_ARCHIVE_DAYS, default=3): cv.positive_int, # type: ignore
314})
316HOUSEKEEPING_SCHEMA = vol.Schema({
317 vol.Optional(CONF_HOUSEKEEPING_TIME, default="00:00:01"): cv.time, # type: ignore
318})
320PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
321 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path, # type: ignore
322 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path, # type: ignore
323 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA, # type: ignore
324 vol.Optional(CONF_HOUSEKEEPING, default=dict): HOUSEKEEPING_SCHEMA, # type: ignore
325 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA, # type: ignore
326 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_SCHEMA}, # type: ignore
327 vol.Optional(CONF_ACTION_GROUPS, default=dict): {cv.string: [ACTION_SCHEMA]}, # type: ignore
328 vol.Optional(CONF_RECIPIENTS, default=list): vol.All(cv.ensure_list, [RECIPIENT_SCHEMA]), # type: ignore # type: ignore
329 vol.Optional(CONF_LINKS, default=list): vol.All(cv.ensure_list, [LINK_SCHEMA]), # type: ignore
330 vol.Optional(CONF_SCENARIOS, default=dict): {cv.string: SCENARIO_SCHEMA}, # type: ignore
331 vol.Optional(CONF_METHODS, default=dict): {cv.string: METHOD_DEFAULTS_SCHEMA}, # type: ignore
332 vol.Optional(CONF_CAMERAS, default=list): vol.All(cv.ensure_list, [CAMERA_SCHEMA]), # type: ignore
333})
335ACTION_DATA_SCHEMA = vol.Schema(
336 {
337 vol.Optional(ATTR_DELIVERY): vol.Any(cv.string, [cv.string], {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}),
338 vol.Optional(ATTR_PRIORITY): vol.In(PRIORITY_VALUES),
339 vol.Optional(ATTR_SCENARIOS_CONSTRAIN): vol.All(cv.ensure_list, [cv.string]),
340 vol.Optional(ATTR_SCENARIOS_APPLY): vol.All(cv.ensure_list, [cv.string]),
341 vol.Optional(ATTR_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
342 vol.Optional(ATTR_RECIPIENTS): vol.All(cv.ensure_list, [cv.entity_id]),
343 vol.Optional(ATTR_MEDIA): MEDIA_SCHEMA,
344 vol.Optional(ATTR_MESSAGE_HTML): cv.string,
345 vol.Optional(ATTR_ACTION_GROUPS, default=[]): vol.All(cv.ensure_list, [cv.string]), # type: ignore
346 vol.Optional(ATTR_ACTIONS, default=[]): vol.All(cv.ensure_list, [ACTION_CALL_SCHEMA]), # type: ignore
347 vol.Optional(ATTR_DEBUG, default=False): cv.boolean, # type: ignore
348 vol.Optional(ATTR_DATA): vol.Any(None, DATA_SCHEMA),
349 },
350 extra=vol.ALLOW_EXTRA, # allow other data, e.g. the android/ios mobile push
351)
353STRICT_ACTION_DATA_SCHEMA = ACTION_DATA_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA)
356class TargetType(StrEnum):
357 pass
360class GlobalTargetType(TargetType):
361 NONCRITICAL = "NONCRITICAL"
362 EVERYTHING = "EVERYTHING"
365class RecipientType(StrEnum):
366 USER = "USER"
367 EVERYONE = "EVERYONE"
370class QualifiedTargetType(TargetType):
371 METHOD = "METHOD"
372 DELIVERY = "DELIVERY"
373 CAMERA = "CAMERA"
374 PRIORITY = "PRIORITY"
375 ACTION = "ACTION"
378class CommandType(StrEnum):
379 SNOOZE = "SNOOZE"
380 SILENCE = "SILENCE"
381 NORMAL = "NORMAL"
384@dataclass
385class ConditionVariables:
386 """Variables presented to all condition evaluations
388 Attributes
389 ----------
390 applied_scenarios (list[str]): Scenarios that have been applied
391 required_scenarios (list[str]): Scenarios that must be applied
392 notification_priority (str): Priority of the notification
393 notification_message (str): Message of the notification
394 notification_title (str): Title of the notification
395 occupancy (list[str]): List of occupancy scenarios
397 """
399 applied_scenarios: list[str] = field(default_factory=list)
400 required_scenarios: list[str] = field(default_factory=list)
401 notification_priority: str = PRIORITY_MEDIUM
402 notification_message: str = ""
403 notification_title: str = ""
404 occupancy: list[str] = field(default_factory=list)
406 def __init__(
407 self,
408 applied_scenarios: list[str] | None = None,
409 required_scenarios: list[str] | None = None,
410 delivery_priority: str | None = PRIORITY_MEDIUM,
411 occupiers: dict | None = None,
412 message: str | None = None,
413 title: str | None = None,
414 ) -> None:
415 occupiers = occupiers or {}
416 self.occupancy = []
417 if not occupiers.get(STATE_NOT_HOME) and occupiers.get(STATE_HOME):
418 self.occupancy.append("ALL_HOME")
419 elif occupiers.get(STATE_NOT_HOME) and not occupiers.get(STATE_HOME):
420 self.occupancy.append("ALL_AWAY")
421 if len(occupiers.get(STATE_HOME, [])) == 1:
422 self.occupancy.extend(["LONE_HOME", "SOME_HOME"])
423 elif len(occupiers.get(STATE_HOME, [])) > 1 and occupiers.get(STATE_NOT_HOME):
424 self.occupancy.extend(["MULTI_HOME", "SOME_HOME"])
425 self.applied_scenarios = applied_scenarios or []
426 self.required_scenarios = required_scenarios or []
427 self.notification_priority = delivery_priority or PRIORITY_MEDIUM
428 self.notification_message = message or ""
429 self.notification_title = title or ""