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