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

1"""The Supernotify integration""" 

2 

3import re 

4from collections.abc import Callable 

5from enum import IntFlag, StrEnum, auto 

6 

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 

27 

28from custom_components.supernotify import MEDIA_DIR, TEMPLATE_DIR 

29 

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) 

156 

157 

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 

167 

168 

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}") 

180 

181 

182class Outcome(StrEnum): 

183 SUCCESS = auto() 

184 NO_DELIVERY = auto() 

185 PARTIAL_DELIVERY = auto() 

186 FALLBACK_DELIVERY = auto() 

187 ERROR = auto() 

188 DUPE = auto() 

189 

190 

191class SelectionRank(StrEnum): 

192 FIRST = "FIRST" 

193 ANY = "ANY" 

194 LAST = "LAST" 

195 

196 

197type ConditionsFunc = Callable[[TemplateVarsType], bool] 

198 

199 

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) 

206 

207 

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 

214 

215 

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) 

229 

230DATA_SCHEMA = vol.Schema({vol.NotIn(RESERVED_DATA_KEYS): vol.Any(str, int, bool, float, dict, list)}) 

231 

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}) 

249 

250 

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}) 

267 

268SNOOZE_SCHEMA = vol.Schema({vol.Optional(CONF_SNOOZE_TIME, default=60 * 60): cv.positive_int}) 

269 

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}) 

295 

296 

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 

304 

305 

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) 

319 

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" 

341 

342 

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}) 

376 

377 

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) 

391 

392 

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 

398 

399 

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) 

419 

420 

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) 

437 

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}) 

442 

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 

465 

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}) 

484 

485 

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) 

506 

507STRICT_ACTION_DATA_SCHEMA = ACTION_DATA_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA)