Coverage for custom_components/supernotify/schema.py: 95%
92 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-01 18:25 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-01 18:25 +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_DOMAIN,
16 CONF_EMAIL,
17 CONF_ENABLED,
18 CONF_HOST,
19 CONF_ICON,
20 CONF_ID,
21 CONF_NAME,
22 CONF_PASSWORD,
23 CONF_PORT,
24 CONF_TARGET,
25 CONF_TIMEOUT,
26 CONF_URL,
27 CONF_USERNAME,
28 CONF_VERIFY_SSL,
29)
30from homeassistant.helpers import config_validation as cv
31from homeassistant.helpers.typing import TemplateVarsType
33from custom_components.supernotify import ARCHIVE_DIR, MEDIA_DIR, TEMPLATE_DIR
35from .const import (
36 ATTR_ACTION,
37 ATTR_ACTION_CATEGORY,
38 ATTR_ACTION_GROUPS,
39 ATTR_ACTION_URL,
40 ATTR_ACTION_URL_TITLE,
41 ATTR_ACTIONS,
42 ATTR_DATA,
43 ATTR_DEBUG,
44 ATTR_DELIVERY,
45 ATTR_DELIVERY_SELECTION,
46 ATTR_DUPE_POLICY_MT,
47 ATTR_DUPE_POLICY_MTSLP,
48 ATTR_DUPE_POLICY_NONE,
49 ATTR_EMAIL,
50 ATTR_FORCE_RESEND,
51 ATTR_JPEG_OPTS,
52 ATTR_MEDIA,
53 ATTR_MEDIA_CAMERA_DELAY,
54 ATTR_MEDIA_CAMERA_ENTITY_ID,
55 ATTR_MEDIA_CAMERA_PTZ_PRESET,
56 ATTR_MEDIA_CLIP_URL,
57 ATTR_MEDIA_SNAPSHOT_URL,
58 ATTR_MESSAGE_HTML,
59 ATTR_MOBILE_APP_ID,
60 ATTR_PERSON_ID,
61 ATTR_PHONE,
62 ATTR_PNG_OPTS,
63 ATTR_PRIORITY,
64 ATTR_RECIPIENTS,
65 ATTR_SCENARIOS_APPLY,
66 ATTR_SCENARIOS_CONSTRAIN,
67 ATTR_SCENARIOS_REQUIRE,
68 ATTR_TIMESTAMP,
69 ATTR_TITLE,
70 CONF_ACTION_GROUP_NAMES,
71 CONF_ACTION_GROUPS,
72 CONF_ACTION_TEMPLATE,
73 CONF_ALT_CAMERA,
74 CONF_ARCHIVE,
75 CONF_ARCHIVE_DAYS,
76 CONF_ARCHIVE_DIAGNOSTICS,
77 CONF_ARCHIVE_EVENT_NAME,
78 CONF_ARCHIVE_EVENT_SELECTION,
79 CONF_ARCHIVE_MQTT_QOS,
80 CONF_ARCHIVE_MQTT_RETAIN,
81 CONF_ARCHIVE_MQTT_TOPIC,
82 CONF_ARCHIVE_PATH,
83 CONF_ARCHIVE_PURGE_INTERVAL,
84 CONF_CAMERA,
85 CONF_CAMERAS,
86 CONF_CLASS,
87 CONF_CONNECTION,
88 CONF_DATA,
89 CONF_DELIVERY,
90 CONF_DELIVERY_DEFAULTS,
91 CONF_DEVICE_DISCOVERY,
92 CONF_DEVICE_DOMAIN,
93 CONF_DEVICE_MODEL_EXCLUDE,
94 CONF_DEVICE_MODEL_INCLUDE,
95 CONF_DEVICE_TRACKER,
96 CONF_DUPE_CHECK,
97 CONF_DUPE_POLICY,
98 CONF_DURATION,
99 CONF_ENCRYPTION,
100 CONF_HOUSEKEEPING,
101 CONF_HOUSEKEEPING_TIME,
102 CONF_LINKS,
103 CONF_MANUFACTURER,
104 CONF_MEDIA,
105 CONF_MEDIA_PATH,
106 CONF_MEDIA_STORAGE_DAYS,
107 CONF_MEDIA_URL_PREFIX,
108 CONF_MESSAGE,
109 CONF_MOBILE_APP_ID,
110 CONF_MOBILE_DEVICES,
111 CONF_MOBILE_DISCOVERY,
112 CONF_MODEL,
113 CONF_OCCUPANCY,
114 CONF_OPTIONS,
115 CONF_PERSON,
116 CONF_PHONE_NUMBER,
117 CONF_PRIORITY,
118 CONF_PTZ_CAMERA,
119 CONF_PTZ_DELAY,
120 CONF_PTZ_METHOD,
121 CONF_PTZ_PRESET_DEFAULT,
122 CONF_RECIPIENTS,
123 CONF_RECIPIENTS_DISCOVERY,
124 CONF_SCENARIOS,
125 CONF_SELECTION,
126 CONF_SELECTION_RANK,
127 CONF_SIZE,
128 CONF_SNOOZE,
129 CONF_SNOOZE_TIME,
130 CONF_TARGET_REQUIRED,
131 CONF_TARGET_USAGE,
132 CONF_TEMPLATE,
133 CONF_TEMPLATE_PATH,
134 CONF_TITLE,
135 CONF_TITLE_TEMPLATE,
136 CONF_TRANSPORT,
137 CONF_TRANSPORTS,
138 CONF_TTL,
139 CONF_TUNE,
140 CONF_URI,
141 CONF_VOLUME,
142 DELIVERY_SELECTION_VALUES,
143 OCCUPANCY_ALL,
144 OCCUPANCY_VALUES,
145 OPTION_CHIME_ALIASES,
146 OPTIONS_CHIME_DOMAINS,
147 PRIORITY_VALUES,
148 PTZ_DELAY_DEFAULT,
149 PTZ_METHOD_ONVIF,
150 PTZ_METHOD_VALUES,
151 RESERVED_DATA_KEYS,
152 RESERVED_SCENARIO_NAMES,
153 SELECTION_VALUES,
154 TARGET_REQUIRE_ALWAYS,
155 TARGET_REQUIRE_NEVER,
156 TARGET_REQUIRE_OPTIONAL,
157 TARGET_USE_FIXED,
158 TARGET_USE_MERGE_ALWAYS,
159 TARGET_USE_MERGE_ON_DELIVERY_TARGETS,
160 TARGET_USE_ON_NO_ACTION_TARGETS,
161 TARGET_USE_ON_NO_DELIVERY_TARGETS,
162 TRANSPORT_VALUES,
163)
166class OutcomeSelection(IntFlag):
167 NONE = 0
168 SUCCESS = 1
169 NO_DELIVERY = 2
170 PARTIAL_DELIVERY = 4
171 FALLBACK_DELIVERY = 8
172 ERROR = 16
173 DUPE = 32
174 ALL = 65536
177def parse_event_policy(value: object) -> OutcomeSelection:
178 if isinstance(value, OutcomeSelection):
179 return value
180 if isinstance(value, int):
181 return OutcomeSelection(value)
182 if isinstance(value, str):
183 result = OutcomeSelection.NONE
184 for part in value.split("|"):
185 result |= OutcomeSelection[part.strip()]
186 return result
187 raise vol.Invalid(f"expected OutcomeSelection, got {value!r}")
190class DeliveryOutcome(StrEnum):
191 SUCCESS = auto()
192 NO_DELIVERY = auto()
193 PARTIAL_DELIVERY = auto()
194 FALLBACK_DELIVERY = auto()
195 ERROR = auto()
196 DUPE = auto()
199class EnvelopeOutcome(StrEnum):
200 SUCCESS = auto()
201 ERROR = auto()
202 SKIPPED = auto() # envelope not generated
203 SUPPRESSED = auto() # envelope generated but intentionally not delivered
206class SelectionRank(StrEnum):
207 FIRST = "FIRST"
208 ANY = "ANY"
209 LAST = "LAST"
212type ConditionsFunc = Callable[[TemplateVarsType], bool]
215def phone(value: str) -> str:
216 """Validate a phone number"""
217 regex = re.compile(r"^(\+\d{1,3})?\s?\(?\d{1,4}\)?[\s.-]?\d{3}[\s.-]?\d{4}$")
218 if not regex.match(value):
219 raise vol.Invalid("Invalid Phone Number")
220 return str(value)
223def validate_scenario_names(scenarios: dict) -> dict:
224 """Validate that scenario names are not reserved."""
225 for name in scenarios:
226 if name in RESERVED_SCENARIO_NAMES:
227 raise vol.Invalid(f"'{name}' is a reserved scenario name")
228 return scenarios
231# TARGET_FIELDS includes entity, device, area, floor, label ids
232TARGET_SCHEMA = vol.Any( # order of schema matters, voluptuous forces into first it finds that works
233 cv.TARGET_FIELDS
234 | {
235 vol.Optional(ATTR_EMAIL): vol.All(cv.ensure_list, [vol.Email()]), # type: ignore[call-arg]
236 vol.Optional(ATTR_PHONE): vol.All(cv.ensure_list, [phone]),
237 vol.Optional(ATTR_MOBILE_APP_ID): vol.All(cv.ensure_list, [cv.service]),
238 vol.Optional(ATTR_PERSON_ID): vol.All(cv.ensure_list, [cv.entity_id]),
239 vol.Optional(cv.string): vol.All(cv.ensure_list, [str]),
240 },
241 str,
242 list[str],
243)
245DATA_SCHEMA = vol.Schema({vol.NotIn(RESERVED_DATA_KEYS): vol.Any(str, int, bool, float, dict, list)})
247MOBILE_DEVICE_SCHEMA = vol.Schema({
248 vol.Optional(CONF_MANUFACTURER): cv.string,
249 vol.Optional(CONF_MODEL): cv.string,
250 vol.Optional(CONF_CLASS): cv.string,
251 vol.Optional(CONF_MOBILE_APP_ID): cv.string,
252 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
253 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
254})
255NOTIFICATION_DUPE_SCHEMA = vol.Schema({
256 vol.Optional(CONF_TTL, default=120): cv.positive_int,
257 vol.Optional(CONF_SIZE, default=100): cv.positive_int,
258 vol.Optional(CONF_DUPE_POLICY, default=ATTR_DUPE_POLICY_MTSLP): vol.In([
259 ATTR_DUPE_POLICY_MTSLP,
260 ATTR_DUPE_POLICY_MT,
261 ATTR_DUPE_POLICY_NONE,
262 ]),
263})
266DELIVERY_CUSTOMIZE_SCHEMA = vol.All(
267 vol.Schema(
268 {
269 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
270 vol.Optional(CONF_ENABLED): vol.Any(None, cv.boolean),
271 vol.Optional(CONF_DATA): DATA_SCHEMA,
272 },
273 ),
274)
275LINK_SCHEMA = vol.Schema({
276 vol.Required(CONF_URL): cv.url,
277 vol.Required(CONF_DESCRIPTION): cv.string,
278 vol.Optional(CONF_ID): cv.string,
279 vol.Optional(CONF_ICON): cv.icon,
280 vol.Optional(CONF_NAME): cv.string,
281})
283SNOOZE_SCHEMA = vol.Schema({vol.Optional(CONF_SNOOZE_TIME, default=60 * 60): cv.positive_int})
285DELIVERY_CONFIG_SCHEMA = vol.Schema({ # shared by Transport Defaults and Delivery definitions
286 # defaults set in model.DeliveryConfig
287 vol.Optional(CONF_ACTION): cv.service, # previously 'service:'
288 vol.Optional(CONF_DEBUG): cv.boolean,
289 vol.Optional(CONF_OPTIONS): dict, # transport tuning
290 vol.Optional(CONF_DATA): DATA_SCHEMA,
291 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
292 vol.Optional(CONF_TARGET_REQUIRED): vol.Any(
293 cv.boolean, vol.In([TARGET_REQUIRE_ALWAYS, TARGET_REQUIRE_NEVER, TARGET_REQUIRE_OPTIONAL])
294 ),
295 vol.Optional(CONF_TARGET_USAGE): vol.In([
296 TARGET_USE_ON_NO_DELIVERY_TARGETS,
297 TARGET_USE_ON_NO_ACTION_TARGETS,
298 TARGET_USE_MERGE_ON_DELIVERY_TARGETS,
299 TARGET_USE_MERGE_ALWAYS,
300 TARGET_USE_FIXED,
301 ]),
302 vol.Optional(CONF_SELECTION): vol.All(cv.ensure_list, [vol.In(SELECTION_VALUES)]),
303 vol.Optional(CONF_PRIORITY): vol.All(cv.ensure_list, [vol.Any(int, str, vol.In(list(PRIORITY_VALUES.keys())))]),
304 vol.Optional(CONF_SELECTION_RANK): vol.In([
305 SelectionRank.ANY,
306 SelectionRank.FIRST,
307 SelectionRank.LAST,
308 ]),
309})
312def _migrate_condition(config: dict) -> dict:
313 """Migrate deprecated 'condition' key to 'conditions'."""
314 if CONF_CONDITION in config:
315 if CONF_CONDITIONS not in config:
316 config = {**config, CONF_CONDITIONS: config[CONF_CONDITION]}
317 config = {k: v for k, v in config.items() if k != CONF_CONDITION}
318 return config
321DELIVERY_SCHEMA = vol.All(
322 _migrate_condition,
323 DELIVERY_CONFIG_SCHEMA.extend({
324 vol.Required(CONF_TRANSPORT): vol.In(TRANSPORT_VALUES),
325 vol.Optional(CONF_ALIAS): cv.string,
326 vol.Optional(CONF_TEMPLATE): cv.string,
327 vol.Optional(CONF_MESSAGE): vol.Any(None, cv.string),
328 vol.Optional(CONF_TITLE): vol.Any(None, cv.string),
329 vol.Optional(CONF_ENABLED): cv.boolean,
330 vol.Optional(CONF_OCCUPANCY, default=OCCUPANCY_ALL): vol.In(OCCUPANCY_VALUES),
331 vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
332 }),
333)
335CONNECTION_SCHEMA = vol.Schema({
336 vol.Required(CONF_HOST): cv.string,
337 vol.Optional(CONF_PORT, default=587): cv.port,
338 vol.Optional(CONF_ENCRYPTION, default="starttls"): vol.In(["tls", "starttls", "none"]),
339 vol.Optional(CONF_USERNAME): cv.string,
340 vol.Optional(CONF_PASSWORD): cv.string,
341 vol.Optional(CONF_TIMEOUT, default=5): cv.positive_int,
342 vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
343})
345TRANSPORT_SCHEMA = vol.All(
346 cv.deprecated(key=CONF_DEVICE_DOMAIN), # deprecated v1.9.0
347 cv.deprecated(key=CONF_DEVICE_DISCOVERY), # deprecated v1.9.0
348 cv.deprecated(key=CONF_DEVICE_MODEL_INCLUDE), # deprecated v1.9.0
349 cv.deprecated(key=CONF_DEVICE_MODEL_EXCLUDE), # deprecated v1.9.0
350 vol.Schema({
351 vol.Optional(CONF_ALIAS): cv.string,
352 vol.Optional(CONF_DEVICE_DOMAIN): vol.All(cv.ensure_list, [cv.string]),
353 vol.Optional(CONF_DEVICE_MODEL_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
354 vol.Optional(CONF_DEVICE_MODEL_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
355 vol.Optional(CONF_DEVICE_DISCOVERY): cv.boolean,
356 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
357 vol.Optional(CONF_DELIVERY_DEFAULTS): DELIVERY_CONFIG_SCHEMA,
358 # only meaningful for transports (eg smtp) that own their own network connection
359 vol.Optional(CONF_CONNECTION): CONNECTION_SCHEMA,
360 }),
361)
362# Idea - differentiate enabled as recipient vs as occupant, for ALL_IN etc check
363# May need condition, and also enabled if delivery disabled
364# CONF_OCCUPANCY="occupancy"
365# OPTION_OCCUPANCY_DEFAULT="default"
366# OPTIONS_OCCUPANCY=[OPTION_OCCUPANCY_DEFAULT,OPTION_OCCUPANCY_EXCLUDE]
367# OPTION_OCCUPANCY_EXCLUDE="exclude"
370RECIPIENT_SCHEMA = vol.Schema({
371 vol.Required(CONF_PERSON): cv.entity_id,
372 vol.Optional(CONF_ALIAS): cv.string,
373 vol.Optional(CONF_EMAIL): cv.string,
374 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
375 # vol.Optional(CONF_OCCUPANCY,default=OPTION_OCCUPANCY_DEFAULT):vol.In(OPTIONS_OCCUPANCY),
376 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
377 vol.Optional(CONF_PHONE_NUMBER): cv.string,
378 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
379 vol.Optional(CONF_MOBILE_DEVICES, default=list): vol.All(cv.ensure_list, [MOBILE_DEVICE_SCHEMA]),
380 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_CUSTOMIZE_SCHEMA},
381})
382CAMERA_SCHEMA = vol.Schema({
383 vol.Required(CONF_CAMERA): cv.entity_id,
384 vol.Optional(CONF_ALT_CAMERA): vol.All(cv.ensure_list, [cv.entity_id]),
385 vol.Optional(CONF_ALIAS): cv.string,
386 vol.Optional(CONF_URL): cv.url,
387 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
388 vol.Optional(CONF_PTZ_CAMERA): cv.entity_id,
389 vol.Optional(CONF_PTZ_PRESET_DEFAULT, default=1): vol.Any(cv.positive_int, cv.string),
390 vol.Optional(CONF_PTZ_DELAY, default=PTZ_DELAY_DEFAULT): int,
391 vol.Optional(CONF_PTZ_METHOD, default=PTZ_METHOD_ONVIF): vol.In(PTZ_METHOD_VALUES),
392})
393MEDIA_SCHEMA = vol.Schema({
394 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id,
395 vol.Optional(ATTR_MEDIA_CAMERA_DELAY, default=0): int,
396 vol.Optional(ATTR_MEDIA_CAMERA_PTZ_PRESET): vol.Any(cv.positive_int, cv.string),
397 # URL fragments allowed
398 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string),
399 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string),
400 vol.Optional(ATTR_JPEG_OPTS): dict,
401 vol.Optional(ATTR_PNG_OPTS): dict,
402})
405SCENARIO_SCHEMA = vol.All(
406 _migrate_condition,
407 cv.deprecated(key="delivery_selection"),
408 vol.Schema({
409 vol.Optional(CONF_ALIAS): cv.string,
410 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
411 vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
412 vol.Optional(CONF_MEDIA): MEDIA_SCHEMA,
413 vol.Optional(CONF_ACTION_GROUP_NAMES, default=[]): vol.All(cv.ensure_list, [cv.string]),
414 vol.Optional("delivery_selection"): cv.string,
415 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)},
416 }),
417)
420def _mobile_action_uri(value: str) -> str:
421 """Validate URI for mobile actions — accepts http/https and homeassistant:// deep links."""
422 if value and not (value.startswith(("http://", "https://", "homeassistant://"))):
423 raise vol.Invalid(f"Invalid URI {value!r}: must start with http://, https://, or homeassistant://")
424 return value
427MOBILE_ACTION_CALL_SCHEMA = vol.Schema(
428 {
429 vol.Optional(ATTR_ACTION): cv.string,
430 vol.Optional(ATTR_TITLE): cv.string,
431 vol.Optional(ATTR_ACTION_CATEGORY): cv.string,
432 vol.Optional(ATTR_ACTION_URL): cv.url,
433 vol.Optional(ATTR_ACTION_URL_TITLE): cv.string,
434 },
435 extra=vol.ALLOW_EXTRA,
436)
437MOBILE_ACTION_SCHEMA = vol.Schema(
438 {
439 vol.Exclusive(CONF_ACTION, CONF_ACTION_TEMPLATE): cv.string,
440 vol.Exclusive(CONF_TITLE, CONF_TITLE_TEMPLATE): cv.string,
441 vol.Optional(CONF_URI): _mobile_action_uri,
442 vol.Optional(CONF_ICON): cv.string,
443 },
444 extra=vol.ALLOW_EXTRA,
445)
448ARCHIVE_SCHEMA = vol.All(
449 cv.deprecated(key=CONF_DEBUG), # deprecated v1.10.0
450 vol.Schema({
451 vol.Optional(CONF_ARCHIVE_PATH, default=ARCHIVE_DIR): cv.path,
452 vol.Optional(CONF_ENABLED, default=False): cv.boolean,
453 vol.Optional(CONF_ARCHIVE_DAYS, default=3): cv.positive_int,
454 vol.Optional(CONF_ARCHIVE_MQTT_TOPIC): cv.string,
455 vol.Optional(CONF_ARCHIVE_MQTT_QOS, default=0): cv.positive_int,
456 vol.Optional(CONF_ARCHIVE_MQTT_RETAIN, default=True): cv.boolean,
457 vol.Optional(CONF_ARCHIVE_PURGE_INTERVAL, default=60): cv.positive_int,
458 vol.Optional(CONF_ARCHIVE_EVENT_NAME, default="supernotification"): cv.string,
459 vol.Optional(CONF_ARCHIVE_EVENT_SELECTION, default=OutcomeSelection.NONE): parse_event_policy,
460 vol.Optional(CONF_ARCHIVE_DIAGNOSTICS, default=OutcomeSelection.ERROR): parse_event_policy,
461 vol.Optional(CONF_DEBUG, default=False): cv.boolean,
462 }),
463)
465HOUSEKEEPING_SCHEMA = vol.Schema({
466 vol.Optional(CONF_HOUSEKEEPING_TIME, default="00:00:01"): cv.time,
467 vol.Optional(CONF_MEDIA_STORAGE_DAYS, default=7): cv.positive_int,
468})
470# The 8 keys not yet configurable via ConfigFlow - defined under the top-level `supernotify:`
471# YAML key (see CONFIG_SCHEMA/async_setup in __init__.py), never under the legacy notify platform.
472# extra=ALLOW_EXTRA matches the old PLATFORM_SCHEMA-derived schema's inherited leniency (HA's
473# base notify PLATFORM_SCHEMA itself uses ALLOW_EXTRA) - stray/unrecognized keys pass through
474# rather than failing validation.
475SUPERNOTIFY_YAML_SCHEMA: vol.Schema = vol.Schema(
476 {
477 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_SCHEMA},
478 vol.Optional(CONF_ACTION_GROUPS, default=dict): {cv.string: [MOBILE_ACTION_SCHEMA]},
479 vol.Optional(CONF_RECIPIENTS, default=list): vol.All(cv.ensure_list, [RECIPIENT_SCHEMA]),
480 vol.Optional(CONF_LINKS, default=list): vol.All(cv.ensure_list, [LINK_SCHEMA]),
481 vol.Optional(CONF_SCENARIOS, default=dict): vol.All(
482 {cv.string: SCENARIO_SCHEMA},
483 validate_scenario_names,
484 ),
485 vol.Optional(CONF_TRANSPORTS, default=dict): {cv.string: TRANSPORT_SCHEMA},
486 vol.Optional(CONF_CAMERAS, default=list): vol.All(cv.ensure_list, [CAMERA_SCHEMA]),
487 vol.Optional(CONF_SNOOZE, default=dict): SNOOZE_SCHEMA,
488 },
489 extra=vol.ALLOW_EXTRA,
490)
492# The 8 ConfigEntry-owned keys only (entry.data/entry.options) - deliberately separate from
493# SUPERNOTIFY_YAML_SCHEMA rather than folded into one combined schema: __init__.py's
494# _entry_full_config validates entry.data/entry.options through this schema, then merges in the
495# top-level YAML section's already-validated dict as-is. Re-validating that already-validated
496# dict a second time (as a single combined schema would require) breaks - cv.template/
497# cv.CONDITIONS_SCHEMA coerce string templates into Template objects, and reject a Template
498# object the second time round ("template value should be a string").
499CONFIG_ENTRY_SCHEMA = vol.Schema(
500 {
501 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path,
502 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path,
503 vol.Optional(CONF_MEDIA_URL_PREFIX, default="/supernotify/media"): cv.string,
504 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA,
505 vol.Optional(CONF_HOUSEKEEPING, default={}): HOUSEKEEPING_SCHEMA,
506 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA,
507 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
508 vol.Optional(CONF_RECIPIENTS_DISCOVERY, default=True): cv.boolean,
509 },
510 extra=vol.ALLOW_EXTRA,
511)
513# The union of CONFIG_ENTRY_SCHEMA and SUPERNOTIFY_YAML_SCHEMA - for validating a single,
514# never-previously-validated raw config dict in one pass (e.g. hass_setup_lib.py's
515# TestingContext). Do NOT use this in __init__.py's _entry_full_config - see CONFIG_ENTRY_SCHEMA.
516FULL_CONFIG_SCHEMA = SUPERNOTIFY_YAML_SCHEMA.extend({
517 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path,
518 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path,
519 vol.Optional(CONF_MEDIA_URL_PREFIX, default="/supernotify/media"): cv.string,
520 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA,
521 vol.Optional(CONF_HOUSEKEEPING, default={}): HOUSEKEEPING_SCHEMA,
522 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA,
523 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
524 vol.Optional(CONF_RECIPIENTS_DISCOVERY, default=True): cv.boolean,
525})
527CHIME_ALIASES_SCHEMA = vol.Schema({
528 vol.Required(OPTION_CHIME_ALIASES, default=dict): vol.Schema({
529 cv.string: vol.Schema({
530 cv.string: vol.Any(
531 vol.Any(None, cv.string, vol.In(OPTIONS_CHIME_DOMAINS)),
532 vol.Schema({
533 vol.Optional(CONF_ALIAS): cv.string,
534 vol.Optional(CONF_DOMAIN): cv.string,
535 vol.Optional(CONF_TUNE): cv.string,
536 vol.Optional(CONF_DATA): DATA_SCHEMA,
537 vol.Optional(CONF_VOLUME): float,
538 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
539 vol.Optional(CONF_DURATION): cv.positive_int,
540 }),
541 )
542 })
543 })
544})
547_ACTION_DATA_FIELDS_SCHEMA = vol.Schema(
548 {
549 vol.Optional(ATTR_DELIVERY): vol.Any(cv.string, [cv.string], {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}),
550 vol.Optional(ATTR_PRIORITY): vol.Any(int, str, vol.In(list(PRIORITY_VALUES.keys()))),
551 vol.Optional(ATTR_SCENARIOS_REQUIRE): vol.All(cv.ensure_list, [cv.string]),
552 vol.Optional(ATTR_SCENARIOS_APPLY): vol.All(cv.ensure_list, [cv.string]),
553 vol.Optional(ATTR_SCENARIOS_CONSTRAIN): vol.All(cv.ensure_list, [cv.string]),
554 vol.Optional(ATTR_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
555 vol.Optional(ATTR_RECIPIENTS): vol.All(cv.ensure_list, [cv.entity_id]),
556 vol.Optional(ATTR_MEDIA): MEDIA_SCHEMA,
557 vol.Optional(ATTR_MESSAGE_HTML): cv.string,
558 vol.Optional(ATTR_ACTION_GROUPS, default=[]): vol.All(cv.ensure_list, [cv.string]),
559 vol.Optional(ATTR_ACTIONS, default=[]): vol.All(cv.ensure_list, [MOBILE_ACTION_CALL_SCHEMA]),
560 vol.Optional(ATTR_DEBUG, default=False): cv.boolean,
561 vol.Optional(ATTR_FORCE_RESEND, default=False): cv.boolean,
562 vol.Optional(ATTR_DATA): vol.Any(None, DATA_SCHEMA),
563 vol.Optional(ATTR_TIMESTAMP): cv.string,
564 },
565 extra=vol.ALLOW_EXTRA, # allow other data, e.g. the android/ios mobile push
566)
568ACTION_DATA_SCHEMA = vol.All(
569 cv.deprecated(key=ATTR_RECIPIENTS), # deprecated v2.2.0
570 _ACTION_DATA_FIELDS_SCHEMA,
571)
573STRICT_ACTION_DATA_SCHEMA = vol.All(
574 cv.deprecated(key=ATTR_RECIPIENTS), # deprecated v2.2.0
575 _ACTION_DATA_FIELDS_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA),
576)