Coverage for custom_components/supernotify/__init__.py: 98%

237 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2026-01-07 15:35 +0000

1"""The Supernotify integration""" 

2 

3import logging 

4import re 

5from collections.abc import Callable 

6from enum import StrEnum 

7from typing import Final 

8 

9import voluptuous as vol 

10from homeassistant.components.notify import PLATFORM_SCHEMA 

11from homeassistant.const import ( 

12 ATTR_DOMAIN, 

13 ATTR_SERVICE, 

14 CONF_ACTION, 

15 CONF_ALIAS, 

16 CONF_CONDITION, 

17 CONF_CONDITIONS, 

18 CONF_DEBUG, 

19 CONF_DESCRIPTION, 

20 CONF_DOMAIN, 

21 CONF_EMAIL, 

22 CONF_ENABLED, 

23 CONF_ICON, 

24 CONF_ID, 

25 CONF_NAME, 

26 CONF_TARGET, 

27 CONF_URL, 

28 Platform, 

29) 

30from homeassistant.helpers import config_validation as cv 

31from homeassistant.helpers.typing import TemplateVarsType 

32 

33_LOGGER = logging.getLogger(__name__) 

34 

35 

36class SelectionRank(StrEnum): 

37 FIRST = "FIRST" 

38 ANY = "ANY" 

39 LAST = "LAST" 

40 

41 

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

43 

44DOMAIN = "supernotify" 

45 

46PLATFORMS = [Platform.NOTIFY] 

47TEMPLATE_DIR: str = "/config/templates/supernotify" 

48MEDIA_DIR: str = "supernotify/media" 

49 

50 

51CONF_ACTIONS: Final[str] = "actions" 

52CONF_TITLE: Final[str] = "title" 

53CONF_URI: Final[str] = "uri" 

54CONF_RECIPIENTS: Final[str] = "recipients" 

55CONF_RECIPIENTS_DISCOVERY: Final[str] = "recipients_discovery" 

56CONF_TEMPLATE_PATH: Final[str] = "template_path" 

57CONF_MEDIA_PATH: Final[str] = "media_path" 

58CONF_HOUSEKEEPING: Final[str] = "housekeeping" 

59CONF_HOUSEKEEPING_TIME: Final[str] = "housekeeping_time" 

60CONF_ARCHIVE_PATH: Final[str] = "file_path" 

61CONF_ARCHIVE: Final[str] = "archive" 

62CONF_ARCHIVE_DAYS: Final[str] = "file_retention_days" 

63CONF_ARCHIVE_MQTT_TOPIC: Final[str] = "mqtt_topic" 

64CONF_ARCHIVE_MQTT_QOS: Final[str] = "mqtt_qos" 

65CONF_ARCHIVE_MQTT_RETAIN: Final[str] = "mqtt_retain" 

66CONF_TEMPLATE: Final[str] = "template" 

67CONF_DELIVERY_DEFAULTS: Final[str] = "delivery_defaults" 

68CONF_LINKS: Final[str] = "links" 

69CONF_PERSON: Final[str] = "person" 

70CONF_TRANSPORT: Final[str] = "transport" 

71CONF_TRANSPORTS: Final[str] = "transports" 

72CONF_DELIVERY: Final[str] = "delivery" 

73CONF_SELECTION: Final[str] = "selection" 

74CONF_SELECTION_RANK: Final[str] = "selection_rank" 

75 

76 

77CONF_DATA: Final[str] = "data" 

78CONF_OPTIONS: Final[str] = "options" 

79CONF_MOBILE: Final[str] = "mobile" 

80CONF_NOTIFY: Final[str] = "notify" 

81 

82CONF_PRIORITY: Final[str] = "priority" 

83CONF_OCCUPANCY: Final[str] = "occupancy" 

84CONF_SCENARIOS: Final[str] = "scenarios" 

85CONF_MANUFACTURER: Final[str] = "manufacturer" 

86CONF_CLASS: Final[str] = "class" 

87 

88 

89CONF_MODEL: Final[str] = "model" 

90CONF_MESSAGE: Final[str] = "message" 

91CONF_TARGET_REQUIRED: Final[str] = "target_required" 

92CONF_MOBILE_DEVICES: Final[str] = "mobile_devices" 

93CONF_MOBILE_DISCOVERY: Final[str] = "mobile_discovery" 

94CONF_ACTION_TEMPLATE: Final[str] = "action_template" 

95CONF_ACTION_GROUPS: Final[str] = "action_groups" 

96CONF_TITLE_TEMPLATE: Final[str] = "title_template" 

97CONF_MEDIA: Final[str] = "media" 

98CONF_CAMERA: Final[str] = "camera" 

99CONF_CLIP_URL: Final[str] = "clip_url" 

100CONF_PTZ_DELAY: Final[str] = "ptz_delay" 

101CONF_PTZ_METHOD: Final[str] = "ptz_method" 

102CONF_PTZ_CAMERA: Final[str] = "ptz_camera" 

103CONF_PTZ_PRESET_DEFAULT: Final[str] = "ptz_default_preset" 

104CONF_ALT_CAMERA: Final[str] = "alt_camera" 

105CONF_CAMERAS: Final[str] = "cameras" 

106CONF_ARCHIVE_PURGE_INTERVAL: Final[str] = "purge_interval" 

107CONF_MEDIA_STORAGE_DAYS: Final[str] = "media_storage_days" 

108 

109OCCUPANCY_ANY_IN = "any_in" 

110OCCUPANCY_ANY_OUT = "any_out" 

111OCCUPANCY_ALL_IN = "all_in" 

112OCCUPANCY_ALL = "all" 

113OCCUPANCY_NONE = "none" 

114OCCUPANCY_ALL_OUT = "all_out" 

115OCCUPANCY_ONLY_IN = "only_in" 

116OCCUPANCY_ONLY_OUT = "only_out" 

117 

118ATTR_ENABLED = "enabled" 

119ATTR_PRIORITY = "priority" 

120ATTR_ACTION = "action" 

121ATTR_SCENARIOS_REQUIRE = "require_scenarios" 

122ATTR_SCENARIOS_APPLY = "apply_scenarios" 

123ATTR_SCENARIOS_CONSTRAIN = "constrain_scenarios" 

124ATTR_DELIVERY = "delivery" 

125ATTR_DEFAULT = "default" 

126ATTR_NOTIFICATION_ID = "notification_id" 

127ATTR_DELIVERY_SELECTION = "delivery_selection" 

128ATTR_RECIPIENTS = "recipients" 

129ATTR_DATA = "data" 

130ATTR_MEDIA = "media" 

131ATTR_TITLE = "title" 

132ATTR_MEDIA_SNAPSHOT_URL = "snapshot_url" 

133ATTR_MEDIA_CAMERA_ENTITY_ID = "camera_entity_id" 

134ATTR_MEDIA_CAMERA_DELAY = "camera_delay" 

135ATTR_MEDIA_CAMERA_PTZ_PRESET = "camera_ptz_preset" 

136ATTR_MEDIA_CLIP_URL = "clip_url" 

137ATTR_MEDIA_SNAPSHOT_PATH = "snapshot_image_path" 

138ATTR_ACTION_GROUPS = "action_groups" 

139CONF_ACTION_GROUP_NAMES = "action_groups" 

140ATTR_ACTION_CATEGORY = "action_category" 

141ATTR_ACTION_URL = "action_url" 

142ATTR_ACTION_URL_TITLE = "action_url_title" 

143ATTR_MESSAGE_HTML = "message_html" 

144ATTR_JPEG_OPTS = "jpeg_opts" 

145ATTR_PNG_OPTS = "png_opts" 

146ATTR_TIMESTAMP = "timestamp" 

147ATTR_DEBUG = "debug" 

148ATTR_ACTIONS = "actions" 

149ATTR_USER_ID = "user_id" 

150ATTR_PERSON_ID = "person_id" 

151ATTR_MOBILE_APP_ID = "mobile_app_id" 

152ATTR_EMAIL = "email" 

153ATTR_PHONE = "phone" 

154ATTR_ALIAS = "alias" 

155 

156DELIVERY_SELECTION_IMPLICIT = "implicit" 

157DELIVERY_SELECTION_EXPLICIT = "explicit" 

158DELIVERY_SELECTION_FIXED = "fixed" 

159 

160DELIVERY_SELECTION_VALUES = [DELIVERY_SELECTION_EXPLICIT, DELIVERY_SELECTION_FIXED, DELIVERY_SELECTION_IMPLICIT] 

161PTZ_METHOD_ONVIF = "onvif" 

162PTZ_METHOD_FRIGATE = "frigate" 

163PTZ_METHOD_VALUES = [PTZ_METHOD_ONVIF, PTZ_METHOD_FRIGATE] 

164 

165SELECTION_FALLBACK_ON_ERROR = "fallback_on_error" 

166SELECTION_FALLBACK = "fallback" 

167SELECTION_BY_SCENARIO = "scenario" 

168SELECTION_DEFAULT = "default" 

169SELECTION_EXPLICIT = "explicit" 

170SELECTION_VALUES = [ 

171 SELECTION_FALLBACK_ON_ERROR, 

172 SELECTION_EXPLICIT, 

173 SELECTION_BY_SCENARIO, 

174 SELECTION_DEFAULT, 

175 SELECTION_FALLBACK, 

176] 

177 

178OCCUPANCY_VALUES = [ 

179 OCCUPANCY_ALL_IN, 

180 OCCUPANCY_ALL_OUT, 

181 OCCUPANCY_ANY_IN, 

182 OCCUPANCY_ANY_OUT, 

183 OCCUPANCY_ONLY_IN, 

184 OCCUPANCY_ONLY_OUT, 

185 OCCUPANCY_ALL, 

186 OCCUPANCY_NONE, 

187] 

188 

189PRIORITY_CRITICAL = "critical" 

190PRIORITY_HIGH = "high" 

191PRIORITY_MEDIUM = "medium" 

192PRIORITY_LOW = "low" 

193PRIORITY_MINIMUM = "minimum" 

194 

195PRIORITY_VALUES: dict[str, int] = { 

196 PRIORITY_MINIMUM: 1, 

197 PRIORITY_LOW: 2, 

198 PRIORITY_MEDIUM: 3, 

199 PRIORITY_HIGH: 4, 

200 PRIORITY_CRITICAL: 5, 

201} 

202 

203CONF_TARGET_USAGE = "target_usage" 

204TARGET_USE_ON_NO_DELIVERY_TARGETS = "no_delivery" 

205TARGET_USE_ON_NO_ACTION_TARGETS = "no_action" 

206TARGET_USE_FIXED = "fixed" 

207TARGET_USE_MERGE_ALWAYS = "merge_always" 

208TARGET_USE_MERGE_ON_DELIVERY_TARGETS = "merge_delivery" 

209 

210OPTION_SIMPLIFY_TEXT = "simplify_text" 

211OPTION_STRIP_URLS = "strip_urls" 

212OPTION_MESSAGE_USAGE = "message_usage" 

213OPTION_RAW = "raw" 

214OPTION_JPEG = "jpeg_opts" 

215OPTION_PNG = "png_opts" 

216OPTION_TTS_ENTITY_ID = "tts_entity_id" 

217MEDIA_OPTION_REPROCESS = "reprocess" 

218OPTION_TARGET_CATEGORIES = "target_categories" 

219OPTION_UNIQUE_TARGETS = "unique_targets" 

220OPTION_TARGET_INCLUDE_RE = "target_include_re" # deprecated v1.9.0 

221OPTION_TARGET_SELECT = "target_select" 

222OPTION_CHIME_ALIASES = "chime_aliases" 

223OPTION_DATA_KEYS_SELECT = "data_keys_select" 

224OPTION_DATA_KEYS_INCLUDE_RE = "data_keys_include_re" # deprecated v1.9.0 

225OPTION_DATA_KEYS_EXCLUDE_RE = "data_keys_exclude_re" # deprecated v1.9.0 

226OPTION_GENERIC_DOMAIN_STYLE = "handle_as_domain" 

227OPTION_STRICT_TEMPLATE = "strict_template" 

228 

229SELECT_INCLUDE = "include" 

230SELECT_EXCLUDE = "exclude" 

231 

232RE_DEVICE_ID = r"^[0-9a-f]{32}$" 

233 

234RESERVED_DELIVERY_NAMES: list[str] = ["ALL"] 

235RESERVED_SCENARIO_NAMES: list[str] = ["NULL"] 

