Coverage for custom_components/supernotify/const.py: 100%
255 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-08 17:42 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-08 17:42 +0000
1"""The Supernotify integration"""
3from typing import Final
5from homeassistant.const import (
6 ATTR_DEVICE_ID,
7 ATTR_DOMAIN,
8 ATTR_ENTITY_ID,
9 ATTR_SERVICE,
10 CONF_TARGET,
11)
13# fired on the notification's context once it has been sent, so the logbook shows it against what caused it
14EVENT_NOTIFICATION: Final[str] = "supernotify_notification"
16CONF_ACTIONS: Final[str] = "actions" # not fully implemented
17CONF_TITLE: Final[str] = "title"
18CONF_URI: Final[str] = "uri"
19CONF_RECIPIENTS: Final[str] = "recipients"
20CONF_RECIPIENTS_DISCOVERY: Final[str] = "recipients_discovery"
21# config subentry type holding one recipient, in RECIPIENT_SCHEMA shape
22SUBENTRY_TYPE_RECIPIENT: Final[str] = "recipient"
23SUBENTRY_TYPE_TRANSPORT: Final[str] = "transport"
24CONF_TEMPLATE_PATH: Final[str] = "template_path"
25CONF_MEDIA_PATH: Final[str] = "media_path"
26CONF_MEDIA_URL_PREFIX: Final[str] = "media_url_prefix"
27CONF_HOUSEKEEPING: Final[str] = "housekeeping"
28CONF_HOUSEKEEPING_TIME: Final[str] = "housekeeping_time"
29# keep the deprecated recipient, delivery and transport binary_sensors an install already has
30CONF_DEPRECATED_ENTITIES: Final[str] = "deprecated_entities"
31CONF_ARCHIVE_PATH: Final[str] = "file_path"
32CONF_ARCHIVE: Final[str] = "archive"
33CONF_ARCHIVE_DAYS: Final[str] = "file_retention_days"
34CONF_ARCHIVE_MQTT_TOPIC: Final[str] = "mqtt_topic"
35CONF_ARCHIVE_MQTT_QOS: Final[str] = "mqtt_qos"
36CONF_ARCHIVE_MQTT_RETAIN: Final[str] = "mqtt_retain"
37CONF_TEMPLATE: Final[str] = "template"
38CONF_DELIVERY_DEFAULTS: Final[str] = "delivery_defaults"
39CONF_PERSON: Final[str] = "person"
40CONF_USER_ID: Final[str] = "user_id"
41CONF_TRANSPORT: Final[str] = "transport"
42CONF_TRANSPORTS: Final[str] = "transports"
43CONF_LOAD: Final[str] = "load"
44CONF_DELIVERY: Final[str] = "delivery"
45CONF_INCLUSION: Final[str] = "inclusion"
46CONF_SELECTION: Final[str] = "selection" # deprecated, use CONF_INCLUSION
47CONF_SELECTION_RANK: Final[str] = "selection_rank"
50CONF_DATA: Final[str] = "data"
51CONF_OPTIONS: Final[str] = "options"
52CONF_MOBILE: Final[str] = "mobile"
53CONF_NOTIFY: Final[str] = "notify"
55CONF_PRIORITY: Final[str] = "priority"
56CONF_OCCUPANCY: Final[str] = "occupancy"
57CONF_SCENARIOS: Final[str] = "scenarios"
58CONF_SCENARIO_CONTROL: Final[str] = "scenario_control"
59CONF_REFRESH_INTERVAL: Final[str] = "refresh_interval"
60CONF_EXPOSE_STATE: Final[str] = "expose_state"
61CONF_REFRESH: Final[str] = "refresh"
62SCENARIO_STATE_REFRESH_DEFAULT: Final[int] = 60
63CONF_MANUFACTURER: Final[str] = "manufacturer"
64CONF_CLASS: Final[str] = "class"
67CONF_MODEL: Final[str] = "model"
68CONF_MESSAGE: Final[str] = "message"
69CONF_TARGET_REQUIRED: Final[str] = "target_required"
70CONF_MOBILE_DEVICES: Final[str] = "mobile_devices"
71CONF_MOBILE_DISCOVERY: Final[str] = "mobile_discovery"
72CONF_ACTION_TEMPLATE: Final[str] = "action_template"
73CONF_ACTION_GROUPS: Final[str] = "action_groups"
74CONF_TITLE_TEMPLATE: Final[str] = "title_template"
75CONF_URI_TEMPLATE: Final[str] = "uri_template"
76CONF_LEVEL: Final[str] = "level"
77# a mobile action is a button by default, or at notification level is what tapping the notification itself opens
78ACTION_LEVEL_ACTION: Final[str] = "action"
79ACTION_LEVEL_NOTIFICATION: Final[str] = "notification"
80CONF_MEDIA: Final[str] = "media"
81CONF_CAMERA: Final[str] = "camera"
82CONF_CLIP_URL: Final[str] = "clip_url"
83CONF_PTZ_DELAY: Final[str] = "ptz_delay"
84CONF_SNAP_WAIT: Final[str] = "snap_wait"
85CONF_PTZ_METHOD: Final[str] = "ptz_method"
86CONF_PTZ_CAMERA: Final[str] = "ptz_camera"
87CONF_PTZ_PRESET_DEFAULT: Final[str] = "ptz_default_preset"
88CONF_ALT_CAMERA: Final[str] = "alt_camera"
89CONF_CAMERAS: Final[str] = "cameras"
90CONF_ARCHIVE_PURGE_INTERVAL: Final[str] = "purge_interval"
91CONF_ARCHIVE_EVENT_NAME: Final[str] = "event_name"
92CONF_ARCHIVE_EVENT_SELECTION: Final[str] = "event_selection"
93CONF_ARCHIVE_DIAGNOSTICS: Final[str] = "diagnostics"
94CONF_MEDIA_STORAGE_DAYS: Final[str] = "media_storage_days"
96PLATFORM_FRIGATE = "frigate"
98OCCUPANCY_ANY_IN = "any_in"
99OCCUPANCY_ANY_OUT = "any_out"
100OCCUPANCY_ALL_IN = "all_in"
101OCCUPANCY_ALL = "all"
102OCCUPANCY_NONE = "none"
103OCCUPANCY_ALL_OUT = "all_out"
104OCCUPANCY_ONLY_IN = "only_in"
105OCCUPANCY_ONLY_OUT = "only_out"
107ATTR_ENABLED = "enabled"
108ATTR_OVERRIDDEN = "overridden"
109# on a delivery switch: when the delivery last sent, and how the latest live notification went for it
110ATTR_LAST_SENT_AT = "last_sent_at"
111ATTR_LAST_OUTCOME = "last_outcome"
112ATTR_LAST_SKIP_REASON = "last_skip_reason"
113ATTR_TRANSPORT_ENABLED = "transport_enabled"
114ATTR_PRIORITY = "priority"
115ATTR_ACTION = "action"
116ATTR_REPLY_TEXT = "reply_text"
117ATTR_SCENARIOS_REQUIRE = "require_scenarios"
118ATTR_SCENARIOS_APPLY = "apply_scenarios"
119ATTR_FORCE_RESEND: Final[str] = "force_resend"
120# a standard action-data field, same as force_resend/debug - Notification.call_transport() is what
121# actually skips sending for SIMULATE, so nothing else needs to special-case it. An enum, not a
122# boolean, so a future mode (e.g. SIMULATE_PLUS_MEDIA, also simulating the camera/media grab) can
123# be added as a new value rather than forcing a second flag or a breaking type change later
124ATTR_DRY_RUN: Final[str] = "dry_run"
125DRY_RUN_LIVE = "live"
126DRY_RUN_SIMULATE = "simulate"
127DRY_RUN_VALUES = [DRY_RUN_LIVE, DRY_RUN_SIMULATE]
128ATTR_SCENARIOS_CONSTRAIN = "constrain_scenarios"
129ATTR_DELIVERY = "delivery"
130ATTR_DEFAULT = "default"
131ATTR_NOTIFICATION_ID = "notification_id"
132ATTR_DELIVERY_SELECTION = "delivery_selection"
133# supernotify.notify's free-form delivery field, merged into `delivery` with the delivery dropdown
134ATTR_DELIVERY_CONTROL = "delivery_control"
135ATTR_RECIPIENTS = "recipients"
136ATTR_CUSTOM_TARGET = "custom_target"
137ATTR_DATA = "data"
138ATTR_EXTRA_DATA: Final[str] = "extra_data"
139ATTR_MEDIA = "media"
140ATTR_TITLE = "title"
141ATTR_IMAGE = "image"
142ATTR_VIDEO = "video"
143ATTR_MEDIA_SNAPSHOT_URL = "snapshot_url"
144ATTR_MEDIA_CAMERA_ENTITY_ID = "camera_entity_id"
145ATTR_MEDIA_CAMERA_DELAY = "camera_delay"
146ATTR_MEDIA_CAMERA_PTZ_PRESET = "camera_ptz_preset"
147ATTR_MEDIA_CLIP_URL = "clip_url"
148ATTR_MEDIA_SNAPSHOT_PATH = "snapshot_image_path"
149ATTR_TOPIC = "topic"
150ATTR_DISCORD_CHANNEL = "discord_channel"
151ATTR_MATRIX_ROOM = "matrix_room"
152ATTR_TELEGRAM_CHAT_ID = "telegram_chat_id"
153ATTR_ACTION_GROUPS = "action_groups"
154CONF_ACTION_GROUP_NAMES = "action_groups"
155ATTR_ACTION_URL = "action_url"
156ATTR_ACTION_URL_TITLE = "action_url_title"
157ATTR_MESSAGE_HTML = "message_html"
158ATTR_JPEG_OPTS = "jpeg_opts"
159ATTR_PNG_OPTS = "png_opts"
160ATTR_TIMESTAMP = "timestamp"
161ATTR_SPOKEN_MESSAGE = "spoken_message"
162ATTR_DEBUG = "debug"
163ATTR_ACTIONS = "actions"
164ATTR_USER_ID = "user_id"
165ATTR_PERSON_ID = "person_id"
166ATTR_MOBILE_APP_ID = "mobile_app_id"
167ATTR_EMAIL = "email"
168ATTR_PHONE = "phone"
169ATTR_ALIAS = "alias"
171DELIVERY_SELECTION_IMPLICIT = "implicit"
172DELIVERY_SELECTION_EXPLICIT = "explicit"
173DELIVERY_SELECTION_FIXED = "fixed"
175DELIVERY_SELECTION_VALUES = [DELIVERY_SELECTION_EXPLICIT, DELIVERY_SELECTION_FIXED, DELIVERY_SELECTION_IMPLICIT]
176PTZ_METHOD_ONVIF = "onvif"
177PTZ_METHOD_FRIGATE = "frigate"
178PTZ_METHOD_VALUES = [PTZ_METHOD_ONVIF, PTZ_METHOD_FRIGATE]
179PTZ_DELAY_DEFAULT = 10
180SNAP_WAIT_DEFAULT = 15
182INCLUSION_FALLBACK_ON_ERROR = "fallback_on_error"
183INCLUSION_FALLBACK = "fallback"
184# a delivery's own deliveries to try, in order, when it fails
185CONF_FALLBACK = "fallback"
186INCLUSION_BY_SCENARIO = "scenario"
187INCLUSION_DEFAULT = "default"
188INCLUSION_EXPLICIT = "explicit"
189INCLUSION_VALUES = [
190 INCLUSION_FALLBACK_ON_ERROR,
191 INCLUSION_EXPLICIT,
192 INCLUSION_BY_SCENARIO,
193 INCLUSION_DEFAULT,
194 INCLUSION_FALLBACK,
195]
197OCCUPANCY_VALUES = [
198 OCCUPANCY_ALL_IN,
199 OCCUPANCY_ALL_OUT,
200 OCCUPANCY_ANY_IN,
201 OCCUPANCY_ANY_OUT,
202 OCCUPANCY_ONLY_IN,
203 OCCUPANCY_ONLY_OUT,
204 OCCUPANCY_ALL,
205 OCCUPANCY_NONE,
206]
208PRIORITY_CRITICAL = "critical"
209PRIORITY_HIGH = "high"
210PRIORITY_MEDIUM = "medium"
211PRIORITY_LOW = "low"
212PRIORITY_MINIMUM = "minimum"
214# other names for priorities, such as those of Home Assistant's SMTP integration
215PRIORITY_SYNONYMS: dict[str, str] = {
216 "highest": PRIORITY_CRITICAL,
217 "normal": PRIORITY_MEDIUM,
218 "lowest": PRIORITY_MINIMUM,
219}
221PRIORITY_VALUES: dict[str, int] = {
222 PRIORITY_MINIMUM: 1,
223 PRIORITY_LOW: 2,
224 PRIORITY_MEDIUM: 3,
225 PRIORITY_HIGH: 4,
226 PRIORITY_CRITICAL: 5,
227}
229CONF_TARGET_USAGE = "target_usage"
230TARGET_USE_ON_NO_DELIVERY_TARGETS = "no_delivery"
231TARGET_USE_ON_NO_ACTION_TARGETS = "no_action"
232TARGET_USE_FIXED = "fixed"
233TARGET_USE_MERGE_ALWAYS = "merge_always"
234TARGET_USE_MERGE_ON_DELIVERY_TARGETS = "merge_delivery"
235TARGET_USAGE_VALUES = [
236 TARGET_USE_ON_NO_ACTION_TARGETS,
237 TARGET_USE_ON_NO_DELIVERY_TARGETS,
238 TARGET_USE_MERGE_ON_DELIVERY_TARGETS,
239 TARGET_USE_MERGE_ALWAYS,
240 TARGET_USE_FIXED,
241]
243# Options constants have been moved to `options.py`
245RE_DEVICE_ID = r"^[0-9a-f]{32}$"
246RE_MEDIA_PLAYER_ENTITY_ID = r"^media_player\.[A-Za-z0-9_]+$"
247RE_NOTIFY_ENTITY_ID = r"^notify\.[A-Za-z0-9_]+$"
249RESERVED_DELIVERY_NAMES: list[str] = ["ALL"]
250RESERVED_SCENARIO_NAMES: list[str] = ["NO_SCENARIO", "NULL"]
251RESERVED_DATA_KEYS: list[str] = [ATTR_DOMAIN, ATTR_SERVICE, "action"]
253CONF_DUPE_CHECK: Final[str] = "dupe_check"
254CONF_DUPE_POLICY: Final[str] = "dupe_policy"
255CONF_TTL: Final[str] = "ttl"
256CONF_SIZE: Final[str] = "size"
257ATTR_DUPE_POLICY_MTSLP: Final[str] = "dupe_policy_message_title_same_or_lower_priority"
258ATTR_DUPE_POLICY_MT: Final[str] = "dupe_policy_message_title_same"
259ATTR_DUPE_POLICY_NONE: Final[str] = "dupe_policy_none"
260CONF_MOBILE_APP_ID: Final[str] = "mobile_app_id"
261CONF_TRANSPORT_DATA: Final[str] = "transport_data"
264CONF_DEVICE_TRACKER: Final[str] = "device_tracker"
267CONF_DEVICE_NAME: Final[str] = "device_name"
268CONF_DEVICE_LABELS: Final[str] = "device_labels"
270MANUFACTURER_APPLE = "Apple"
272TARGET_REQUIRE_ALWAYS = "always"
273TARGET_REQUIRE_NEVER = "never"
274TARGET_REQUIRE_OPTIONAL = "optional"
276## Transports
277# Defined here rather than transports.py so that the strings can be imported
278# without the transports, and avoid circular references or heavier imports
279TRANSPORT_SMS = "sms"
280TRANSPORT_TTS = "tts"
281TRANSPORT_MQTT = "mqtt"
282TRANSPORT_EMAIL = "email"
283TRANSPORT_ALEXA = "alexa_devices"
284TRANSPORT_ALEXA_MEDIA_PLAYER = "alexa_media_player"
285TRANSPORT_MOBILE_PUSH = "mobile_push"
286TRANSPORT_MEDIA = "media"
287TRANSPORT_CHIME = "chime"
288TRANSPORT_GENERIC = "generic"
289TRANSPORT_NOTIFY_ENTITY = "notify_entity"
290# a scenario or delivery exposed as its own notify.<value> entity, see notify.py
291CONF_NOTIFY_ENTITY = "notify_entity"
292TRANSPORT_PERSISTENT = "persistent"
293TRANSPORT_NTFY = "ntfy"
294TRANSPORT_GOTIFY = "gotify"
295TRANSPORT_TELEGRAM = "telegram"
296TRANSPORT_LAMETRIC = "lametric"
297TRANSPORT_PUSHOVER = "pushover"
298TRANSPORT_HTML5 = "html5"
299TRANSPORT_MATRIX = "matrix"
300TRANSPORT_KODI = "kodi"
301TRANSPORT_DISCORD = "discord"
302TRANSPORT_WEBOSTV = "webostv"
303TRANSPORT_VALUES = [
304 TRANSPORT_SMS,
305 TRANSPORT_MQTT,
306 TRANSPORT_ALEXA,
307 TRANSPORT_ALEXA_MEDIA_PLAYER,
308 TRANSPORT_MOBILE_PUSH,
309 TRANSPORT_CHIME,
310 TRANSPORT_EMAIL,
311 TRANSPORT_MEDIA,
312 TRANSPORT_PERSISTENT,
313 TRANSPORT_TTS,
314 TRANSPORT_GENERIC,
315 TRANSPORT_NOTIFY_ENTITY,
316 TRANSPORT_NTFY,
317 TRANSPORT_GOTIFY,
318 TRANSPORT_TELEGRAM,
319 TRANSPORT_LAMETRIC,
320 TRANSPORT_PUSHOVER,
321 TRANSPORT_HTML5,
322 TRANSPORT_MATRIX,
323 TRANSPORT_KODI,
324 TRANSPORT_DISCORD,
325 TRANSPORT_WEBOSTV,
326]
328# The master list of target category names, independent of any one transport - both
329# `Target` (for qualifying a target, e.g. `topic:my/topic` or `target: {topic: ...}`)
330# and `Transport.target_categories` (for declaring which categories a transport accepts)
331# reference this same list, rather than transports and targets each keeping their own.
332TARGET_CATEGORY_VALUES = [
333 ATTR_ENTITY_ID,
334 ATTR_DEVICE_ID,
335 ATTR_EMAIL,
336 ATTR_PHONE,
337 ATTR_MOBILE_APP_ID,
338 ATTR_TOPIC,
339 ATTR_DISCORD_CHANNEL,
340 ATTR_MATRIX_ROOM,
341 ATTR_TELEGRAM_CHAT_ID,
342]
345CONF_CONNECTION: Final[str] = "connection"
346CONF_ENCRYPTION: Final[str] = "encryption"
348CONF_DEVICE_DISCOVERY: Final[str] = "device_discovery"
349CONF_DEVICE_DOMAIN: Final[str] = "device_domain"
350CONF_DEVICE_MODEL_INCLUDE: Final[str] = "device_model_include"
351CONF_DEVICE_MODEL_EXCLUDE: Final[str] = "device_model_exclude"
353# What a runtime override of a configured `enabled` flag can apply to - each also the unique_id
354# prefix of the switch for it, e.g. switch unique_id "delivery_<name>"
355OVERRIDE_KIND_SCENARIO: Final[str] = "scenario"
356OVERRIDE_KIND_RECIPIENT: Final[str] = "recipient"
357OVERRIDE_KIND_DELIVERY: Final[str] = "delivery"
358OVERRIDE_KIND_TRANSPORT: Final[str] = "transport"
359OVERRIDE_KINDS: Final[tuple[str, ...]] = (
360 OVERRIDE_KIND_SCENARIO,
361 OVERRIDE_KIND_RECIPIENT,
362 OVERRIDE_KIND_DELIVERY,
363 OVERRIDE_KIND_TRANSPORT,
364)
365# Entity state attributes too large, or changing too often, to be worth keeping in history
366DELIVERY_UNRECORDED_ATTRIBUTES: Final[frozenset[str]] = frozenset({
367 CONF_OPTIONS,
368 CONF_DATA,
369 CONF_TARGET,
370 ATTR_LAST_SENT_AT,
371 ATTR_LAST_OUTCOME,
372 ATTR_LAST_SKIP_REASON,
373})
374TRANSPORT_UNRECORDED_ATTRIBUTES: Final[frozenset[str]] = frozenset({
375 CONF_DELIVERY_DEFAULTS,
376 "action_titles",
377 "action_title_failures",
378 "cached_templates",
379})
381CONF_SNOOZE = "snooze"
382CONF_SNOOZE_TIME = "snooze_time"
384# Config entry options for the LLM tools offered to Assist and MCP, see llm.py
385CONF_LLM_TOOLS: Final[str] = "llm_tools"
386CONF_LLM_ACTION_TOOLS: Final[str] = "action_tools"
387CONF_LLM_DIAGNOSTIC_TOOLS: Final[str] = "diagnostic_tools"
388# sentences for Home Assistant's built-in conversation agent, which can't use LLM tools, see sentences.py
389CONF_SENTENCE_COMMANDS: Final[str] = "sentence_commands"
391# Config entry options giving deliveries their defaults, where neither they nor their transport set one
392CONF_DELIVERY_CONTROL: Final[str] = "delivery_control"
393CONF_DEFAULT_INCLUSION: Final[str] = "default_inclusion"
394CONF_VOICE_OCCUPANCY: Final[str] = "voice_occupancy"
395DEFAULT_INCLUSION_VALUES: Final[list[str]] = [INCLUSION_DEFAULT, INCLUSION_EXPLICIT, INCLUSION_BY_SCENARIO]
397# Idea - differentiate enabled as recipient vs as occupant, for ALL_IN etc check
398# May need condition, and also enabled if delivery disabled
399# CONF_OCCUPANCY="occupancy"
400# OPTION_OCCUPANCY_DEFAULT="default"
401# OPTIONS_OCCUPANCY=[OPTION_OCCUPANCY_DEFAULT,OPTION_OCCUPANCY_EXCLUDE]
402# OPTION_OCCUPANCY_EXCLUDE="exclude"
404CONF_PHONE_NUMBER: str = "phone_number"
407CONF_TUNE: Final[str] = "tune"
408CONF_VOLUME: Final[str] = "volume"
409CONF_DURATION: Final[str] = "duration"
411OPTIONS_CHIME_DOMAINS = ["media_player", "switch", "script", "rest_command", "siren", "alexa_devices"]
414# transport names that are acronyms, shown in capitals
415_TRANSPORT_ACRONYMS: Final[dict[str, str]] = {"tts": "TTS", "sms": "SMS", "mqtt": "MQTT", "html5": "HTML5"}
418def transport_display_name(name: str) -> str:
419 """A transport's name to show, such as "Alexa Media Player" for alexa_media_player"""
420 return " ".join(_TRANSPORT_ACRONYMS.get(word, word.capitalize()) for word in name.split("_"))
423# subentry titles are prefixed by their type, to tell them apart in the integration's list of them
424RECIPIENT_TITLE_PREFIX: Final[str] = "Recipient - "
425TRANSPORT_TITLE_PREFIX: Final[str] = "Transport - "
428def recipient_subentry_title(name: str) -> str:
429 """A recipient subentry's title, such as "Recipient - Alice" for alice"""
430 return name if name.startswith(RECIPIENT_TITLE_PREFIX) else f"{RECIPIENT_TITLE_PREFIX}{name}"
433def transport_subentry_title(name: str) -> str:
434 """A transport subentry's title, such as "Transport - Alexa Media Player" for alexa_media_player"""
435 return f"{TRANSPORT_TITLE_PREFIX}{transport_display_name(name)}"