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

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.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 

32 

33from custom_components.supernotify import ARCHIVE_DIR, MEDIA_DIR, TEMPLATE_DIR 

34 

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) 

164 

165 

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 

175 

176 

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

188 

189 

190class DeliveryOutcome(StrEnum): 

191 SUCCESS = auto() 

192 NO_DELIVERY = auto() 

193 PARTIAL_DELIVERY = auto() 

194 FALLBACK_DELIVERY = auto() 

195 ERROR = auto() 

196 DUPE = auto() 

197 

198 

199class EnvelopeOutcome(StrEnum): 

200 SUCCESS = auto() 

201 ERROR = auto() 

202 SKIPPED = auto() # envelope not generated 

203 SUPPRESSED = auto() # envelope generated but intentionally not delivered 

204 

205 

206class SelectionRank(StrEnum): 

207 FIRST = "FIRST" 

208 ANY = "ANY" 

209 LAST = "LAST" 

210 

211 

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

213 

214 

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) 

221 

222 

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 

229 

230 

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) 

244 

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

246 

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

264 

265 

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

282 

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

284 

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

310 

311 

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 

319 

320 

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) 

334 

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

344 

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" 

368 

369 

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

403 

404 

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) 

418 

419 

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 

425 

426 

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) 

446 

447 

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) 

464 

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

469 

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) 

491 

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) 

512 

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

526 

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

545 

546 

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) 

567 

568ACTION_DATA_SCHEMA = vol.All( 

569 cv.deprecated(key=ATTR_RECIPIENTS), # deprecated v2.2.0 

570 _ACTION_DATA_FIELDS_SCHEMA, 

571) 

572 

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)