236RESERVED_DATA_KEYS: list[str] = [ATTR_DOMAIN, ATTR_SERVICE, "action"] 

237 

238 

239CONF_DUPE_CHECK: Final[str] = "dupe_check" 

240CONF_DUPE_POLICY: Final[str] = "dupe_policy" 

241CONF_TTL: Final[str] = "ttl" 

242CONF_SIZE: Final[str] = "size" 

243ATTR_DUPE_POLICY_MTSLP: Final[str] = "dupe_policy_message_title_same_or_lower_priority" 

244ATTR_DUPE_POLICY_NONE: Final[str] = "dupe_policy_none" 

245CONF_MOBILE_APP_ID: Final[str] = "mobile_app_id" 

246CONF_TRANSPORT_DATA: Final[str] = "transport_data" 

247 

248 

249def phone(value: str) -> str: 

250 """Validate a phone number""" 

251 regex = re.compile(r"^(\+\d{1,3})?\s?\(?\d{1,4}\)?[\s.-]?\d{3}[\s.-]?\d{4}$") 

252 if not regex.match(value): 

253 raise vol.Invalid("Invalid Phone Number") 

254 return str(value) 

255 

256 

257# TARGET_FIELDS includes entity, device, area, floor, label ids 

258TARGET_SCHEMA = vol.Any( # order of schema matters, voluptuous forces into first it finds that works 

259 cv.TARGET_FIELDS 

260 | { 

261 vol.Optional(ATTR_EMAIL): vol.All(cv.ensure_list, [vol.Email]), 

262 vol.Optional(ATTR_PHONE): vol.All(cv.ensure_list, [phone]), 

263 vol.Optional(ATTR_MOBILE_APP_ID): vol.All(cv.ensure_list, [cv.service]), 

264 vol.Optional(ATTR_PERSON_ID): vol.All(cv.ensure_list, [cv.entity_id]), 

265 vol.Optional(cv.string): vol.All(cv.ensure_list, [str]), 

266 }, 

267 str, 

268 list[str], 

269) 

270 

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

272 

273CONF_DEVICE_TRACKER: Final[str] = "device_tracker" 

274MOBILE_DEVICE_SCHEMA = vol.Schema({ 

275 vol.Optional(CONF_MANUFACTURER): cv.string, 

276 vol.Optional(CONF_MODEL): cv.string, 

277 vol.Optional(CONF_CLASS): cv.string, 

278 vol.Optional(CONF_MOBILE_APP_ID): cv.string, 

279 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id, 

280 vol.Optional(CONF_ENABLED, default=True): cv.boolean, 

281}) 

282NOTIFICATION_DUPE_SCHEMA = vol.Schema({ 

283 vol.Optional(CONF_TTL): cv.positive_int, 

284 vol.Optional(CONF_SIZE, default=100): cv.positive_int, 

285 vol.Optional(CONF_DUPE_POLICY, default=ATTR_DUPE_POLICY_MTSLP): vol.In([ATTR_DUPE_POLICY_MTSLP, ATTR_DUPE_POLICY_NONE]), 

286}) 

287 

288 

289DELIVERY_CUSTOMIZE_SCHEMA = vol.All( 

290 vol.Schema( 

291 { 

292 vol.Optional(CONF_TARGET): TARGET_SCHEMA, 

293 vol.Optional(CONF_ENABLED): vol.Any(None, cv.boolean), 

294 vol.Optional(CONF_DATA): DATA_SCHEMA, 

295 }, 

296 ), 

297) 

298LINK_SCHEMA = vol.Schema({ 

299 vol.Required(CONF_URL): cv.url, 

300 vol.Required(CONF_DESCRIPTION): cv.string, 

301 vol.Optional(CONF_ID): cv.string, 

302 vol.Optional(CONF_ICON): cv.icon, 

303 vol.Optional(CONF_NAME): cv.string, 

304}) 

305 

306 

307CONF_DEVICE_NAME: Final[str] = "device_name" 

308CONF_DEVICE_LABELS: Final[str] = "device_labels" 

309 

310OPTION_DEVICE_DOMAIN: Final[str] = "device_domain" 

311 

312OPTION_DEVICE_MODEL_SELECT: Final[str] = "device_model_select" 

313OPTION_DEVICE_MANUFACTURER_SELECT: Final[str] = "device_manufacturer_select" 

314OPTION_DEVICE_OS_SELECT: Final[str] = "device_os_select" 

315OPTION_DEVICE_LABEL_SELECT: Final[str] = "device_label_select" 

316OPTION_DEVICE_AREA_SELECT: Final[str] = "device_area_select" 

317OPTION_DEVICE_DISCOVERY: Final[str] = "device_discovery" 

318 

319 

320TARGET_REQUIRE_ALWAYS = "always" 

321TARGET_REQUIRE_NEVER = "never" 

322TARGET_REQUIRE_OPTIONAL = "optional" 

323 

324DELIVERY_CONFIG_SCHEMA = vol.Schema({ # shared by Transport Defaults and Delivery definitions 

325 # defaults set in model.DeliveryConfig 

326 vol.Optional(CONF_ACTION): cv.service, # previously 'service:' 

327 vol.Optional(CONF_DEBUG): cv.boolean, 

328 vol.Optional(CONF_OPTIONS): dict, # transport tuning 

329 vol.Optional(CONF_DATA): DATA_SCHEMA, 

330 vol.Optional(CONF_TARGET): TARGET_SCHEMA, 

331 vol.Optional(CONF_TARGET_REQUIRED): vol.Any( 

332 cv.boolean, vol.In([TARGET_REQUIRE_ALWAYS, TARGET_REQUIRE_NEVER, TARGET_REQUIRE_OPTIONAL]) 

333 ), 

334 vol.Optional(CONF_TARGET_USAGE): vol.In([ 

335 TARGET_USE_ON_NO_DELIVERY_TARGETS, 

336 TARGET_USE_ON_NO_ACTION_TARGETS, 

337 TARGET_USE_MERGE_ON_DELIVERY_TARGETS, 

338 TARGET_USE_MERGE_ALWAYS, 

339 TARGET_USE_FIXED, 

340 ]), 

341 vol.Optional(CONF_SELECTION): vol.All(cv.ensure_list, [vol.In(SELECTION_VALUES)]), 

342 vol.Optional(CONF_PRIORITY): vol.All(cv.ensure_list, [vol.Any(int, str, vol.In(list(PRIORITY_VALUES.keys())))]), 

343 vol.Optional(CONF_SELECTION_RANK): vol.In([ 

344 SelectionRank.ANY, 

345 SelectionRank.FIRST, 

346 SelectionRank.LAST, 

347 ]), 

348}) 

349 

350TRANSPORT_SMS = "sms" 

351TRANSPORT_TTS = "tts" 

352TRANSPORT_MQTT = "mqtt" 

353TRANSPORT_EMAIL = "email" 

354TRANSPORT_ALEXA = "alexa_devices" 

355TRANSPORT_ALEXA_MEDIA_PLAYER = "alexa_media_player" 

356TRANSPORT_MOBILE_PUSH = "mobile_push" 

357TRANSPORT_MEDIA = "media" 

358TRANSPORT_CHIME = "chime" 

359TRANSPORT_GENERIC = "generic" 

360TRANSPORT_NOTIFY_ENTITY = "notify_entity" 

361TRANSPORT_PERSISTENT = "persistent" 

362TRANSPORT_VALUES = [ 

363 TRANSPORT_SMS, 

364 TRANSPORT_MQTT, 

365 TRANSPORT_ALEXA, 

366 TRANSPORT_ALEXA_MEDIA_PLAYER, 

367 TRANSPORT_MOBILE_PUSH, 

368 TRANSPORT_CHIME, 

369 TRANSPORT_EMAIL, 

370 TRANSPORT_MEDIA, 

371 TRANSPORT_PERSISTENT, 

372 TRANSPORT_TTS, 

373 TRANSPORT_GENERIC, 

374 TRANSPORT_NOTIFY_ENTITY, 

375] 

376 

377DELIVERY_SCHEMA = vol.All( 

378 cv.deprecated(key=CONF_CONDITION), 

379 DELIVERY_CONFIG_SCHEMA.extend({ 

380 vol.Required(CONF_TRANSPORT): vol.In(TRANSPORT_VALUES), 

381 vol.Optional(CONF_ALIAS): cv.string, 

382 vol.Optional(CONF_TEMPLATE): cv.string, 

383 vol.Optional(CONF_MESSAGE): vol.Any(None, cv.string), 

384 vol.Optional(CONF_TITLE): vol.Any(None, cv.string), 

385 vol.Optional(CONF_ENABLED): cv.boolean, 

386 vol.Optional(CONF_OCCUPANCY, default=OCCUPANCY_ALL): vol.In(OCCUPANCY_VALUES), 

387 vol.Optional(CONF_CONDITION): cv.CONDITIONS_SCHEMA, 

388 vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA, 

389 }), 

390) 

391 

392CONF_DEVICE_DISCOVERY: Final[str] = "device_discovery" 

393CONF_DEVICE_DOMAIN: Final[str] = OPTION_DEVICE_DOMAIN 

394CONF_DEVICE_MODEL_INCLUDE: Final[str] = "device_model_include" 

395CONF_DEVICE_MODEL_EXCLUDE: Final[str] = "device_model_exclude" 

396TRANSPORT_SCHEMA = vol.All( 

397 cv.deprecated(key=CONF_DEVICE_DOMAIN), # deprecated v1.9.0 

398 cv.deprecated(key=CONF_DEVICE_DISCOVERY), # deprecated v1.9.0 

399 cv.deprecated(key=CONF_DEVICE_MODEL_INCLUDE), # deprecated v1.9.0 

400 cv.deprecated(key=CONF_DEVICE_MODEL_EXCLUDE), # deprecated v1.9.0 

401 vol.Schema({ 

402 vol.Optional(CONF_ALIAS): cv.string, 

403 vol.Optional(CONF_DEVICE_DOMAIN): vol.All(cv.ensure_list, [cv.string]), 

404 vol.Optional(CONF_DEVICE_MODEL_INCLUDE): vol.All(cv.ensure_list, [cv.string]), 

405 vol.Optional(CONF_DEVICE_MODEL_EXCLUDE): vol.All(cv.ensure_list, [cv.string]), 

406 vol.Optional(CONF_DEVICE_DISCOVERY): cv.boolean, 

407 vol.Optional(CONF_ENABLED, default=True): cv.boolean, 

408 vol.Optional(CONF_DELIVERY_DEFAULTS): DELIVERY_CONFIG_SCHEMA, 

409 }), 

410) 

411# Idea - differentiate enabled as recipient vs as occupant, for ALL_IN etc check 

412# May need condition, and also enabled if delivery disabled 

413# CONF_OCCUPANCY="occupancy" 

414# OPTION_OCCUPANCY_DEFAULT="default" 

415# OPTIONS_OCCUPANCY=[OPTION_OCCUPANCY_DEFAULT,OPTION_OCCUPANCY_EXCLUDE] 

416# OPTION_OCCUPANCY_EXCLUDE="exclude" 

417 

418CONF_PHONE_NUMBER: str = "phone_number" 

419 

420RECIPIENT_SCHEMA = vol.Schema({ 

421 vol.Required(CONF_PERSON): cv.entity_id, 

422 vol.Optional(CONF_ALIAS): cv.string, 

423 vol.Optional(CONF_EMAIL): cv.string, 

424 vol.Optional(CONF_ENABLED, default=True): cv.boolean, 

425 # vol.Optional(CONF_OCCUPANCY,default=OPTION_OCCUPANCY_DEFAULT):vol.In(OPTIONS_OCCUPANCY), 

426 vol.Optional(CONF_TARGET): TARGET_SCHEMA, 

427 vol.Optional(CONF_PHONE_NUMBER): cv.string, 

428 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean, 

429 vol.Optional(CONF_MOBILE_DEVICES, default=list): vol.All(cv.ensure_list, [MOBILE_DEVICE_SCHEMA]), 

430 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_CUSTOMIZE_SCHEMA}, 

431}) 

432CAMERA_SCHEMA = vol.Schema({ 

433 vol.Required(CONF_CAMERA): cv.entity_id, 

434 vol.Optional(CONF_ALT_CAMERA): vol.All(cv.ensure_list, [cv.entity_id]), 

435 vol.Optional(CONF_ALIAS): cv.string, 

436 vol.Optional(CONF_URL): cv.url, 

437 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id, 

438 vol.Optional(CONF_PTZ_CAMERA): cv.entity_id, 

439 vol.Optional(CONF_PTZ_PRESET_DEFAULT, default=1): vol.Any(cv.positive_int, cv.string), 

440 vol.Optional(CONF_PTZ_DELAY, default=0): int, 

441 vol.Optional(CONF_PTZ_METHOD, default=PTZ_METHOD_ONVIF): vol.In(PTZ_METHOD_VALUES), 

442}) 

443MEDIA_SCHEMA = vol.Schema({ 

444 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id, 

445 vol.Optional(ATTR_MEDIA_CAMERA_DELAY, default=0): int, 

446 vol.Optional(ATTR_MEDIA_CAMERA_PTZ_PRESET): vol.Any(cv.positive_int, cv.string), 

447 # URL fragments allowed 

448 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string), 

449 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string), 

450 vol.Optional(ATTR_JPEG_OPTS): dict, 

451 vol.Optional(ATTR_PNG_OPTS): dict, 

452}) 

453 

454 

455SCENARIO_SCHEMA = vol.All( 

456 cv.deprecated(key=CONF_CONDITION), 

457 cv.deprecated(key="delivery_selection"), 

458 vol.Schema({ 

459 vol.Optional(CONF_ALIAS): cv.string, 

460 vol.Optional(CONF_ENABLED, default=True): cv.boolean, 

461 vol.Optional(CONF_CONDITION): cv.CONDITIONS_SCHEMA, 

462 vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA, 

463 vol.Optional(CONF_MEDIA): MEDIA_SCHEMA, 

464 vol.Optional(CONF_ACTION_GROUP_NAMES, default=[]): vol.All(cv.ensure_list, [cv.string]), 

465 vol.Optional("delivery_selection"): cv.string, 

466 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}, 

467 }), 

468) 

469MOBILE_ACTION_CALL_SCHEMA = vol.Schema( 

470 { 

471 vol.Optional(ATTR_ACTION): cv.string, 

472 vol.Optional(ATTR_TITLE): cv.string, 

473 vol.Optional(ATTR_ACTION_CATEGORY): cv.string, 

474 vol.Optional(ATTR_ACTION_URL): cv.url, 

475 vol.Optional(ATTR_ACTION_URL_TITLE): cv.string, 

476 }, 

477 extra=vol.ALLOW_EXTRA, 

478) 

479MOBILE_ACTION_SCHEMA = vol.Schema( 

480 { 

481 vol.Exclusive(CONF_ACTION, CONF_ACTION_TEMPLATE): cv.string, 

482 vol.Exclusive(CONF_TITLE, CONF_TITLE_TEMPLATE): cv.string, 

483 vol.Optional(CONF_URI): cv.url, 

484 vol.Optional(CONF_ICON): cv.string, 

485 }, 

486 extra=vol.ALLOW_EXTRA, 

487) 

488 

489 

490ARCHIVE_SCHEMA = vol.Schema({ 

491 vol.Optional(CONF_ARCHIVE_PATH): cv.path, 

492 vol.Optional(CONF_ENABLED, default=False): cv.boolean, 

493 vol.Optional(CONF_ARCHIVE_DAYS, default=3): cv.positive_int, 

494 vol.Optional(CONF_ARCHIVE_MQTT_TOPIC): cv.string, 

495 vol.Optional(CONF_ARCHIVE_MQTT_QOS, default=0): cv.positive_int, 

496 vol.Optional(CONF_ARCHIVE_MQTT_RETAIN, default=True): cv.boolean, 

497 vol.Optional(CONF_ARCHIVE_PURGE_INTERVAL, default=60): cv.positive_int, 

498 vol.Optional(CONF_DEBUG, default=False): cv.boolean, 

499}) 

500 

501HOUSEKEEPING_SCHEMA = vol.Schema({ 

502 vol.Optional(CONF_HOUSEKEEPING_TIME, default="00:00:01"): cv.time, 

503 vol.Optional(CONF_MEDIA_STORAGE_DAYS, default=7): cv.positive_int, 

504}) 

505 

506PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 

507 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path, 

508 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path, 

509 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA, 

