Coverage for custom_components/supernotify/__init__.py: 99%
197 statements
« prev ^ index » next coverage.py v7.6.8, created at 2025-05-31 23:25 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2025-05-31 23:25 +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_REQUIRE = "require_scenarios"
102ATTR_SCENARIOS_APPLY = "apply_scenarios"
103ATTR_SCENARIOS_CONSTRAIN = "constrain_scenarios"
104ATTR_DELIVERY = "delivery"
105ATTR_DEFAULT = "default"
106ATTR_NOTIFICATION_ID = "notification_id"
107ATTR_DELIVERY_SELECTION = "delivery_selection"
108ATTR_RECIPIENTS = "recipients"
109ATTR_DATA = "data"
110ATTR_MEDIA = "media"
111ATTR_TITLE = "title"
112ATTR_MEDIA_SNAPSHOT_URL = "snapshot_url"
113ATTR_MEDIA_CAMERA_ENTITY_ID = "camera_entity_id"
114ATTR_MEDIA_CAMERA_DELAY = "camera_delay"
115ATTR_MEDIA_CAMERA_PTZ_PRESET = "camera_ptz_preset"
116ATTR_MEDIA_CLIP_URL = "clip_url"
117ATTR_ACTION_GROUPS = "action_groups"
118CONF_ACTION_GROUP_NAMES = "action_groups"
119ATTR_ACTION_CATEGORY = "action_category"
120ATTR_ACTION_URL = "action_url"
121ATTR_ACTION_URL_TITLE = "action_url_title"
122ATTR_MESSAGE_HTML = "message_html"
123ATTR_JPEG_FLAGS = "jpeg_flags"
124ATTR_TIMESTAMP = "timestamp"
125ATTR_DEBUG = "debug"
126ATTR_ACTIONS = "actions"
127ATTR_USER_ID = "user_id"
129DELIVERY_SELECTION_IMPLICIT = "implicit"
130DELIVERY_SELECTION_EXPLICIT = "explicit"
131DELIVERY_SELECTION_FIXED = "fixed"
133DELIVERY_SELECTION_VALUES = [DELIVERY_SELECTION_EXPLICIT, DELIVERY_SELECTION_FIXED, DELIVERY_SELECTION_IMPLICIT]
134PTZ_METHOD_ONVIF = "onvif"
135PTZ_METHOD_FRIGATE = "frigate"
136PTZ_METHOD_VALUES = [PTZ_METHOD_ONVIF, PTZ_METHOD_FRIGATE]
138SELECTION_FALLBACK_ON_ERROR = "fallback_on_error"
139SELECTION_FALLBACK = "fallback"
140SELECTION_BY_SCENARIO = "scenario"
141SELECTION_DEFAULT = "default"
142SELECTION_VALUES = [SELECTION_FALLBACK_ON_ERROR, SELECTION_BY_SCENARIO, SELECTION_DEFAULT, SELECTION_FALLBACK]
144OCCUPANCY_VALUES = [
145 OCCUPANCY_ALL_IN,
146 OCCUPANCY_ALL_OUT,
147 OCCUPANCY_ANY_IN,
148 OCCUPANCY_ANY_OUT,
149 OCCUPANCY_ONLY_IN,
150 OCCUPANCY_ONLY_OUT,
151 OCCUPANCY_ALL,
152 OCCUPANCY_NONE,
153]
155PRIORITY_CRITICAL = "critical"
156PRIORITY_HIGH = "high"
157PRIORITY_MEDIUM = "medium"
158PRIORITY_LOW = "low"
160PRIORITY_VALUES = [PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH, PRIORITY_CRITICAL]
161METHOD_SMS = "sms"
162METHOD_EMAIL = "email"
163METHOD_ALEXA = "alexa"
164METHOD_MOBILE_PUSH = "mobile_push"
165METHOD_MEDIA = "media"
166METHOD_CHIME = "chime"
167METHOD_GENERIC = "generic"
168METHOD_PERSISTENT = "persistent"
169METHOD_VALUES = [
170 METHOD_SMS,
171 METHOD_ALEXA,
172 METHOD_MOBILE_PUSH,
173 METHOD_CHIME,
174 METHOD_EMAIL,
175 METHOD_MEDIA,
176 METHOD_PERSISTENT,
177 METHOD_GENERIC,
178]
180SCENARIO_DEFAULT = "DEFAULT"
181SCENARIO_NULL = "NULL"
183RESERVED_DELIVERY_NAMES = ["ALL"]
184RESERVED_SCENARIO_NAMES = [SCENARIO_DEFAULT, SCENARIO_NULL]
185RESERVED_DATA_KEYS = [ATTR_DOMAIN, ATTR_SERVICE, "action"]
187CONF_DUPE_CHECK = "dupe_check"
188CONF_DUPE_POLICY = "dupe_policy"
189CONF_TTL = "ttl"
190CONF_SIZE = "size"
191ATTR_DUPE_POLICY_MTSLP = "dupe_policy_message_title_same_or_lower_priority"
192ATTR_DUPE_POLICY_NONE = "dupe_policy_none"
194DATA_SCHEMA = vol.Schema({vol.NotIn(RESERVED_DATA_KEYS): vol.Any(str, int, bool, float, dict, list)})
195MOBILE_DEVICE_SCHEMA = vol.Schema({
196 vol.Optional(CONF_MANUFACTURER): cv.string,
197 vol.Optional(CONF_MODEL): cv.string,
198 vol.Optional(CONF_NOTIFY_ACTION): cv.string,
199 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
200})
201NOTIFICATION_DUPE_SCHEMA = vol.Schema({
202 vol.Optional(CONF_TTL): cv.positive_int,
203 vol.Optional(CONF_SIZE, default=100): cv.positive_int, # type: ignore
204 vol.Optional(CONF_DUPE_POLICY, default=ATTR_DUPE_POLICY_MTSLP): vol.In( # type: ignore
205 [ATTR_DUPE_POLICY_MTSLP, ATTR_DUPE_POLICY_NONE]
206 ), # type: ignore
207})
208DELIVERY_CUSTOMIZE_SCHEMA = vol.Schema({
209 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
210 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
211 vol.Optional(CONF_ENABLED, default=True): cv.boolean, # type: ignore
212 vol.Optional(CONF_DATA): DATA_SCHEMA,
213})
214LINK_SCHEMA = vol.Schema({
215 vol.Optional(CONF_ID): cv.string,
216 vol.Required(CONF_URL): cv.url,
217 vol.Optional(CONF_ICON): cv.icon,
218 vol.Required(CONF_DESCRIPTION): cv.string,
219 vol.Optional(CONF_NAME): cv.string,
220})
221METHOD_DEFAULTS_SCHEMA = vol.Schema({
222 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
223 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
224 vol.Optional(CONF_ACTION): cv.service,
225 vol.Optional(CONF_TARGETS_REQUIRED): cv.boolean,
226 vol.Optional(CONF_OPTIONS, default=dict): dict, # type: ignore
227 vol.Optional(CONF_DATA): DATA_SCHEMA,
228})
229RECIPIENT_SCHEMA = vol.Schema({
230 vol.Required(CONF_PERSON): cv.entity_id,
231 vol.Optional(CONF_ALIAS): cv.string,
232 vol.Optional(CONF_EMAIL): cv.string,
233 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
234 vol.Optional(CONF_PHONE_NUMBER): cv.string,
235 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean, # type: ignore
236 vol.Optional(CONF_MOBILE_DEVICES, default=list): vol.All(cv.ensure_list, [MOBILE_DEVICE_SCHEMA]), # type: ignore
237 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_CUSTOMIZE_SCHEMA}, # type: ignore
238})
239CAMERA_SCHEMA = vol.Schema({
240 vol.Required(CONF_CAMERA): cv.entity_id,
241 vol.Optional(CONF_ALT_CAMERA): vol.All(cv.ensure_list, [cv.entity_id]),
242 vol.Optional(CONF_ALIAS): cv.string,
243 vol.Optional(CONF_URL): cv.url,
244 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
245 vol.Optional(CONF_PTZ_PRESET_DEFAULT, default=1): vol.Any(cv.positive_int, cv.string), # type: ignore
246 vol.Optional(CONF_PTZ_DELAY, default=0): int, # type: ignore
247 vol.Optional(CONF_PTZ_METHOD, default=PTZ_METHOD_ONVIF): vol.In(PTZ_METHOD_VALUES), # type: ignore
248})
249MEDIA_SCHEMA = vol.Schema({
250 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id,
251 vol.Optional(ATTR_MEDIA_CAMERA_DELAY, default=0): int, # type: ignore
252 vol.Optional(ATTR_MEDIA_CAMERA_PTZ_PRESET): vol.Any(cv.positive_int, cv.string),
253 # URL fragments allowed
254 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string),
255 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string),
256 vol.Optional(ATTR_JPEG_FLAGS): dict,
257})
258DELIVERY_SCHEMA = vol.Schema({
259 vol.Optional(CONF_ALIAS): cv.string,
260 vol.Required(CONF_METHOD): vol.In(METHOD_VALUES),
261 vol.Optional(CONF_ACTION): cv.service, # previously 'service:'
262 vol.Optional(CONF_PLATFORM): cv.string,
263 vol.Optional(CONF_TEMPLATE): cv.string,
264 vol.Optional(CONF_DEFAULT, default=False): cv.boolean, # type: ignore
265 vol.Optional(CONF_SELECTION, default=[SELECTION_DEFAULT]): vol.All( # type: ignore
266 cv.ensure_list, [vol.In(SELECTION_VALUES)]
267 ),
268 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
269 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
270 vol.Optional(CONF_MESSAGE): vol.Any(None, cv.string),
271 vol.Optional(CONF_TITLE): vol.Any(None, cv.string),
272 vol.Optional(CONF_DATA): DATA_SCHEMA,
273 vol.Optional(CONF_ENABLED, default=True): cv.boolean, # type: ignore # type: ignore
274 vol.Optional(CONF_OPTIONS, default=dict): dict, # type: ignore
275 vol.Optional(CONF_PRIORITY, default=PRIORITY_VALUES): vol.All( # type: ignore
276 cv.ensure_list, [vol.In(PRIORITY_VALUES)]
277 ),
278 vol.Optional(CONF_OCCUPANCY, default=OCCUPANCY_ALL): vol.In(OCCUPANCY_VALUES), # type: ignore
279 vol.Optional(CONF_CONDITION): cv.CONDITION_SCHEMA,
280})
282SCENARIO_SCHEMA = vol.Schema({
283 vol.Optional(CONF_ALIAS): cv.string,
284 vol.Optional(CONF_CONDITION): cv.CONDITION_SCHEMA,
285 vol.Optional(CONF_MEDIA): MEDIA_SCHEMA,
286 vol.Optional(CONF_ACTION_GROUP_NAMES, default=[]): vol.All(cv.ensure_list, [cv.string]), # type: ignore
287 vol.Optional(CONF_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
288 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}, # type: ignore
289})
290ACTION_CALL_SCHEMA = vol.Schema(
291 {
292 vol.Optional(ATTR_ACTION): cv.string,
293 vol.Optional(ATTR_TITLE): cv.string,
294 vol.Optional(ATTR_ACTION_CATEGORY): cv.string,
295 vol.Optional(ATTR_ACTION_URL): cv.url,
296 vol.Optional(ATTR_ACTION_URL_TITLE): cv.string,
297 },
298 extra=vol.ALLOW_EXTRA,
299)
300ACTION_SCHEMA = vol.Schema(
301 {
302 vol.Exclusive(CONF_ACTION, CONF_ACTION_TEMPLATE): cv.string,
303 vol.Exclusive(CONF_TITLE, CONF_TITLE_TEMPLATE): cv.string,
304 vol.Optional(CONF_URI): cv.url,
305 vol.Optional(CONF_ICON): cv.string,
306 },
307 extra=vol.ALLOW_EXTRA,
308)
311ARCHIVE_SCHEMA = vol.Schema({
312 vol.Optional(CONF_ARCHIVE_PATH): cv.path,
313 vol.Optional(CONF_ENABLED, default=False): cv.boolean, # type: ignore
314 vol.Optional(CONF_ARCHIVE_DAYS, default=3): cv.positive_int, # type: ignore
315})
317HOUSEKEEPING_SCHEMA = vol.Schema({
318 vol.Optional(CONF_HOUSEKEEPING_TIME, default="00:00:01"): cv.time, # type: ignore
319})
321PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
322 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path, # type: ignore
323 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path, # type: ignore
324 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA, # type: ignore
325 vol.Optional(CONF_HOUSEKEEPING, default=dict): HOUSEKEEPING_SCHEMA, # type: ignore
326 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA, # type: ignore
327 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_SCHEMA}, # type: ignore
328 vol.Optional(CONF_ACTION_GROUPS, default=dict): {cv.string: [ACTION_SCHEMA]}, # type: ignore
329 vol.Optional(CONF_RECIPIENTS, default=list): vol.All(cv.ensure_list, [RECIPIENT_SCHEMA]), # type: ignore # type: ignore
330 vol.Optional(CONF_LINKS, default=list): vol.All(cv.ensure_list, [LINK_SCHEMA]), # type: ignore
331 vol.Optional(CONF_SCENARIOS, default=dict): {cv.string: SCENARIO_SCHEMA}, # type: ignore
332 vol.Optional(CONF_METHODS, default=dict): {cv.string: METHOD_DEFAULTS_SCHEMA}, # type: ignore
333 vol.Optional(CONF_CAMERAS, default=list): vol.All(cv.ensure_list, [CAMERA_SCHEMA]), # type: ignore
334})
336ACTION_DATA_SCHEMA = vol.Schema(
337 {
338 vol.Optional(ATTR_DELIVERY): vol.Any(cv.string, [cv.string], {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}),
339 vol.Optional(ATTR_PRIORITY): vol.In(PRIORITY_VALUES),
340 vol.Optional(ATTR_SCENARIOS_REQUIRE): vol.All(cv.ensure_list, [cv.string]),
341 vol.Optional(ATTR_SCENARIOS_APPLY): vol.All(cv.ensure_list, [cv.string]),
342 vol.Optional(ATTR_SCENARIOS_CONSTRAIN): vol.All(cv.ensure_list, [cv.string]),
343 vol.Optional(ATTR_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
344 vol.Optional(ATTR_RECIPIENTS): vol.All(cv.ensure_list, [cv.entity_id]),
345 vol.Optional(ATTR_MEDIA): MEDIA_SCHEMA,
346 vol.Optional(ATTR_MESSAGE_HTML): cv.string,
347 vol.Optional(ATTR_ACTION_GROUPS, default=[]): vol.All(cv.ensure_list, [cv.string]), # type: ignore
348 vol.Optional(ATTR_ACTIONS, default=[]): vol.All(cv.ensure_list, [ACTION_CALL_SCHEMA]), # type: ignore
349 vol.Optional(ATTR_DEBUG, default=False): cv.boolean, # type: ignore
350 vol.Optional(ATTR_DATA): vol.Any(None, DATA_SCHEMA),
351 },
352 extra=vol.ALLOW_EXTRA, # allow other data, e.g. the android/ios mobile push
353)
355STRICT_ACTION_DATA_SCHEMA = ACTION_DATA_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA)
358class TargetType(StrEnum):
359 pass
362class GlobalTargetType(TargetType):
363 NONCRITICAL = "NONCRITICAL"
364 EVERYTHING = "EVERYTHING"
367class RecipientType(StrEnum):
368 USER = "USER"
369 EVERYONE = "EVERYONE"
372class QualifiedTargetType(TargetType):
373 METHOD = "METHOD"
374 DELIVERY = "DELIVERY"
375 CAMERA = "CAMERA"
376 PRIORITY = "PRIORITY"
377 ACTION = "ACTION"
380class CommandType(StrEnum):
381 SNOOZE = "SNOOZE"
382 SILENCE = "SILENCE"
383 NORMAL = "NORMAL"
386@dataclass
387class ConditionVariables:
388 """Variables presented to all condition evaluations
390 Attributes
391 ----------
392 applied_scenarios (list[str]): Scenarios that have been applied
393 required_scenarios (list[str]): Scenarios that must be applied
394 constrain_scenarios (list[str]): Only scenarios in this list, or in explicit apply_scenarios, can be applied
395 notification_priority (str): Priority of the notification
396 notification_message (str): Message of the notification
397 notification_title (str): Title of the notification
398 occupancy (list[str]): List of occupancy scenarios
400 """
402 applied_scenarios: list[str] = field(default_factory=list)
403 required_scenarios: list[str] = field(default_factory=list)
404 constrain_scenarios: list[str] = field(default_factory=list)
405 notification_priority: str = PRIORITY_MEDIUM
406 notification_message: str = ""
407 notification_title: str = ""
408 occupancy: list[str] = field(default_factory=list)
410 def __init__(
411 self,
412 applied_scenarios: list[str] | None = None,
413 required_scenarios: list[str] | None = None,
414 constrain_scenarios: list[str] | None = None,
415 delivery_priority: str | None = PRIORITY_MEDIUM,
416 occupiers: dict | None = None,
417 message: str | None = None,
418 title: str | None = None,
419 ) -> None:
420 occupiers = occupiers or {}
421 self.occupancy = []
422 if not occupiers.get(STATE_NOT_HOME) and occupiers.get(STATE_HOME):
423 self.occupancy.append("ALL_HOME")
424 elif occupiers.get(STATE_NOT_HOME) and not occupiers.get(STATE_HOME):
425 self.occupancy.append("ALL_AWAY")
426 if len(occupiers.get(STATE_HOME, [])) == 1:
427 self.occupancy.extend(["LONE_HOME", "SOME_HOME"])
428 elif len(occupiers.get(STATE_HOME, [])) > 1 and occupiers.get(STATE_NOT_HOME):
429 self.occupancy.extend(["MULTI_HOME", "SOME_HOME"])
430 self.applied_scenarios = applied_scenarios or []
431 self.required_scenarios = required_scenarios or []
432 self.constrain_scenarios = constrain_scenarios or []
433 self.notification_priority = delivery_priority or PRIORITY_MEDIUM
434 self.notification_message = message or ""
435 self.notification_title = title or ""