Coverage for custom_components / supernotify / schema.py: 96%
85 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-11 22:18 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-11 22:18 +0000
1"""The Supernotify integration"""
3import re
4from collections.abc import Callable
5from enum import IntFlag, StrEnum, auto
7import voluptuous as vol
8from homeassistant.components.notify import PLATFORM_SCHEMA
9from homeassistant.const import (
10 CONF_ACTION,
11 CONF_ALIAS,
12 CONF_CONDITION,
13 CONF_CONDITIONS,
14 CONF_DEBUG,
15 CONF_DESCRIPTION,
16 CONF_DOMAIN,
17 CONF_EMAIL,
18 CONF_ENABLED,
19 CONF_ICON,
20 CONF_ID,
21 CONF_NAME,
22 CONF_TARGET,
23 CONF_URL,
24)
25from homeassistant.helpers import config_validation as cv
26from homeassistant.helpers.typing import TemplateVarsType
28from custom_components.supernotify import MEDIA_DIR, TEMPLATE_DIR
30from .const import (
31 ATTR_ACTION,
32 ATTR_ACTION_CATEGORY,
33 ATTR_ACTION_GROUPS,
34 ATTR_ACTION_URL,
35 ATTR_ACTION_URL_TITLE,
36 ATTR_ACTIONS,
37 ATTR_DATA,
38 ATTR_DEBUG,
39 ATTR_DELIVERY,
40 ATTR_DELIVERY_SELECTION,
41 ATTR_DUPE_POLICY_MT,
42 ATTR_DUPE_POLICY_MTSLP,
43 ATTR_DUPE_POLICY_NONE,
44 ATTR_EMAIL,
45 ATTR_FORCE_RESEND,
46 ATTR_JPEG_OPTS,
47 ATTR_MEDIA,
48 ATTR_MEDIA_CAMERA_DELAY,
49 ATTR_MEDIA_CAMERA_ENTITY_ID,
50 ATTR_MEDIA_CAMERA_PTZ_PRESET,
51 ATTR_MEDIA_CLIP_URL,
52 ATTR_MEDIA_SNAPSHOT_URL,
53 ATTR_MESSAGE_HTML,
54 ATTR_MOBILE_APP_ID,
55 ATTR_PERSON_ID,
56 ATTR_PHONE,
57 ATTR_PNG_OPTS,
58 ATTR_PRIORITY,
59 ATTR_RECIPIENTS,
60 ATTR_SCENARIOS_APPLY,
61 ATTR_SCENARIOS_CONSTRAIN,
62 ATTR_SCENARIOS_REQUIRE,
63 ATTR_TIMESTAMP,
64 ATTR_TITLE,
65 CONF_ACTION_GROUP_NAMES,
66 CONF_ACTION_GROUPS,
67 CONF_ACTION_TEMPLATE,
68 CONF_ALT_CAMERA,
69 CONF_ARCHIVE,
70 CONF_ARCHIVE_DAYS,
71 CONF_ARCHIVE_DIAGNOSTICS,
72 CONF_ARCHIVE_EVENT_NAME,
73 CONF_ARCHIVE_EVENT_SELECTION,
74 CONF_ARCHIVE_MQTT_QOS,
75 CONF_ARCHIVE_MQTT_RETAIN,
76 CONF_ARCHIVE_MQTT_TOPIC,
77 CONF_ARCHIVE_PATH,
78 CONF_ARCHIVE_PURGE_INTERVAL,
79 CONF_CAMERA,
80 CONF_CAMERAS,
81 CONF_CLASS,
82 CONF_DATA,
83 CONF_DELIVERY,
84 CONF_DELIVERY_DEFAULTS,
85 CONF_DEVICE_DISCOVERY,
86 CONF_DEVICE_DOMAIN,
87 CONF_DEVICE_MODEL_EXCLUDE,
88 CONF_DEVICE_MODEL_INCLUDE,
89 CONF_DEVICE_TRACKER,
90 CONF_DUPE_CHECK,
91 CONF_DUPE_POLICY,
92 CONF_DURATION,
93 CONF_HOUSEKEEPING,
94 CONF_HOUSEKEEPING_TIME,
95 CONF_LINKS,
96 CONF_MANUFACTURER,
97 CONF_MEDIA,
98 CONF_MEDIA_PATH,
99 CONF_MEDIA_STORAGE_DAYS,
100 CONF_MEDIA_URL_PREFIX,
101 CONF_MESSAGE,
102 CONF_MOBILE_APP_ID,
103 CONF_MOBILE_DEVICES,
104 CONF_MOBILE_DISCOVERY,
105 CONF_MODEL,
106 CONF_OCCUPANCY,
107 CONF_OPTIONS,
108 CONF_PERSON,
109 CONF_PHONE_NUMBER,
110 CONF_PRIORITY,
111 CONF_PTZ_CAMERA,
112 CONF_PTZ_DELAY,
113 CONF_PTZ_METHOD,
114 CONF_PTZ_PRESET_DEFAULT,
115 CONF_RECIPIENTS,
116 CONF_RECIPIENTS_DISCOVERY,
117 CONF_SCENARIOS,
118 CONF_SELECTION,
119 CONF_SELECTION_RANK,
120 CONF_SIZE,
121 CONF_SNOOZE,
122 CONF_SNOOZE_TIME,
123 CONF_TARGET_REQUIRED,
124 CONF_TARGET_USAGE,
125 CONF_TEMPLATE,
126 CONF_TEMPLATE_PATH,
127 CONF_TITLE,
128 CONF_TITLE_TEMPLATE,
129 CONF_TRANSPORT,
130 CONF_TRANSPORTS,
131 CONF_TTL,
132 CONF_TUNE,
133 CONF_URI,
134 CONF_VOLUME,
135 DELIVERY_SELECTION_VALUES,
136 OCCUPANCY_ALL,
137 OCCUPANCY_VALUES,
138 OPTION_CHIME_ALIASES,
139 OPTIONS_CHIME_DOMAINS,
140 PRIORITY_VALUES,
141 PTZ_METHOD_ONVIF,
142 PTZ_METHOD_VALUES,
143 RESERVED_DATA_KEYS,
144 RESERVED_SCENARIO_NAMES,
145 SELECTION_VALUES,
146 TARGET_REQUIRE_ALWAYS,
147 TARGET_REQUIRE_NEVER,
148 TARGET_REQUIRE_OPTIONAL,
149 TARGET_USE_FIXED,
150 TARGET_USE_MERGE_ALWAYS,
151 TARGET_USE_MERGE_ON_DELIVERY_TARGETS,
152 TARGET_USE_ON_NO_ACTION_TARGETS,
153 TARGET_USE_ON_NO_DELIVERY_TARGETS,
154 TRANSPORT_VALUES,
155)
158class OutcomeSelection(IntFlag):
159 NONE = 0
160 SUCCESS = 1
161 NO_DELIVERY = 2
162 PARTIAL_DELIVERY = 4
163 FALLBACK_DELIVERY = 8
164 ERROR = 16
165 DUPE = 32
166 ALL = 65536
169def parse_event_policy(value: object) -> OutcomeSelection:
170 if isinstance(value, OutcomeSelection):
171 return value
172 if isinstance(value, int):
173 return OutcomeSelection(value)
174 if isinstance(value, str):
175 result = OutcomeSelection.NONE
176 for part in value.split("|"):
177 result |= OutcomeSelection[part.strip()]
178 return result
179 raise vol.Invalid(f"expected OutcomeSelection, got {value!r}")
182class Outcome(StrEnum):
183 SUCCESS = auto()
184 NO_DELIVERY = auto()
185 PARTIAL_DELIVERY = auto()
186 FALLBACK_DELIVERY = auto()
187 ERROR = auto()
188 DUPE = auto()
191class SelectionRank(StrEnum):
192 FIRST = "FIRST"
193 ANY = "ANY"
194 LAST = "LAST"
197type ConditionsFunc = Callable[[TemplateVarsType], bool]
200def phone(value: str) -> str:
201 """Validate a phone number"""
202 regex = re.compile(r"^(\+\d{1,3})?\s?\(?\d{1,4}\)?[\s.-]?\d{3}[\s.-]?\d{4}$")
203 if not regex.match(value):
204 raise vol.Invalid("Invalid Phone Number")
205 return str(value)
208def validate_scenario_names(scenarios: dict) -> dict:
209 """Validate that scenario names are not reserved."""
210 for name in scenarios:
211 if name in RESERVED_SCENARIO_NAMES:
212 raise vol.Invalid(f"'{name}' is a reserved scenario name")
213 return scenarios
216# TARGET_FIELDS includes entity, device, area, floor, label ids
217TARGET_SCHEMA = vol.Any( # order of schema matters, voluptuous forces into first it finds that works
218 cv.TARGET_FIELDS
219 | {
220 vol.Optional(ATTR_EMAIL): vol.All(cv.ensure_list, [vol.Email]),
221 vol.Optional(ATTR_PHONE): vol.All(cv.ensure_list, [phone]),
222 vol.Optional(ATTR_MOBILE_APP_ID): vol.All(cv.ensure_list, [cv.service]),
223 vol.Optional(ATTR_PERSON_ID): vol.All(cv.ensure_list, [cv.entity_id]),
224 vol.Optional(cv.string): vol.All(cv.ensure_list, [str]),
225 },
226 str,
227 list[str],
228)
230DATA_SCHEMA = vol.Schema({vol.NotIn(RESERVED_DATA_KEYS): vol.Any(str, int, bool, float, dict, list)})
232MOBILE_DEVICE_SCHEMA = vol.Schema({
233 vol.Optional(CONF_MANUFACTURER): cv.string,
234 vol.Optional(CONF_MODEL): cv.string,
235 vol.Optional(CONF_CLASS): cv.string,
236 vol.Optional(CONF_MOBILE_APP_ID): cv.string,
237 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
238 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
239})
240NOTIFICATION_DUPE_SCHEMA = vol.Schema({
241 vol.Optional(CONF_TTL, default=120): cv.positive_int,
242 vol.Optional(CONF_SIZE, default=100): cv.positive_int,
243 vol.Optional(CONF_DUPE_POLICY, default=ATTR_DUPE_POLICY_MTSLP): vol.In([
244 ATTR_DUPE_POLICY_MTSLP,
245 ATTR_DUPE_POLICY_MT,
246 ATTR_DUPE_POLICY_NONE,
247 ]),
248})
251DELIVERY_CUSTOMIZE_SCHEMA = vol.All(
252 vol.Schema(
253 {
254 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
255 vol.Optional(CONF_ENABLED): vol.Any(None, cv.boolean),
256 vol.Optional(CONF_DATA): DATA_SCHEMA,
257 },
258 ),
259)
260LINK_SCHEMA = vol.Schema({
261 vol.Required(CONF_URL): cv.url,
262 vol.Required(CONF_DESCRIPTION): cv.string,
263 vol.Optional(CONF_ID): cv.string,
264 vol.Optional(CONF_ICON): cv.icon,
265 vol.Optional(CONF_NAME): cv.string,
266})
268SNOOZE_SCHEMA = vol.Schema({vol.Optional(CONF_SNOOZE_TIME, default=60 * 60): cv.positive_int})
270DELIVERY_CONFIG_SCHEMA = vol.Schema({ # shared by Transport Defaults and Delivery definitions
271 # defaults set in model.DeliveryConfig
272 vol.Optional(CONF_ACTION): cv.service, # previously 'service:'
273 vol.Optional(CONF_DEBUG): cv.boolean,
274 vol.Optional(CONF_OPTIONS): dict, # transport tuning
275 vol.Optional(CONF_DATA): DATA_SCHEMA,
276 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
277 vol.Optional(CONF_TARGET_REQUIRED): vol.Any(
278 cv.boolean, vol.In([TARGET_REQUIRE_ALWAYS, TARGET_REQUIRE_NEVER, TARGET_REQUIRE_OPTIONAL])
279 ),
280 vol.Optional(CONF_TARGET_USAGE): vol.In([
281 TARGET_USE_ON_NO_DELIVERY_TARGETS,
282 TARGET_USE_ON_NO_ACTION_TARGETS,
283 TARGET_USE_MERGE_ON_DELIVERY_TARGETS,
284 TARGET_USE_MERGE_ALWAYS,
285 TARGET_USE_FIXED,
286 ]),
287 vol.Optional(CONF_SELECTION): vol.All(cv.ensure_list, [vol.In(SELECTION_VALUES)]),
288 vol.Optional(CONF_PRIORITY): vol.All(cv.ensure_list, [vol.Any(int, str, vol.In(list(PRIORITY_VALUES.keys())))]),
289 vol.Optional(CONF_SELECTION_RANK): vol.In([
290 SelectionRank.ANY,
291 SelectionRank.FIRST,
292 SelectionRank.LAST,
293 ]),
294})
297def _migrate_condition(config: dict) -> dict:
298 """Migrate deprecated 'condition' key to 'conditions'."""
299 if CONF_CONDITION in config:
300 if CONF_CONDITIONS not in config:
301 config = {**config, CONF_CONDITIONS: config[CONF_CONDITION]}
302 config = {k: v for k, v in config.items() if k != CONF_CONDITION}
303 return config
306DELIVERY_SCHEMA = vol.All(
307 _migrate_condition,
308 DELIVERY_CONFIG_SCHEMA.extend({
309 vol.Required(CONF_TRANSPORT): vol.In(TRANSPORT_VALUES),
310 vol.Optional(CONF_ALIAS): cv.string,
311 vol.Optional(CONF_TEMPLATE): cv.string,
312 vol.Optional(CONF_MESSAGE): vol.Any(None, cv.string),
313 vol.Optional(CONF_TITLE): vol.Any(None, cv.string),
314 vol.Optional(CONF_ENABLED): cv.boolean,
315 vol.Optional(CONF_OCCUPANCY, default=OCCUPANCY_ALL): vol.In(OCCUPANCY_VALUES),
316 vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
317 }),
318)
320TRANSPORT_SCHEMA = vol.All(
321 cv.deprecated(key=CONF_DEVICE_DOMAIN), # deprecated v1.9.0
322 cv.deprecated(key=CONF_DEVICE_DISCOVERY), # deprecated v1.9.0
323 cv.deprecated(key=CONF_DEVICE_MODEL_INCLUDE), # deprecated v1.9.0
324 cv.deprecated(key=CONF_DEVICE_MODEL_EXCLUDE), # deprecated v1.9.0
325 vol.Schema({
326 vol.Optional(CONF_ALIAS): cv.string,
327 vol.Optional(CONF_DEVICE_DOMAIN): vol.All(cv.ensure_list, [cv.string]),
328 vol.Optional(CONF_DEVICE_MODEL_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
329 vol.Optional(CONF_DEVICE_MODEL_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
330 vol.Optional(CONF_DEVICE_DISCOVERY): cv.boolean,
331 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
332 vol.Optional(CONF_DELIVERY_DEFAULTS): DELIVERY_CONFIG_SCHEMA,
333 }),
334)
335# Idea - differentiate enabled as recipient vs as occupant, for ALL_IN etc check
336# May need condition, and also enabled if delivery disabled
337# CONF_OCCUPANCY="occupancy"
338# OPTION_OCCUPANCY_DEFAULT="default"
339# OPTIONS_OCCUPANCY=[OPTION_OCCUPANCY_DEFAULT,OPTION_OCCUPANCY_EXCLUDE]
340# OPTION_OCCUPANCY_EXCLUDE="exclude"
343RECIPIENT_SCHEMA = vol.Schema({
344 vol.Required(CONF_PERSON): cv.entity_id,
345 vol.Optional(CONF_ALIAS): cv.string,
346 vol.Optional(CONF_EMAIL): cv.string,
347 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
348 # vol.Optional(CONF_OCCUPANCY,default=OPTION_OCCUPANCY_DEFAULT):vol.In(OPTIONS_OCCUPANCY),
349 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
350 vol.Optional(CONF_PHONE_NUMBER): cv.string,
351 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
352 vol.Optional(CONF_MOBILE_DEVICES, default=list): vol.All(cv.ensure_list, [MOBILE_DEVICE_SCHEMA]),
353 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_CUSTOMIZE_SCHEMA},
354})
355CAMERA_SCHEMA = vol.Schema({
356 vol.Required(CONF_CAMERA): cv.entity_id,
357 vol.Optional(CONF_ALT_CAMERA): vol.All(cv.ensure_list, [cv.entity_id]),
358 vol.Optional(CONF_ALIAS): cv.string,
359 vol.Optional(CONF_URL): cv.url,
360 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
361 vol.Optional(CONF_PTZ_CAMERA): cv.entity_id,
362 vol.Optional(CONF_PTZ_PRESET_DEFAULT, default=1): vol.Any(cv.positive_int, cv.string),
363 vol.Optional(CONF_PTZ_DELAY, default=0): int,
364 vol.Optional(CONF_PTZ_METHOD, default=PTZ_METHOD_ONVIF): vol.In(PTZ_METHOD_VALUES),
365})
366MEDIA_SCHEMA = vol.Schema({
367 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id,
368 vol.Optional(ATTR_MEDIA_CAMERA_DELAY, default=0): int,
369 vol.Optional(ATTR_MEDIA_CAMERA_PTZ_PRESET): vol.Any(cv.positive_int, cv.string),
370 # URL fragments allowed
371 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string),
372 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string),
373 vol.Optional(ATTR_JPEG_OPTS): dict,
374 vol.Optional(ATTR_PNG_OPTS): dict,
375})
378SCENARIO_SCHEMA = vol.All(
379 _migrate_condition,
380 cv.deprecated(key="delivery_selection"),
381 vol.Schema({
382 vol.Optional(CONF_ALIAS): cv.string,
383 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
384 vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA,
385 vol.Optional(CONF_MEDIA): MEDIA_SCHEMA,
386 vol.Optional(CONF_ACTION_GROUP_NAMES, default=[]): vol.All(cv.ensure_list, [cv.string]),
387 vol.Optional("delivery_selection"): cv.string,
388 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)},
389 }),
390)
393def _mobile_action_uri(value: str) -> str:
394 """Validate URI for mobile actions — accepts http/https and homeassistant:// deep links."""
395 if value and not (value.startswith(("http://", "https://", "homeassistant://"))):
396 raise vol.Invalid(f"Invalid URI {value!r}: must start with http://, https://, or homeassistant://")
397 return value
400MOBILE_ACTION_CALL_SCHEMA = vol.Schema(
401 {
402 vol.Optional(ATTR_ACTION): cv.string,
403 vol.Optional(ATTR_TITLE): cv.string,
404 vol.Optional(ATTR_ACTION_CATEGORY): cv.string,
405 vol.Optional(ATTR_ACTION_URL): cv.url,
406 vol.Optional(ATTR_ACTION_URL_TITLE): cv.string,
407 },
408 extra=vol.ALLOW_EXTRA,
409)
410MOBILE_ACTION_SCHEMA = vol.Schema(
411 {
412 vol.Exclusive(CONF_ACTION, CONF_ACTION_TEMPLATE): cv.string,
413 vol.Exclusive(CONF_TITLE, CONF_TITLE_TEMPLATE): cv.string,
414 vol.Optional(CONF_URI): _mobile_action_uri,
415 vol.Optional(CONF_ICON): cv.string,
416 },
417 extra=vol.ALLOW_EXTRA,
418)
421ARCHIVE_SCHEMA = vol.All(
422 cv.deprecated(key=CONF_DEBUG), # deprecated v1.10.0
423 vol.Schema({
424 vol.Optional(CONF_ARCHIVE_PATH): cv.path,
425 vol.Optional(CONF_ENABLED, default=False): cv.boolean,
426 vol.Optional(CONF_ARCHIVE_DAYS, default=3): cv.positive_int,
427 vol.Optional(CONF_ARCHIVE_MQTT_TOPIC): cv.string,
428 vol.Optional(CONF_ARCHIVE_MQTT_QOS, default=0): cv.positive_int,
429 vol.Optional(CONF_ARCHIVE_MQTT_RETAIN, default=True): cv.boolean,
430 vol.Optional(CONF_ARCHIVE_PURGE_INTERVAL, default=60): cv.positive_int,
431 vol.Optional(CONF_ARCHIVE_EVENT_NAME, default="supernotification"): cv.string,
432 vol.Optional(CONF_ARCHIVE_EVENT_SELECTION, default=OutcomeSelection.NONE): parse_event_policy,
433 vol.Optional(CONF_ARCHIVE_DIAGNOSTICS, default=OutcomeSelection.ERROR): parse_event_policy,
434 vol.Optional(CONF_DEBUG, default=False): cv.boolean,
435 }),
436)
438HOUSEKEEPING_SCHEMA = vol.Schema({
439 vol.Optional(CONF_HOUSEKEEPING_TIME, default="00:00:01"): cv.time,
440 vol.Optional(CONF_MEDIA_STORAGE_DAYS, default=7): cv.positive_int,
441})
443PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
444 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path,
445 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path,
446 vol.Optional(CONF_MEDIA_URL_PREFIX, default="/supernotify/media"): cv.string,
447 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA,
448 vol.Optional(CONF_HOUSEKEEPING, default={}): HOUSEKEEPING_SCHEMA,
449 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA,
450 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_SCHEMA},
451 vol.Optional(CONF_ACTION_GROUPS, default=dict): {cv.string: [MOBILE_ACTION_SCHEMA]},
452 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
453 vol.Optional(CONF_RECIPIENTS_DISCOVERY, default=True): cv.boolean,
454 vol.Optional(CONF_RECIPIENTS, default=list): vol.All(cv.ensure_list, [RECIPIENT_SCHEMA]),
455 vol.Optional(CONF_LINKS, default=list): vol.All(cv.ensure_list, [LINK_SCHEMA]),
456 vol.Optional(CONF_SCENARIOS, default=dict): vol.All(
457 {cv.string: SCENARIO_SCHEMA},
458 validate_scenario_names,
459 ),
460 vol.Optional(CONF_TRANSPORTS, default=dict): {cv.string: TRANSPORT_SCHEMA},
461 vol.Optional(CONF_CAMERAS, default=list): vol.All(cv.ensure_list, [CAMERA_SCHEMA]),
462 vol.Optional(CONF_SNOOZE, default=dict): SNOOZE_SCHEMA,
463})
464SUPERNOTIFY_SCHEMA = PLATFORM_SCHEMA
466CHIME_ALIASES_SCHEMA = vol.Schema({
467 vol.Required(OPTION_CHIME_ALIASES, default=dict): vol.Schema({
468 cv.string: vol.Schema({
469 cv.string: vol.Any(
470 vol.Any(None, cv.string, vol.In(OPTIONS_CHIME_DOMAINS)),
471 vol.Schema({
472 vol.Optional(CONF_ALIAS): cv.string,
473 vol.Optional(CONF_DOMAIN): cv.string,
474 vol.Optional(CONF_TUNE): cv.string,
475 vol.Optional(CONF_DATA): DATA_SCHEMA,
476 vol.Optional(CONF_VOLUME): float,
477 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
478 vol.Optional(CONF_DURATION): cv.positive_int,
479 }),
480 )
481 })
482 })
483})
486ACTION_DATA_SCHEMA = vol.Schema(
487 {
488 vol.Optional(ATTR_DELIVERY): vol.Any(cv.string, [cv.string], {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}),
489 vol.Optional(ATTR_PRIORITY): vol.Any(int, str, vol.In(list(PRIORITY_VALUES.keys()))),
490 vol.Optional(ATTR_SCENARIOS_REQUIRE): vol.All(cv.ensure_list, [cv.string]),
491 vol.Optional(ATTR_SCENARIOS_APPLY): vol.All(cv.ensure_list, [cv.string]),
492 vol.Optional(ATTR_SCENARIOS_CONSTRAIN): vol.All(cv.ensure_list, [cv.string]),
493 vol.Optional(ATTR_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
494 vol.Optional(ATTR_RECIPIENTS): vol.All(cv.ensure_list, [cv.entity_id]),
495 vol.Optional(ATTR_MEDIA): MEDIA_SCHEMA,
496 vol.Optional(ATTR_MESSAGE_HTML): cv.string,
497 vol.Optional(ATTR_ACTION_GROUPS, default=[]): vol.All(cv.ensure_list, [cv.string]),
498 vol.Optional(ATTR_ACTIONS, default=[]): vol.All(cv.ensure_list, [MOBILE_ACTION_CALL_SCHEMA]),
499 vol.Optional(ATTR_DEBUG, default=False): cv.boolean,
500 vol.Optional(ATTR_FORCE_RESEND, default=False): cv.boolean,
501 vol.Optional(ATTR_DATA): vol.Any(None, DATA_SCHEMA),
502 vol.Optional(ATTR_TIMESTAMP): cv.string,
503 },
504 extra=vol.ALLOW_EXTRA, # allow other data, e.g. the android/ios mobile push
505)
507STRICT_ACTION_DATA_SCHEMA = ACTION_DATA_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA)