Coverage for custom_components/supernotify/schema.py: 95%
96 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-25 21:14 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-25 21:14 +0000
1"""The Supernotify integration"""
3import re
4from collections.abc import Callable
5from enum import IntFlag, StrEnum, auto
7import voluptuous as vol
8from homeassistant.const import (
9 CONF_ACTION,
10 CONF_ALIAS,
11 CONF_CONDITION,
12 CONF_CONDITIONS,
13 CONF_DEBUG,
14 CONF_DESCRIPTION,
15 CONF_EMAIL,
16 CONF_ENABLED,
17 CONF_HOST,
18 CONF_ICON,
19 CONF_ID,
20 CONF_NAME,
21 CONF_PASSWORD,
22 CONF_PORT,
23 CONF_TARGET,
24 CONF_TIMEOUT,
25 CONF_URL,
26 CONF_USERNAME,
27 CONF_VERIFY_SSL,
28)
29from homeassistant.helpers import config_validation as cv
30from homeassistant.helpers.typing import TemplateVarsType
32from custom_components.supernotify import ARCHIVE_DIR, MEDIA_DIR, TEMPLATE_DIR
34from .const import (
35 ATTR_ACTION,
36 ATTR_ACTION_GROUPS,
37 ATTR_ACTION_URL,
38 ATTR_ACTION_URL_TITLE,
39 ATTR_ACTIONS,
40 ATTR_CUSTOM_TARGET,
41 ATTR_DATA,
42 ATTR_DEBUG,
43 ATTR_DELIVERY,
44 ATTR_DELIVERY_CONTROL,
45 ATTR_DELIVERY_SELECTION,
46 ATTR_DUPE_POLICY_MT,
47 ATTR_DUPE_POLICY_MTSLP,
48 ATTR_DUPE_POLICY_NONE,
49 ATTR_EMAIL,
50 ATTR_EXTRA_DATA,
51 ATTR_FORCE_RESEND,
52 ATTR_JPEG_OPTS,
53 ATTR_MEDIA,
54 ATTR_MEDIA_CAMERA_DELAY,
55 ATTR_MEDIA_CAMERA_ENTITY_ID,
56 ATTR_MEDIA_CAMERA_PTZ_PRESET,
57 ATTR_MEDIA_CLIP_URL,
58 ATTR_MEDIA_SNAPSHOT_PATH,
59 ATTR_MEDIA_SNAPSHOT_URL,
60 ATTR_MESSAGE_HTML,
61 ATTR_MOBILE_APP_ID,
62 ATTR_PERSON_ID,
63 ATTR_PHONE,
64 ATTR_PNG_OPTS,
65 ATTR_PRIORITY,
66 ATTR_RECIPIENTS,
67 ATTR_SCENARIOS_APPLY,
68 ATTR_SCENARIOS_CONSTRAIN,
69 ATTR_SCENARIOS_REQUIRE,
70 ATTR_SPOKEN_MESSAGE,
71 ATTR_TIMESTAMP,
72 ATTR_TITLE,
73 CONF_ACTION_GROUP_NAMES,
74 CONF_ACTION_GROUPS,
75 CONF_ACTION_TEMPLATE,
76 CONF_ALT_CAMERA,
77 CONF_ARCHIVE,
78 CONF_ARCHIVE_DAYS,
79 CONF_ARCHIVE_DIAGNOSTICS,
80 CONF_ARCHIVE_EVENT_NAME,
81 CONF_ARCHIVE_EVENT_SELECTION,
82 CONF_ARCHIVE_MQTT_QOS,
83 CONF_ARCHIVE_MQTT_RETAIN,
84 CONF_ARCHIVE_MQTT_TOPIC,
85 CONF_ARCHIVE_PATH,
86 CONF_ARCHIVE_PURGE_INTERVAL,
87 CONF_CAMERA,
88 CONF_CAMERAS,
89 CONF_CLASS,
90 CONF_CONNECTION,
91 CONF_DATA,
92 CONF_DEFAULT_INCLUSION,
93 CONF_DELIVERY,
94 CONF_DELIVERY_CONTROL,
95 CONF_DELIVERY_DEFAULTS,
96 CONF_DEVICE_DISCOVERY,
97 CONF_DEVICE_DOMAIN,
98 CONF_DEVICE_MODEL_EXCLUDE,
99 CONF_DEVICE_MODEL_INCLUDE,
100 CONF_DEVICE_TRACKER,
101 CONF_DUPE_CHECK,
102 CONF_DUPE_POLICY,
103 CONF_ENCRYPTION,
104 CONF_EXPOSE_STATE,
105 CONF_HOUSEKEEPING,
106 CONF_HOUSEKEEPING_TIME,
107 CONF_INCLUSION,
108 CONF_LINKS,
109 CONF_LOAD,
110 CONF_MANUFACTURER,
111 CONF_MEDIA,
112 CONF_MEDIA_PATH,
113 CONF_MEDIA_STORAGE_DAYS,
114 CONF_MEDIA_URL_PREFIX,
115 CONF_MESSAGE,
116 CONF_MOBILE_APP_ID,
117 CONF_MOBILE_DEVICES,
118 CONF_MOBILE_DISCOVERY,
119 CONF_MODEL,
120 CONF_OCCUPANCY,
121 CONF_OPTIONS,
122 CONF_PERSON,
123 CONF_PHONE_NUMBER,
124 CONF_PRIORITY,
125 CONF_PTZ_CAMERA,
126 CONF_PTZ_DELAY,
127 CONF_PTZ_METHOD,
128 CONF_PTZ_PRESET_DEFAULT,
129 CONF_RECIPIENTS,
130 CONF_RECIPIENTS_DISCOVERY,
131 CONF_REFRESH,
132 CONF_REFRESH_INTERVAL,
133 CONF_SCENARIO_CONTROL,
134 CONF_SCENARIOS,
135 CONF_SELECTION,
136 CONF_SELECTION_RANK,
137 CONF_SIZE,
138 CONF_SNAP_WAIT,
139 CONF_SNOOZE,
140 CONF_SNOOZE_TIME,
141 CONF_TARGET_REQUIRED,
142 CONF_TARGET_USAGE,
143 CONF_TEMPLATE,
144 CONF_TEMPLATE_PATH,
145 CONF_TITLE,
146 CONF_TITLE_TEMPLATE,
147 CONF_TRANSPORT,
148 CONF_TRANSPORTS,
149 CONF_TTL,
150 CONF_URI,
151 CONF_USER_ID,
152 CONF_VOICE_OCCUPANCY,
153 DEFAULT_INCLUSION_VALUES,
154 DELIVERY_SELECTION_VALUES,
155 INCLUSION_VALUES,
156 OCCUPANCY_VALUES,
157 PRIORITY_VALUES,
158 PTZ_DELAY_DEFAULT,
159 PTZ_METHOD_ONVIF,
160 PTZ_METHOD_VALUES,
161 RESERVED_DATA_KEYS,
162 RESERVED_SCENARIO_NAMES,
163 SCENARIO_STATE_REFRESH_DEFAULT,
164 SNAP_WAIT_DEFAULT,
165 TARGET_REQUIRE_ALWAYS,
166 TARGET_REQUIRE_NEVER,
167 TARGET_REQUIRE_OPTIONAL,
168 TARGET_USE_FIXED,
169 TARGET_USE_MERGE_ALWAYS,
170 TARGET_USE_MERGE_ON_DELIVERY_TARGETS,
171 TARGET_USE_ON_NO_ACTION_TARGETS,
172 TARGET_USE_ON_NO_DELIVERY_TARGETS,
173 TRANSPORT_VALUES,
174)
177class OutcomeSelection(IntFlag):
178 NONE = 0
179 SUCCESS = 1
180 NO_DELIVERY = 2
181 PARTIAL_DELIVERY = 4
182 FALLBACK_DELIVERY = 8
183 ERROR = 16
184 DUPE = 32
185 ALL = 65536
188def parse_event_policy(value: object) -> OutcomeSelection:
189 if isinstance(value, OutcomeSelection):
190 return value
191 if isinstance(value, int):
192 return OutcomeSelection(value)
193 if isinstance(value, str):
194 result = OutcomeSelection.NONE
195 for part in value.split("|"):
196 result |= OutcomeSelection[part.strip()]
197 return result
198 raise vol.Invalid(f"expected OutcomeSelection, got {value!r}")
201class DeliveryOutcome(StrEnum):
202 SUCCESS = auto()
203 NO_DELIVERY = auto()
204 PARTIAL_DELIVERY = auto()
205 FALLBACK_DELIVERY = auto()
206 ERROR = auto()
207 DUPE = auto()
210class EnvelopeOutcome(StrEnum):
211 SUCCESS = auto()
212 ERROR = auto()
213 SKIPPED = auto() # envelope not generated
214 SUPPRESSED = auto() # envelope generated but intentionally not delivered
217class SelectionRank(StrEnum):
218 FIRST = "FIRST"
219 ANY = "ANY"
220 LAST = "LAST"
223type ConditionsFunc = Callable[[TemplateVarsType], bool]
226def phone(value: str) -> str:
227 """Validate a phone number"""
228 regex = re.compile(r"^(\+\d{1,3})?\s?\(?\d{1,4}\)?[\s.-]?\d{3}[\s.-]?\d{4}$")
229 if not regex.match(value):
230 raise vol.Invalid("Invalid Phone Number")
231 return str(value)
234def validate_scenario_names(scenarios: dict) -> dict:
235 """Validate that scenario names are not reserved."""
236 for name in scenarios:
237 if name in RESERVED_SCENARIO_NAMES:
238 raise vol.Invalid(f"'{name}' is a reserved scenario name")
239 return scenarios
242# TARGET_FIELDS includes entity, device, area, floor, label ids
243TARGET_SCHEMA = vol.Any( # order of schema matters, voluptuous forces into first it finds that works
244 cv.TARGET_FIELDS
245 | {
246 vol.Optional(ATTR_EMAIL): vol.All(cv.ensure_list, [vol.Email()]), # type: ignore[call-arg] # ty: ignore[missing-argument]
247 vol.Optional(ATTR_PHONE): vol.All(cv.ensure_list, [phone]),
248 vol.Optional(ATTR_MOBILE_APP_ID): vol.All(cv.ensure_list, [cv.service]),
249 vol.Optional(ATTR_PERSON_ID): vol.All(cv.ensure_list, [cv.entity_id]),
250 vol.Optional(cv.string): vol.All(cv.ensure_list, [str]),
251 },
252 str,
253 list[str],
254)
256DATA_SCHEMA = vol.Schema({vol.NotIn(RESERVED_DATA_KEYS): vol.Any(None, str, int, bool, float, dict, list)})
258MOBILE_DEVICE_SCHEMA = vol.Schema({
259 vol.Optional(CONF_MANUFACTURER): cv.string,
260 vol.Optional(CONF_MODEL): cv.string,
261 vol.Optional(CONF_CLASS): cv.string,
262 vol.Optional(CONF_MOBILE_APP_ID): cv.string,
263 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
264 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
265})
266NOTIFICATION_DUPE_SCHEMA = vol.Schema({
267 vol.Optional(CONF_TTL, default=120): cv.positive_int,
268 vol.Optional(CONF_SIZE, default=100): cv.positive_int,
269 vol.Optional(CONF_DUPE_POLICY, default=ATTR_DUPE_POLICY_MTSLP): vol.In([
270 ATTR_DUPE_POLICY_MTSLP,
271 ATTR_DUPE_POLICY_MT,
272 ATTR_DUPE_POLICY_NONE,
273 ]),
274})
277DELIVERY_CUSTOMIZE_SCHEMA = vol.All(
278 vol.Schema(
279 {
280 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
281 vol.Optional(CONF_ENABLED): vol.Any(None, cv.boolean),
282 vol.Optional(CONF_DATA): DATA_SCHEMA,
283 },
284 ),
285)
286LINK_SCHEMA = vol.Schema({
287 vol.Required(CONF_URL): cv.url,
288 vol.Required(CONF_DESCRIPTION): cv.string,
289 vol.Optional(CONF_ID): cv.string,
290 vol.Optional(CONF_ICON): cv.icon,
291 vol.Optional(CONF_NAME): cv.string,
292})
294# global scenario control options and defaults, advanced config, YAML only
295SCENARIO_CONTROL_SCHEMA = vol.Schema({
296 # Evaluating scenario conditions to publish binary_sensor state costs whatever the
297 # conditions cost, which for template-heavy scenarios is not free on small hardware.
298 vol.Optional(CONF_REFRESH, default=False): cv.boolean,
299 # Sweep every scenario this often, for conditions no entity change can announce (time
300 # windows, sun, templates whose dependencies could not be extracted). 0 disables the
301 # sweep and leaves state entirely event driven.
302 vol.Optional(CONF_REFRESH_INTERVAL, default=SCENARIO_STATE_REFRESH_DEFAULT): cv.positive_int,
303})
305SNOOZE_SCHEMA = vol.Schema({vol.Optional(CONF_SNOOZE_TIME, default=60 * 60): cv.positive_int})
307DELIVERY_CONFIG_SCHEMA = vol.Schema({ # shared by Transport Defaults and Delivery definitions
308 # defaults set in model.DeliveryConfig
309 vol.Optional(CONF_ACTION): cv.service, # previously 'service:'
310 vol.Optional(CONF_DEBUG): cv.boolean,
311 vol.Optional(CONF_OPTIONS): dict, # transport tuning
312 vol.Optional(CONF_DATA): DATA_SCHEMA,
313 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
314 vol.Optional(CONF_TARGET_REQUIRED): vol.Any(
315 cv.boolean, vol.In([TARGET_REQUIRE_ALWAYS, TARGET_REQUIRE_NEVER, TARGET_REQUIRE_OPTIONAL])
316 ),
317 vol.Optional(CONF_TARGET_USAGE): vol.In([
318 TARGET_USE_ON_NO_DELIVERY_TARGETS,
319 TARGET_USE_ON_NO_ACTION_TARGETS,
320 TARGET_USE_MERGE_ON_DELIVERY_TARGETS,
321 TARGET_USE_MERGE_ALWAYS,
322 TARGET_USE_FIXED,
323 ]),
324 vol.Optional(CONF_INCLUSION): vol.All(cv.ensure_list, [vol.In(INCLUSION_VALUES)]),
325 vol.Optional(CONF_PRIORITY): vol.All(cv.ensure_list, [vol.Any(int, str, vol.In(list(PRIORITY_VALUES.keys())))]),
326 vol.Optional(CONF_SELECTION_RANK): vol.In([
327 SelectionRank.ANY,
328 SelectionRank.FIRST,
329 SelectionRank.LAST,
330 ]),
331 # also settable at Delivery level - a Transport's delivery_defaults let these be
332 # configured once for its auto-generated delivery, without needing an explicit Delivery
333 vol.Optional(CONF_ALIAS): cv.string,
334 vol.Optional(CONF_TEMPLATE): cv.string,
335 vol.Optional(CONF_MESSAGE): vol.Any(None, cv.string),
336 vol.Optional(CONF_TITLE): vol.Any(None, cv.string),
337 # no default here, which would hide a transport's own - DeliveryConfig falls back to OCCUPANCY_ALL
338 vol.Optional(CONF_OCCUPANCY): vol.In(OCCUPANCY_VALUES),
339 vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
340})
343def _migrate_condition(config: dict) -> dict:
344 """Migrate deprecated 'condition' key to 'conditions'."""
345 if CONF_CONDITION in config:
346 if CONF_CONDITIONS not in config:
347 config = {**config, CONF_CONDITIONS: config[CONF_CONDITION]}
348 config = {k: v for k, v in config.items() if k != CONF_CONDITION}
349 return config
352DELIVERY_SCHEMA = vol.All(
353 _migrate_condition,
354 cv.deprecated(key=CONF_SELECTION, replacement_key=CONF_INCLUSION),
355 DELIVERY_CONFIG_SCHEMA.extend({
356 vol.Required(CONF_TRANSPORT): vol.In(TRANSPORT_VALUES),
357 vol.Optional(CONF_ENABLED): cv.boolean,
358 }),
359)
361CONNECTION_SCHEMA = vol.Schema({
362 vol.Required(CONF_HOST): cv.string,
363 vol.Optional(CONF_PORT, default=587): cv.port,
364 vol.Optional(CONF_ENCRYPTION, default="starttls"): vol.In(["tls", "starttls", "none"]),
365 vol.Optional(CONF_USERNAME): cv.string,
366 vol.Optional(CONF_PASSWORD): cv.string,
367 vol.Optional(CONF_TIMEOUT, default=5): cv.positive_int,
368 vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
369})
371TRANSPORT_SCHEMA = vol.All(
372 cv.deprecated(key=CONF_DEVICE_DOMAIN), # deprecated v1.9.0
373 cv.deprecated(key=CONF_DEVICE_DISCOVERY), # deprecated v1.9.0
374 cv.deprecated(key=CONF_DEVICE_MODEL_INCLUDE), # deprecated v1.9.0
375 cv.deprecated(key=CONF_DEVICE_MODEL_EXCLUDE), # deprecated v1.9.0
376 vol.Schema({
377 vol.Optional(CONF_ALIAS): cv.string,
378 vol.Optional(CONF_LOAD, default=True): cv.boolean,
379 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
380 vol.Optional(CONF_DELIVERY_DEFAULTS): vol.All(
381 cv.deprecated(key=CONF_SELECTION, replacement_key=CONF_INCLUSION), DELIVERY_CONFIG_SCHEMA
382 ),
383 # only meaningful for transports (eg smtp) that own their own network connection
384 vol.Optional(CONF_CONNECTION): CONNECTION_SCHEMA,
385 # deprecated, replaced by options usage
386 vol.Optional(CONF_DEVICE_DOMAIN): vol.All(cv.ensure_list, [cv.string]),
387 vol.Optional(CONF_DEVICE_MODEL_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
388 vol.Optional(CONF_DEVICE_MODEL_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
389 vol.Optional(CONF_DEVICE_DISCOVERY): cv.boolean,
390 }),
391)
392# Idea - differentiate enabled as recipient vs as occupant, for ALL_IN etc check
393# May need condition, and also enabled if delivery disabled
394# CONF_OCCUPANCY="occupancy"
395# OPTION_OCCUPANCY_DEFAULT="default"
396# OPTIONS_OCCUPANCY=[OPTION_OCCUPANCY_DEFAULT,OPTION_OCCUPANCY_EXCLUDE]
397# OPTION_OCCUPANCY_EXCLUDE="exclude"
400RECIPIENT_SCHEMA = vol.All(
401 vol.Schema({
402 vol.Optional(CONF_PERSON): cv.entity_id,
403 vol.Optional(CONF_USER_ID): cv.string,
404 vol.Optional(CONF_ALIAS): cv.string,
405 vol.Optional(CONF_EMAIL): cv.string,
406 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
407 # vol.Optional(CONF_OCCUPANCY,default=OPTION_OCCUPANCY_DEFAULT):vol.In(OPTIONS_OCCUPANCY),
408 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
409 vol.Optional(CONF_PHONE_NUMBER): cv.string,
410 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
411 vol.Optional(CONF_MOBILE_DEVICES, default=list): vol.All(cv.ensure_list, [MOBILE_DEVICE_SCHEMA]),
412 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_CUSTOMIZE_SCHEMA},
413 }),
414 cv.has_at_least_one_key(CONF_PERSON, CONF_USER_ID),
415)
416CAMERA_SCHEMA = vol.Schema({
417 vol.Required(CONF_CAMERA): cv.entity_id,
418 vol.Optional(CONF_ALT_CAMERA): vol.All(cv.ensure_list, [cv.entity_id]),
419 vol.Optional(CONF_ALIAS): cv.string,
420 vol.Optional(CONF_URL): cv.url,
421 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
422 vol.Optional(CONF_PTZ_CAMERA): cv.entity_id,
423 vol.Optional(CONF_PTZ_PRESET_DEFAULT, default=1): vol.Any(cv.positive_int, cv.string),
424 vol.Optional(CONF_PTZ_DELAY, default=PTZ_DELAY_DEFAULT): int,
425 vol.Optional(CONF_PTZ_METHOD, default=PTZ_METHOD_ONVIF): vol.In(PTZ_METHOD_VALUES),
426 vol.Optional(CONF_SNAP_WAIT, default=SNAP_WAIT_DEFAULT): int,
427})
428MEDIA_SCHEMA = vol.Schema({
429 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id,
430 vol.Optional(ATTR_MEDIA_CAMERA_DELAY): int,
431 vol.Optional(ATTR_MEDIA_CAMERA_PTZ_PRESET): vol.Any(cv.positive_int, cv.string),
432 # URL fragments allowed
433 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string),
434 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string),
435 vol.Optional(ATTR_MEDIA_SNAPSHOT_PATH): cv.path,
436 vol.Optional(ATTR_JPEG_OPTS): dict,
437 vol.Optional(ATTR_PNG_OPTS): dict,
438})
441SCENARIO_SCHEMA = vol.All(
442 _migrate_condition,
443 cv.deprecated(key="delivery_selection"),
444 vol.Schema({
445 vol.Optional(CONF_ALIAS): cv.string,
446 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
447 vol.Optional(CONF_EXPOSE_STATE, default=True): cv.boolean,
448 vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
449 vol.Optional(CONF_MEDIA): MEDIA_SCHEMA,
450 vol.Optional(CONF_ACTION_GROUP_NAMES, default=[]): vol.All(cv.ensure_list, [cv.string]),
451 vol.Optional("delivery_selection"): cv.string,
452 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)},
453 }),
454)
457def _mobile_action_uri(value: str) -> str:
458 """Validate URI for mobile actions — accepts http/https and homeassistant:// deep links."""
459 if value and not (value.startswith(("http://", "https://", "homeassistant://"))):
460 raise vol.Invalid(f"Invalid URI {value!r}: must start with http://, https://, or homeassistant://")
461 return value
464MOBILE_ACTION_CALL_SCHEMA = vol.All(
465 cv.deprecated(key="action_category"), # deprecated v2.5.0
466 vol.Schema(
467 {
468 vol.Optional(ATTR_ACTION): cv.string,
469 vol.Optional(ATTR_TITLE): cv.string,
470 vol.Optional("action_category"): cv.string,
471 vol.Optional(ATTR_ACTION_URL): cv.url,
472 vol.Optional(ATTR_ACTION_URL_TITLE): cv.string,
473 },
474 extra=vol.ALLOW_EXTRA,
475 ),
476)
478MOBILE_ACTION_SCHEMA = vol.Schema(
479 {
480 vol.Exclusive(CONF_ACTION, CONF_ACTION_TEMPLATE): cv.string,
481 vol.Exclusive(CONF_TITLE, CONF_TITLE_TEMPLATE): cv.string,
482 vol.Optional(CONF_URI): _mobile_action_uri,
483 vol.Optional(CONF_ICON): cv.string,
484 },
485 extra=vol.ALLOW_EXTRA,
486)
489ARCHIVE_SCHEMA = vol.All(
490 cv.deprecated(key=CONF_DEBUG), # deprecated v1.10.0
491 vol.Schema({
492 vol.Optional(CONF_ARCHIVE_PATH, default=ARCHIVE_DIR): cv.path,
493 vol.Optional(CONF_ENABLED, default=False): cv.boolean,
494 vol.Optional(CONF_ARCHIVE_DAYS, default=3): cv.positive_int,
495 vol.Optional(CONF_ARCHIVE_MQTT_TOPIC): cv.string,
496 vol.Optional(CONF_ARCHIVE_MQTT_QOS, default=0): cv.positive_int,
497 vol.Optional(CONF_ARCHIVE_MQTT_RETAIN, default=True): cv.boolean,
498 vol.Optional(CONF_ARCHIVE_PURGE_INTERVAL, default=60): cv.positive_int,
499 vol.Optional(CONF_ARCHIVE_EVENT_NAME, default="supernotification"): cv.string,
500 vol.Optional(CONF_ARCHIVE_EVENT_SELECTION, default=OutcomeSelection.NONE): parse_event_policy,
501 vol.Optional(CONF_ARCHIVE_DIAGNOSTICS, default=OutcomeSelection.ERROR): parse_event_policy,
502 vol.Optional(CONF_DEBUG, default=False): cv.boolean,
503 }),
504)
506HOUSEKEEPING_SCHEMA = vol.Schema({
507 vol.Optional(CONF_HOUSEKEEPING_TIME, default="00:00:01"): cv.time,
508 vol.Optional(CONF_MEDIA_STORAGE_DAYS, default=7): cv.positive_int,
509})
511# The 8 keys not yet configurable via ConfigFlow - defined under the top-level `supernotify:`
512# YAML key (see CONFIG_SCHEMA/async_setup in __init__.py), never under the legacy notify platform.
513# extra=ALLOW_EXTRA matches the old PLATFORM_SCHEMA-derived schema's inherited leniency (HA's
514# base notify PLATFORM_SCHEMA itself uses ALLOW_EXTRA) - stray/unrecognized keys pass through
515# rather than failing validation.
516SUPERNOTIFY_YAML_SCHEMA: vol.Schema = vol.Schema(
517 {
518 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_SCHEMA},
519 vol.Optional(CONF_ACTION_GROUPS, default=dict): {cv.string: [MOBILE_ACTION_SCHEMA]},
520 vol.Optional(CONF_RECIPIENTS, default=list): vol.All(cv.ensure_list, [RECIPIENT_SCHEMA]),
521 vol.Optional(CONF_LINKS, default=list): vol.All(cv.ensure_list, [LINK_SCHEMA]),
522 vol.Optional(CONF_SCENARIOS, default=dict): vol.All(
523 {cv.string: SCENARIO_SCHEMA},
524 validate_scenario_names,
525 ),
526 vol.Optional(CONF_TRANSPORTS, default=dict): {cv.string: TRANSPORT_SCHEMA},
527 vol.Optional(CONF_CAMERAS, default=list): vol.All(cv.ensure_list, [CAMERA_SCHEMA]),
528 vol.Optional(CONF_SNOOZE, default=dict): SNOOZE_SCHEMA,
529 vol.Optional(CONF_SCENARIO_CONTROL, default=dict): SCENARIO_CONTROL_SCHEMA,
530 },
531 extra=vol.ALLOW_EXTRA,
532)
534# The 8 ConfigEntry-owned keys only (entry.data/entry.options) - deliberately separate from
535# SUPERNOTIFY_YAML_SCHEMA rather than folded into one combined schema: __init__.py's
536# Defaults for every delivery, from the Delivery Control options page - each left out when not set,
537# so deliveries keep their transport's own default
538# REMOVE_EXTRA drops options no longer offered, such as the 2.10.0 beta's apple_drop_mp4
539DELIVERY_CONTROL_SCHEMA = vol.Schema(
540 {
541 vol.Optional(CONF_DEFAULT_INCLUSION): vol.In(DEFAULT_INCLUSION_VALUES),
542 vol.Optional(CONF_VOICE_OCCUPANCY): vol.In(OCCUPANCY_VALUES),
543 },
544 extra=vol.REMOVE_EXTRA,
545)
547# _entry_full_config validates entry.data/entry.options through this schema, then merges in the
548# top-level YAML section's already-validated dict as-is. Re-validating that already-validated
549# dict a second time (as a single combined schema would require) breaks - cv.template/
550# cv.CONDITIONS_SCHEMA coerce string templates into Template objects, and reject a Template
551# object the second time round ("template value should be a string").
552CONFIG_ENTRY_SCHEMA = vol.Schema(
553 {
554 vol.Optional(CONF_DELIVERY_CONTROL, default=dict): DELIVERY_CONTROL_SCHEMA,
555 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path,
556 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path,
557 vol.Optional(CONF_MEDIA_URL_PREFIX, default="/supernotify/media"): cv.string,
558 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA,
559 vol.Optional(CONF_HOUSEKEEPING, default={}): HOUSEKEEPING_SCHEMA,
560 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA,
561 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
562 vol.Optional(CONF_RECIPIENTS_DISCOVERY, default=True): cv.boolean,
563 },
564 extra=vol.ALLOW_EXTRA,
565)
567# The union of CONFIG_ENTRY_SCHEMA and SUPERNOTIFY_YAML_SCHEMA - for validating a single,
568# never-previously-validated raw config dict in one pass (e.g. hass_setup_lib.py's
569# TestingContext). Do NOT use this in __init__.py's _entry_full_config - see CONFIG_ENTRY_SCHEMA.
570FULL_CONFIG_SCHEMA = SUPERNOTIFY_YAML_SCHEMA.extend({
571 vol.Optional(CONF_DELIVERY_CONTROL, default=dict): DELIVERY_CONTROL_SCHEMA,
572 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path,
573 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path,
574 vol.Optional(CONF_MEDIA_URL_PREFIX, default="/supernotify/media"): cv.string,
575 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA,
576 vol.Optional(CONF_HOUSEKEEPING, default={}): HOUSEKEEPING_SCHEMA,
577 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA,
578 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
579 vol.Optional(CONF_RECIPIENTS_DISCOVERY, default=True): cv.boolean,
580})
583# A delivery name, a list of names to restrict to, or a mapping of names to tuning
584DELIVERY_FIELD_SCHEMA = vol.Any(cv.string, [cv.string], {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)})
586_ACTION_DATA_FIELDS_SCHEMA = vol.Schema(
587 {
588 vol.Optional(ATTR_DELIVERY): DELIVERY_FIELD_SCHEMA,
589 vol.Optional(ATTR_PRIORITY): vol.Any(int, str, vol.In(list(PRIORITY_VALUES.keys()))),
590 vol.Optional(ATTR_SCENARIOS_REQUIRE): vol.All(cv.ensure_list, [cv.string]),
591 vol.Optional(ATTR_SCENARIOS_APPLY): vol.All(cv.ensure_list, [cv.string]),
592 vol.Optional(ATTR_SCENARIOS_CONSTRAIN): vol.All(cv.ensure_list, [cv.string]),
593 vol.Optional(ATTR_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
594 vol.Optional(ATTR_RECIPIENTS): vol.All(cv.ensure_list, [cv.entity_id]),
595 vol.Optional(ATTR_MEDIA): MEDIA_SCHEMA,
596 vol.Optional(ATTR_MESSAGE_HTML): cv.string,
597 vol.Optional(ATTR_SPOKEN_MESSAGE): cv.string,
598 vol.Optional(ATTR_ACTION_GROUPS, default=[]): vol.All(cv.ensure_list, [cv.string]),
599 vol.Optional(ATTR_ACTIONS, default=[]): vol.All(cv.ensure_list, [MOBILE_ACTION_CALL_SCHEMA]),
600 vol.Optional(ATTR_DEBUG, default=False): cv.boolean,
601 vol.Optional(ATTR_FORCE_RESEND, default=False): cv.boolean,
602 vol.Optional(ATTR_DATA): vol.Any(None, DATA_SCHEMA),
603 vol.Optional(ATTR_TIMESTAMP): cv.string,
604 },
605 extra=vol.ALLOW_EXTRA, # allow other data, e.g. the android/ios mobile push
606)
608ACTION_DATA_FIELDS: frozenset[str] = frozenset(str(key) for key in _ACTION_DATA_FIELDS_SCHEMA.schema)
610ACTION_DATA_SCHEMA = vol.All(
611 cv.deprecated(key=ATTR_RECIPIENTS), # deprecated v2.2.0
612 _ACTION_DATA_FIELDS_SCHEMA,
613)
615STRICT_ACTION_DATA_SCHEMA = vol.All(
616 cv.deprecated(key=ATTR_RECIPIENTS), # deprecated v2.2.0
617 _ACTION_DATA_FIELDS_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA),
618)
620# Schema for the supernotify.notify action - the same fields as ACTION_DATA_SCHEMA (normally
621# buried in notify.supernotify's generic `data:` blob), but promoted to top-level fields so
622# services.yaml can attach a selector to each one for a typed, discoverable UI in Developer
623# Tools/the action picker, rather than one opaque object field.
624NOTIFY_ACTION_SCHEMA = vol.All(
625 cv.deprecated(key=ATTR_RECIPIENTS), # deprecated v2.2.0
626 _ACTION_DATA_FIELDS_SCHEMA.extend({
627 vol.Required(CONF_MESSAGE): cv.string,
628 vol.Optional(CONF_TITLE): cv.string,
629 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
630 # pass-through data exempt from the legacy nested-data migration - see actions.py
631 vol.Optional(ATTR_EXTRA_DATA): vol.Any(None, DATA_SCHEMA),
632 # the target selector behind CONF_TARGET can only produce entity/device/area/floor/label
633 # ids, so custom_target is a free-text escape hatch for targets it can't - e-mail
634 # addresses, phone numbers, Slack ids etc. notify.py's action_notify merges
635 # it back into target before Notification ever sees it
636 vol.Optional(ATTR_CUSTOM_TARGET): vol.All(cv.ensure_list, [cv.string]),
637 # the delivery dropdown can only give names, so this takes any form `delivery` does, and
638 # actions.py's action_notify merges the two into `delivery` before Notification sees them
639 vol.Optional(ATTR_DELIVERY_CONTROL): vol.Any(None, DELIVERY_FIELD_SCHEMA),
640 # promoted out of media: for their own selectors - notify.py's action_notify
641 # merges them back into media before Notification ever sees them
642 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id,
643 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string),
644 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string),
645 }),
646)