510 vol.Optional(CONF_HOUSEKEEPING, default={}): HOUSEKEEPING_SCHEMA, 

511 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA, 

512 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_SCHEMA}, 

513 vol.Optional(CONF_ACTION_GROUPS, default=dict): {cv.string: [MOBILE_ACTION_SCHEMA]}, 

514 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean, 

515 vol.Optional(CONF_RECIPIENTS_DISCOVERY, default=True): cv.boolean, 

516 vol.Optional(CONF_RECIPIENTS, default=list): vol.All(cv.ensure_list, [RECIPIENT_SCHEMA]), 

517 vol.Optional(CONF_LINKS, default=list): vol.All(cv.ensure_list, [LINK_SCHEMA]), 

518 vol.Optional(CONF_SCENARIOS, default=dict): {cv.string: SCENARIO_SCHEMA}, 

519 vol.Optional(CONF_TRANSPORTS, default=dict): {cv.string: TRANSPORT_SCHEMA}, 

520 vol.Optional(CONF_CAMERAS, default=list): vol.All(cv.ensure_list, [CAMERA_SCHEMA]), 

521}) 

522SUPERNOTIFY_SCHEMA = PLATFORM_SCHEMA 

523 

524 

525CONF_TUNE: Final[str] = "tune" 

526CONF_VOLUME: Final[str] = "volume" 

527CONF_DURATION: Final[str] = "duration" 

528 

529OPTIONS_CHIME_DOMAINS = ["media_player", "switch", "script", "rest_command", "siren", "alexa_devices"] 

530 

531CHIME_ALIASES_SCHEMA = vol.Schema({ 

532 vol.Required(OPTION_CHIME_ALIASES, default=dict): vol.Schema({ 

533 cv.string: vol.Schema({ 

534 cv.string: vol.Any( 

535 vol.Any(None, cv.string, vol.In(OPTIONS_CHIME_DOMAINS)), 

536 vol.Schema({ 

537 vol.Optional(CONF_ALIAS): cv.string, 

538 vol.Optional(CONF_DOMAIN): cv.string, 

539 vol.Optional(CONF_TUNE): cv.string, 

540 vol.Optional(CONF_DATA): DATA_SCHEMA, 

541 vol.Optional(CONF_VOLUME): float, 

542 vol.Optional(CONF_TARGET): TARGET_SCHEMA, 

543 vol.Optional(CONF_DURATION): cv.positive_int, 

544 }), 

545 ) 

546 }) 

547 }) 

548}) 

549 

550 

551ACTION_DATA_SCHEMA = vol.Schema( 

552 { 

553 vol.Optional(ATTR_DELIVERY): vol.Any(cv.string, [cv.string], {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}), 

554 vol.Optional(ATTR_PRIORITY): vol.Any(int, str, vol.In(list(PRIORITY_VALUES.keys()))), 

555 vol.Optional(ATTR_SCENARIOS_REQUIRE): vol.All(cv.ensure_list, [cv.string]), 

556 vol.Optional(ATTR_SCENARIOS_APPLY): vol.All(cv.ensure_list, [cv.string]), 

557 vol.Optional(ATTR_SCENARIOS_CONSTRAIN): vol.All(cv.ensure_list, [cv.string]), 

558 vol.Optional(ATTR_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES), 

559 vol.Optional(ATTR_RECIPIENTS): vol.All(cv.ensure_list, [cv.entity_id]), 

560 vol.Optional(ATTR_MEDIA): MEDIA_SCHEMA, 

561 vol.Optional(ATTR_MESSAGE_HTML): cv.string, 

562 vol.Optional(ATTR_ACTION_GROUPS, default=[]): vol.All(cv.ensure_list, [cv.string]), 

563 vol.Optional(ATTR_ACTIONS, default=[]): vol.All(cv.ensure_list, [MOBILE_ACTION_CALL_SCHEMA]), 

564 vol.Optional(ATTR_DEBUG, default=False): cv.boolean, 

565 vol.Optional(ATTR_DATA): vol.Any(None, DATA_SCHEMA), 

566 vol.Optional(ATTR_TIMESTAMP): cv.string, 

567 }, 

568 extra=vol.ALLOW_EXTRA, # allow other data, e.g. the android/ios mobile push 

569) 

570 

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