Coverage for custom_components/supernotify/__init__.py: 100%
194 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-21 23:31 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-11-21 23:31 +0000
1"""The Supernotify integration"""
3import logging
4from enum import StrEnum
6import voluptuous as vol
7from homeassistant.components.notify import PLATFORM_SCHEMA
8from homeassistant.const import (
9 ATTR_DOMAIN,
10 ATTR_SERVICE,
11 CONF_ACTION,
12 CONF_ALIAS,
13 CONF_CONDITION,
14 CONF_DEBUG,
15 CONF_DESCRIPTION,
16 CONF_EMAIL,
17 CONF_ENABLED,
18 CONF_ICON,
19 CONF_ID,
20 CONF_NAME,
21 CONF_TARGET,
22 CONF_URL,
23 Platform,
24)
25from homeassistant.helpers import config_validation as cv
27_LOGGER = logging.getLogger(__name__)
30class SelectionRank(StrEnum):
31 FIRST = "FIRST"
32 ANY = "ANY"
33 LAST = "LAST"
36DOMAIN = "supernotify"
38PLATFORMS = [Platform.NOTIFY]
39TEMPLATE_DIR = "/config/templates/supernotify"
40MEDIA_DIR = "supernotify/media"
43CONF_ACTIONS = "actions"
44CONF_TITLE = "title"
45CONF_URI = "uri"
46CONF_RECIPIENTS = "recipients"
47CONF_TEMPLATE_PATH = "template_path"
48CONF_MEDIA_PATH = "media_path"
49CONF_HOUSEKEEPING = "housekeeping"
50CONF_HOUSEKEEPING_TIME = "housekeeping_time"
51CONF_ARCHIVE_PATH = "file_path"
52CONF_ARCHIVE = "archive"
53CONF_ARCHIVE_DAYS = "file_retention_days"
54CONF_ARCHIVE_MQTT_TOPIC = "mqtt_topic"
55CONF_ARCHIVE_MQTT_QOS = "mqtt_qos"
56CONF_ARCHIVE_MQTT_RETAIN = "mqtt_retain"
57CONF_TEMPLATE = "template"
58CONF_DELIVERY_DEFAULTS = "delivery_defaults"
59CONF_LINKS = "links"
60CONF_PERSON = "person"
61CONF_TRANSPORT = "transport"
62CONF_TRANSPORTS = "transports"
63CONF_DELIVERY = "delivery"
64CONF_SELECTION = "selection"
65CONF_SELECTION_RANK = "selection_rank"
67CONF_DATA: str = "data"
68CONF_OPTIONS: str = "options"
69CONF_MOBILE: str = "mobile"
70CONF_NOTIFY: str = "notify"
71CONF_MOBILE_APP_ID: str = "mobile_app_id"
72CONF_PHONE_NUMBER: str = "phone_number"
73CONF_PRIORITY: str = "priority"
74CONF_OCCUPANCY: str = "occupancy"
75CONF_SCENARIOS: str = "scenarios"
76CONF_MANUFACTURER: str = "manufacturer"
77CONF_DEVICE_DISCOVERY: str = "device_discovery"
78CONF_DEVICE_TRACKER: str = "device_tracker"
79CONF_DEVICE_NAME: str = "device_name"
80CONF_DEVICE_LABELS: str = "device_labels"
81CONF_DEVICE_DOMAIN: str = "device_domain"
83CONF_MODEL: str = "model"
84CONF_MESSAGE: str = "message"
85CONF_TARGET_REQUIRED: str = "target_required"
86CONF_MOBILE_DEVICES: str = "mobile_devices"
87CONF_MOBILE_DISCOVERY: str = "mobile_discovery"
88CONF_ACTION_TEMPLATE: str = "action_template"
89CONF_ACTION_GROUPS: str = "action_groups"
90CONF_TITLE_TEMPLATE: str = "title_template"
91CONF_DELIVERY_SELECTION: str = "delivery_selection"
92CONF_MEDIA: str = "media"
93CONF_CAMERA: str = "camera"
94CONF_CLIP_URL: str = "clip_url"
95CONF_SNAPSHOT_URL: str = "snapshot_url"
96CONF_PTZ_DELAY: str = "ptz_delay"
97CONF_PTZ_METHOD: str = "ptz_method"
98CONF_PTZ_PRESET_DEFAULT: str = "ptz_default_preset"
99CONF_ALT_CAMERA: str = "alt_camera"
100CONF_CAMERAS: str = "cameras"
101CONF_ARCHIVE_PURGE_INTERVAL: str = "purge_interval"
103OCCUPANCY_ANY_IN = "any_in"
104OCCUPANCY_ANY_OUT = "any_out"
105OCCUPANCY_ALL_IN = "all_in"
106OCCUPANCY_ALL = "all"
107OCCUPANCY_NONE = "none"
108OCCUPANCY_ALL_OUT = "all_out"
109OCCUPANCY_ONLY_IN = "only_in"
110OCCUPANCY_ONLY_OUT = "only_out"
112ATTR_ENABLED = "enabled"
113ATTR_PRIORITY = "priority"
114ATTR_ACTION = "action"
115ATTR_SCENARIOS_REQUIRE = "require_scenarios"
116ATTR_SCENARIOS_APPLY = "apply_scenarios"
117ATTR_SCENARIOS_CONSTRAIN = "constrain_scenarios"
118ATTR_DELIVERY = "delivery"
119ATTR_DEFAULT = "default"
120ATTR_NOTIFICATION_ID = "notification_id"
121ATTR_DELIVERY_SELECTION = "delivery_selection"
122ATTR_RECIPIENTS = "recipients"
123ATTR_DATA = "data"
124ATTR_MEDIA = "media"
125ATTR_TITLE = "title"
126ATTR_MEDIA_SNAPSHOT_URL = "snapshot_url"
127ATTR_MEDIA_CAMERA_ENTITY_ID = "camera_entity_id"
128ATTR_MEDIA_CAMERA_DELAY = "camera_delay"
129ATTR_MEDIA_CAMERA_PTZ_PRESET = "camera_ptz_preset"
130ATTR_MEDIA_CLIP_URL = "clip_url"
131ATTR_ACTION_GROUPS = "action_groups"
132CONF_ACTION_GROUP_NAMES = "action_groups"
133ATTR_ACTION_CATEGORY = "action_category"
134ATTR_ACTION_URL = "action_url"
135ATTR_ACTION_URL_TITLE = "action_url_title"
136ATTR_MESSAGE_HTML = "message_html"
137ATTR_JPEG_OPTS = "jpeg_opts"
138ATTR_TIMESTAMP = "timestamp"
139ATTR_DEBUG = "debug"
140ATTR_ACTIONS = "actions"
141ATTR_USER_ID = "user_id"
142ATTR_PERSON_ID = "person_id"
143ATTR_MOBILE_APP_ID = "mobile_app_id"
144ATTR_EMAIL = "email"
145ATTR_PHONE = "phone"
147DELIVERY_SELECTION_IMPLICIT = "implicit"
148DELIVERY_SELECTION_EXPLICIT = "explicit"
149DELIVERY_SELECTION_FIXED = "fixed"
151DELIVERY_SELECTION_VALUES = [DELIVERY_SELECTION_EXPLICIT, DELIVERY_SELECTION_FIXED, DELIVERY_SELECTION_IMPLICIT]
152PTZ_METHOD_ONVIF = "onvif"
153PTZ_METHOD_FRIGATE = "frigate"
154PTZ_METHOD_VALUES = [PTZ_METHOD_ONVIF, PTZ_METHOD_FRIGATE]
156SELECTION_FALLBACK_ON_ERROR = "fallback_on_error"
157SELECTION_FALLBACK = "fallback"
158SELECTION_BY_SCENARIO = "scenario"
159SELECTION_DEFAULT = "default"
160SELECTION_VALUES = [SELECTION_FALLBACK_ON_ERROR, SELECTION_BY_SCENARIO, SELECTION_DEFAULT, SELECTION_FALLBACK]
162OCCUPANCY_VALUES = [
163 OCCUPANCY_ALL_IN,
164 OCCUPANCY_ALL_OUT,
165 OCCUPANCY_ANY_IN,
166 OCCUPANCY_ANY_OUT,
167 OCCUPANCY_ONLY_IN,
168 OCCUPANCY_ONLY_OUT,
169 OCCUPANCY_ALL,
170 OCCUPANCY_NONE,
171]
173PRIORITY_CRITICAL = "critical"
174PRIORITY_HIGH = "high"
175PRIORITY_MEDIUM = "medium"
176PRIORITY_LOW = "low"
178PRIORITY_VALUES = [PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH, PRIORITY_CRITICAL]
179TRANSPORT_SMS = "sms"
180TRANSPORT_MQTT = "mqtt"
181TRANSPORT_EMAIL = "email"
182TRANSPORT_ALEXA = "alexa_devices"
183TRANSPORT_ALEXA_MEDIA_PLAYER = "alexa_media_player"
184TRANSPORT_MOBILE_PUSH = "mobile_push"
185TRANSPORT_MEDIA = "media"
186TRANSPORT_CHIME = "chime"
187TRANSPORT_GENERIC = "generic"
188TRANSPORT_NOTIFY_ENTITY = "notify_entity"
189TRANSPORT_PERSISTENT = "persistent"
190TRANSPORT_VALUES = [
191 TRANSPORT_SMS,
192 TRANSPORT_MQTT,
193 TRANSPORT_ALEXA,
194 TRANSPORT_ALEXA_MEDIA_PLAYER,
195 TRANSPORT_MOBILE_PUSH,
196 TRANSPORT_CHIME,
197 TRANSPORT_EMAIL,
198 TRANSPORT_MEDIA,
199 TRANSPORT_PERSISTENT,
200 TRANSPORT_GENERIC,
201 TRANSPORT_NOTIFY_ENTITY,
202]
204CONF_TARGET_USAGE = "target_usage"
205TARGET_USE_ON_NO_DELIVERY_TARGETS = "no_delivery"
206TARGET_USE_ON_NO_ACTION_TARGETS = "no_action"
207TARGET_USE_FIXED = "fixed"
208TARGET_USE_MERGE_ALWAYS = "merge_always"
209TARGET_USE_MERGE_ON_DELIVERY_TARGETS = "merge_delivery"
211OPTION_SIMPLIFY_TEXT = "simplify_text"
212OPTION_STRIP_URLS = "strip_urls"
213OPTION_MESSAGE_USAGE = "message_usage"
214OPTION_JPEG = "jpeg_opts"
215OPTION_TARGET_CATEGORIES = "target_categories"
216OPTION_UNIQUE_TARGETS = "unique_targets"
217OPTION_TARGET_INCLUDE_RE = "target_include_re"
218OPTION_CHIME_ALIASES = "chime_aliases"
220RE_DEVICE_ID = r"^[0-9a-f]{32}$"
222TARGET_REQUIRE_ALWAYS = "always"
223TARGET_REQUIRE_NEVER = "never"
224TARGET_REQUIRE_OPTIONAL = "optional"
226SCENARIO_NULL = "NULL"
227SCENARIO_TEMPLATE_ATTRS = ("message_template", "title_template")
229RESERVED_DELIVERY_NAMES = ["ALL"]
230RESERVED_SCENARIO_NAMES = [SCENARIO_NULL]
231RESERVED_DATA_KEYS = [ATTR_DOMAIN, ATTR_SERVICE, "action"]
234CONF_DUPE_CHECK = "dupe_check"
235CONF_DUPE_POLICY = "dupe_policy"
236CONF_TTL = "ttl"
237CONF_SIZE = "size"
238ATTR_DUPE_POLICY_MTSLP = "dupe_policy_message_title_same_or_lower_priority"
239ATTR_DUPE_POLICY_NONE = "dupe_policy_none"
240# TARGET_FIELDS includes entity, device, area, floor, label ids
241TARGET_SCHEMA = vol.Any(dict[str, list[str]], dict[str, str], str, list[str], cv.TARGET_FIELDS)
243DATA_SCHEMA = vol.Schema({vol.NotIn(RESERVED_DATA_KEYS): vol.Any(str, int, bool, float, dict, list)})
244MOBILE_DEVICE_SCHEMA = vol.Schema({
245 vol.Optional(CONF_MANUFACTURER): cv.string,
246 vol.Optional(CONF_MODEL): cv.string,
247 vol.Optional(CONF_MOBILE_APP_ID): cv.string,
248 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
249})
250NOTIFICATION_DUPE_SCHEMA = vol.Schema({
251 vol.Optional(CONF_TTL): cv.positive_int,
252 vol.Optional(CONF_SIZE, default=100): cv.positive_int,
253 vol.Optional(CONF_DUPE_POLICY, default=ATTR_DUPE_POLICY_MTSLP): vol.In([ATTR_DUPE_POLICY_MTSLP, ATTR_DUPE_POLICY_NONE]),
254})
255DELIVERY_CUSTOMIZE_SCHEMA = vol.Schema({
256 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
257 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
258 vol.Optional(CONF_DATA): DATA_SCHEMA,
259})
260LINK_SCHEMA = vol.Schema({
261 vol.Optional(CONF_ID): cv.string,
262 vol.Required(CONF_URL): cv.url,
263 vol.Optional(CONF_ICON): cv.icon,
264 vol.Required(CONF_DESCRIPTION): cv.string,
265 vol.Optional(CONF_NAME): cv.string,
266})
267DELIVERY_CONFIG_SCHEMA = vol.Schema({ # shared by Transport Defaults and Delivery definitions
268 # defaults set in model.DeliveryConfig
269 vol.Optional(CONF_ACTION): cv.service, # previously 'service:'
270 vol.Optional(CONF_DEBUG): cv.boolean,
271 vol.Optional(CONF_OPTIONS): dict, # transport tuning
272 vol.Optional(CONF_DATA): DATA_SCHEMA,
273 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
274 vol.Optional(CONF_TARGET_REQUIRED): vol.Any(
275 cv.boolean, vol.In([TARGET_REQUIRE_ALWAYS, TARGET_REQUIRE_NEVER, TARGET_REQUIRE_OPTIONAL])
276 ),
277 vol.Optional(CONF_TARGET_USAGE): vol.In([
278 TARGET_USE_ON_NO_DELIVERY_TARGETS,
279 TARGET_USE_ON_NO_ACTION_TARGETS,
280 TARGET_USE_MERGE_ON_DELIVERY_TARGETS,
281 TARGET_USE_MERGE_ALWAYS,
282 TARGET_USE_FIXED,
283 ]),
284 vol.Optional(CONF_SELECTION): vol.All(cv.ensure_list, [vol.In(SELECTION_VALUES)]),
285 vol.Optional(CONF_PRIORITY): vol.All(cv.ensure_list, [vol.In(PRIORITY_VALUES)]),
286 vol.Optional(CONF_SELECTION_RANK): vol.In([
287 SelectionRank.ANY,
288 SelectionRank.FIRST,
289 SelectionRank.LAST,
290 ]),
291})
292DELIVERY_SCHEMA = DELIVERY_CONFIG_SCHEMA.extend({
293 vol.Optional(CONF_ALIAS): cv.string,
294 vol.Required(CONF_TRANSPORT): vol.In(TRANSPORT_VALUES),
295 vol.Optional(CONF_TEMPLATE): cv.string,
296 vol.Optional(CONF_MESSAGE): vol.Any(None, cv.string),
297 vol.Optional(CONF_TITLE): vol.Any(None, cv.string),
298 vol.Optional(CONF_ENABLED): cv.boolean,
299 vol.Optional(CONF_OCCUPANCY, default=OCCUPANCY_ALL): vol.In(OCCUPANCY_VALUES),
300 vol.Optional(CONF_CONDITION): cv.CONDITION_SCHEMA,
301})
302TRANSPORT_SCHEMA = vol.Schema({
303 vol.Optional(CONF_ALIAS): cv.string,
304 vol.Optional(CONF_DEVICE_DOMAIN): vol.All(cv.ensure_list, [cv.string]),
305 vol.Optional(CONF_DEVICE_DISCOVERY, default=False): cv.boolean,
306 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
307 vol.Optional(CONF_DELIVERY_DEFAULTS): DELIVERY_CONFIG_SCHEMA,
308})
309RECIPIENT_SCHEMA = vol.Schema({
310 vol.Required(CONF_PERSON): cv.entity_id,
311 vol.Optional(CONF_ALIAS): cv.string,
312 vol.Optional(CONF_EMAIL): cv.string,
313 vol.Optional(CONF_TARGET): TARGET_SCHEMA,
314 vol.Optional(CONF_PHONE_NUMBER): cv.string,
315 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
316 vol.Optional(CONF_MOBILE_DEVICES, default=list): vol.All(cv.ensure_list, [MOBILE_DEVICE_SCHEMA]),
317 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_CUSTOMIZE_SCHEMA},
318})
319CAMERA_SCHEMA = vol.Schema({
320 vol.Required(CONF_CAMERA): cv.entity_id,
321 vol.Optional(CONF_ALT_CAMERA): vol.All(cv.ensure_list, [cv.entity_id]),
322 vol.Optional(CONF_ALIAS): cv.string,
323 vol.Optional(CONF_URL): cv.url,
324 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
325 vol.Optional(CONF_PTZ_PRESET_DEFAULT, default=1): vol.Any(cv.positive_int, cv.string),
326 vol.Optional(CONF_PTZ_DELAY, default=0): int,
327 vol.Optional(CONF_PTZ_METHOD, default=PTZ_METHOD_ONVIF): vol.In(PTZ_METHOD_VALUES),
328})
329MEDIA_SCHEMA = vol.Schema({
330 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id,
331 vol.Optional(ATTR_MEDIA_CAMERA_DELAY, default=0): int,
332 vol.Optional(ATTR_MEDIA_CAMERA_PTZ_PRESET): vol.Any(cv.positive_int, cv.string),
333 # URL fragments allowed
334 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string),
335 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string),
336 vol.Optional(ATTR_JPEG_OPTS): dict,
337})
340SCENARIO_SCHEMA = vol.Schema({
341 vol.Optional(CONF_ALIAS): cv.string,
342 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
343 vol.Optional(CONF_CONDITION): cv.CONDITION_SCHEMA,
344 vol.Optional(CONF_MEDIA): MEDIA_SCHEMA,
345 vol.Optional(CONF_ACTION_GROUP_NAMES, default=[]): vol.All(cv.ensure_list, [cv.string]),
346 vol.Optional(CONF_DELIVERY_SELECTION, default=DELIVERY_SELECTION_IMPLICIT): vol.In([
347 DELIVERY_SELECTION_IMPLICIT,
348 DELIVERY_SELECTION_EXPLICIT,
349 ]),
350 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)},
351})
352ACTION_CALL_SCHEMA = vol.Schema(
353 {
354 vol.Optional(ATTR_ACTION): cv.string,
355 vol.Optional(ATTR_TITLE): cv.string,
356 vol.Optional(ATTR_ACTION_CATEGORY): cv.string,
357 vol.Optional(ATTR_ACTION_URL): cv.url,
358 vol.Optional(ATTR_ACTION_URL_TITLE): cv.string,
359 },
360 extra=vol.ALLOW_EXTRA,
361)
362ACTION_SCHEMA = vol.Schema(
363 {
364 vol.Exclusive(CONF_ACTION, CONF_ACTION_TEMPLATE): cv.string,
365 vol.Exclusive(CONF_TITLE, CONF_TITLE_TEMPLATE): cv.string,
366 vol.Optional(CONF_URI): cv.url,
367 vol.Optional(CONF_ICON): cv.string,
368 },
369 extra=vol.ALLOW_EXTRA,
370)
373ARCHIVE_SCHEMA = vol.Schema({
374 vol.Optional(CONF_ARCHIVE_PATH): cv.path,
375 vol.Optional(CONF_ENABLED, default=False): cv.boolean,
376 vol.Optional(CONF_ARCHIVE_DAYS, default=3): cv.positive_int,
377 vol.Optional(CONF_ARCHIVE_MQTT_TOPIC): cv.string,
378 vol.Optional(CONF_ARCHIVE_MQTT_QOS, default=0): cv.positive_int,
379 vol.Optional(CONF_ARCHIVE_MQTT_RETAIN, default=True): cv.boolean,
380 vol.Optional(CONF_ARCHIVE_PURGE_INTERVAL, default=60): cv.positive_int,
381 vol.Optional(CONF_DEBUG, default=False): cv.boolean,
382})
384HOUSEKEEPING_SCHEMA = vol.Schema({
385 vol.Optional(CONF_HOUSEKEEPING_TIME, default="00:00:01"): cv.time,
386})
388PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
389 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path,
390 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path,
391 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA,
392 vol.Optional(CONF_HOUSEKEEPING, default={}): HOUSEKEEPING_SCHEMA,
393 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA,
394 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_SCHEMA},
395 vol.Optional(CONF_ACTION_GROUPS, default=dict): {cv.string: [ACTION_SCHEMA]},
396 vol.Optional(CONF_RECIPIENTS, default=list): vol.All(cv.ensure_list, [RECIPIENT_SCHEMA]),
397 vol.Optional(CONF_LINKS, default=list): vol.All(cv.ensure_list, [LINK_SCHEMA]),
398 vol.Optional(CONF_SCENARIOS, default=dict): {cv.string: SCENARIO_SCHEMA},
399 vol.Optional(CONF_TRANSPORTS, default=dict): {cv.string: TRANSPORT_SCHEMA},
400 vol.Optional(CONF_CAMERAS, default=list): vol.All(cv.ensure_list, [CAMERA_SCHEMA]),
401})
402SUPERNOTIFY_SCHEMA = PLATFORM_SCHEMA
404ACTION_DATA_SCHEMA = vol.Schema(
405 {
406 vol.Optional(ATTR_DELIVERY): vol.Any(cv.string, [cv.string], {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}),
407 vol.Optional(ATTR_PRIORITY): vol.In(PRIORITY_VALUES),
408 vol.Optional(ATTR_SCENARIOS_REQUIRE): vol.All(cv.ensure_list, [cv.string]),
409 vol.Optional(ATTR_SCENARIOS_APPLY): vol.All(cv.ensure_list, [cv.string]),
410 vol.Optional(ATTR_SCENARIOS_CONSTRAIN): vol.All(cv.ensure_list, [cv.string]),
411 vol.Optional(ATTR_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
412 vol.Optional(ATTR_RECIPIENTS): vol.All(cv.ensure_list, [cv.entity_id]),
413 vol.Optional(ATTR_MEDIA): MEDIA_SCHEMA,
414 vol.Optional(ATTR_MESSAGE_HTML): cv.string,
415 vol.Optional(ATTR_ACTION_GROUPS, default=[]): vol.All(cv.ensure_list, [cv.string]),
416 vol.Optional(ATTR_ACTIONS, default=[]): vol.All(cv.ensure_list, [ACTION_CALL_SCHEMA]),
417 vol.Optional(ATTR_DEBUG, default=False): cv.boolean,
418 vol.Optional(ATTR_DATA): vol.Any(None, DATA_SCHEMA),
419 vol.Optional(ATTR_TIMESTAMP): cv.string,
420 },
421 extra=vol.ALLOW_EXTRA, # allow other data, e.g. the android/ios mobile push
422)
424STRICT_ACTION_DATA_SCHEMA = ACTION_DATA_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA)