Coverage for custom_components/supernotify/__init__.py: 99%
206 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-18 09:29 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-18 09:29 +0000
1"""The SuperNotification integration"""
3from dataclasses import dataclass, field
4from enum import StrEnum
5from typing import Any
7import voluptuous as vol
8from homeassistant.components.notify import PLATFORM_SCHEMA
9from homeassistant.const import (
10 ATTR_DOMAIN,
11 ATTR_SERVICE,
12 CONF_ACTION,
13 CONF_ALIAS,
14 CONF_CONDITION,
15 CONF_DEFAULT,
16 CONF_DESCRIPTION,
17 CONF_EMAIL,
18 CONF_ENABLED,
19 CONF_ENTITIES,
20 CONF_ICON,
21 CONF_ID,
22 CONF_NAME,
23 CONF_PLATFORM,
24 CONF_TARGET,
25 CONF_URL,
26 STATE_HOME,
27 STATE_NOT_HOME,
28 Platform,
29)
30from homeassistant.helpers import config_validation as cv
32from custom_components.supernotify.common import format_timestamp as format_timestamp
34DOMAIN = "supernotify"
36PLATFORMS = [Platform.NOTIFY]
37TEMPLATE_DIR = "/config/templates/supernotify"
38MEDIA_DIR = "supernotify/media"
40CONF_ACTIONS = "actions"
41CONF_TITLE = "title"
42CONF_URI = "uri"
43CONF_RECIPIENTS = "recipients"
44CONF_TEMPLATE_PATH = "template_path"
45CONF_MEDIA_PATH = "media_path"
46CONF_HOUSEKEEPING = "housekeeping"
47CONF_HOUSEKEEPING_TIME = "housekeeping_time"
48CONF_ARCHIVE_PATH = "archive_path"
49CONF_ARCHIVE = "archive"
50CONF_ARCHIVE_DAYS = "archive_days"
51CONF_ARCHIVE_MQTT_TOPIC = "archive_mqtt_topic"
52CONF_ARCHIVE_MQTT_QOS = "archive_mqtt_qos"
53CONF_ARCHIVE_MQTT_RETAIN = "archive_mqtt_retain"
54CONF_TEMPLATE = "template"
55CONF_LINKS = "links"
56CONF_PERSON = "person"
57CONF_METHOD = "method"
58CONF_METHODS = "methods"
59CONF_DELIVERY = "delivery"
60CONF_SELECTION = "selection"
62CONF_DATA: str = "data"
63CONF_OPTIONS: str = "options"
64CONF_MOBILE: str = "mobile"
65CONF_NOTIFY: str = "notify"
66CONF_NOTIFY_ACTION: str = "notify_action"
67CONF_PHONE_NUMBER: str = "phone_number"
68CONF_PRIORITY: str = "priority"
69CONF_OCCUPANCY: str = "occupancy"
70CONF_SCENARIOS: str = "scenarios"
71CONF_MANUFACTURER: str = "manufacturer"
72CONF_DEVICE_TRACKER: str = "device_tracker"
73CONF_DEVICE_NAME: str = "device_name"
74CONF_DEVICE_LABELS: str = "device_labels"
75CONF_MODEL: str = "model"
76CONF_MESSAGE: str = "message"
77CONF_TARGETS_REQUIRED: str = "targets_required"
78CONF_MOBILE_DEVICES: str = "mobile_devices"
79CONF_MOBILE_DISCOVERY: str = "mobile_discovery"
80CONF_ACTION_TEMPLATE: str = "action_template"
81CONF_ACTION_GROUPS: str = "action_groups"
82CONF_TITLE_TEMPLATE: str = "title_template"
83CONF_DELIVERY_SELECTION: str = "delivery_selection"
84CONF_MEDIA: str = "media"
85CONF_CAMERA: str = "camera"
86CONF_CLIP_URL: str = "clip_url"
87CONF_SNAPSHOT_URL: str = "snapshot_url"
88CONF_PTZ_DELAY: str = "ptz_delay"
89CONF_PTZ_METHOD: str = "ptz_method"
90CONF_PTZ_PRESET_DEFAULT: str = "ptz_default_preset"
91CONF_ALT_CAMERA: str = "alt_camera"
92CONF_CAMERAS: str = "cameras"
94OCCUPANCY_ANY_IN = "any_in"
95OCCUPANCY_ANY_OUT = "any_out"
96OCCUPANCY_ALL_IN = "all_in"
97OCCUPANCY_ALL = "all"
98OCCUPANCY_NONE = "none"
99OCCUPANCY_ALL_OUT = "all_out"
100OCCUPANCY_ONLY_IN = "only_in"
101OCCUPANCY_ONLY_OUT = "only_out"
103ATTR_PRIORITY = "priority"
104ATTR_ACTION = "action"
105ATTR_SCENARIOS_REQUIRE = "require_scenarios"
106ATTR_SCENARIOS_APPLY = "apply_scenarios"
107ATTR_SCENARIOS_CONSTRAIN = "constrain_scenarios"
108ATTR_DELIVERY = "delivery"
109ATTR_DEFAULT = "default"
110ATTR_NOTIFICATION_ID = "notification_id"
111ATTR_DELIVERY_SELECTION = "delivery_selection"
112ATTR_RECIPIENTS = "recipients"
113ATTR_DATA = "data"
114ATTR_MEDIA = "media"
115ATTR_TITLE = "title"
116ATTR_MEDIA_SNAPSHOT_URL = "snapshot_url"
117ATTR_MEDIA_CAMERA_ENTITY_ID = "camera_entity_id"
118ATTR_MEDIA_CAMERA_DELAY = "camera_delay"
119ATTR_MEDIA_CAMERA_PTZ_PRESET = "camera_ptz_preset"
120ATTR_MEDIA_CLIP_URL = "clip_url"
121ATTR_ACTION_GROUPS = "action_groups"
122CONF_ACTION_GROUP_NAMES = "action_groups"
123ATTR_ACTION_CATEGORY = "action_category"
124ATTR_ACTION_URL = "action_url"
125ATTR_ACTION_URL_TITLE = "action_url_title"
126ATTR_MESSAGE_HTML = "message_html"
127ATTR_JPEG_FLAGS = "jpeg_flags"
128ATTR_TIMESTAMP = "timestamp"
129ATTR_DEBUG = "debug"
130ATTR_ACTIONS = "actions"
131ATTR_USER_ID = "user_id"
133DELIVERY_SELECTION_IMPLICIT = "implicit"
134DELIVERY_SELECTION_EXPLICIT = "explicit"
135DELIVERY_SELECTION_FIXED = "fixed"
137DELIVERY_SELECTION_VALUES = [DELIVERY_SELECTION_EXPLICIT, DELIVERY_SELECTION_FIXED, DELIVERY_SELECTION_IMPLICIT]
138PTZ_METHOD_ONVIF = "onvif"
139PTZ_METHOD_FRIGATE = "frigate"
140PTZ_METHOD_VALUES = [PTZ_METHOD_ONVIF, PTZ_METHOD_FRIGATE]
142SELECTION_FALLBACK_ON_ERROR = "fallback_on_error"
143SELECTION_FALLBACK = "fallback"
144SELECTION_BY_SCENARIO = "scenario"
145SELECTION_DEFAULT = "default"
146SELECTION_VALUES = [SELECTION_FALLBACK_ON_ERROR, SELECTION_BY_SCENARIO, SELECTION_DEFAULT, SELECTION_FALLBACK]
148OCCUPANCY_VALUES = [
149 OCCUPANCY_ALL_IN,
150 OCCUPANCY_ALL_OUT,
151 OCCUPANCY_ANY_IN,
152 OCCUPANCY_ANY_OUT,
153 OCCUPANCY_ONLY_IN,
154 OCCUPANCY_ONLY_OUT,
155 OCCUPANCY_ALL,
156 OCCUPANCY_NONE,
157]
159PRIORITY_CRITICAL = "critical"
160PRIORITY_HIGH = "high"
161PRIORITY_MEDIUM = "medium"
162PRIORITY_LOW = "low"
164PRIORITY_VALUES = [PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH, PRIORITY_CRITICAL]
165METHOD_SMS = "sms"
166METHOD_EMAIL = "email"
167METHOD_ALEXA = "alexa"
168METHOD_ALEXA_MEDIA_PLAYER = "alexa_media_player"
169METHOD_MOBILE_PUSH = "mobile_push"
170METHOD_MEDIA = "media"
171METHOD_CHIME = "chime"
172METHOD_GENERIC = "generic"
173METHOD_PERSISTENT = "persistent"
174METHOD_VALUES = [
175 METHOD_SMS,
176 METHOD_ALEXA,
177 METHOD_ALEXA_MEDIA_PLAYER,
178 METHOD_MOBILE_PUSH,
179 METHOD_CHIME,
180 METHOD_EMAIL,
181 METHOD_MEDIA,
182 METHOD_PERSISTENT,
183 METHOD_GENERIC,
184]
186SCENARIO_DEFAULT = "DEFAULT"
187SCENARIO_NULL = "NULL"
188SCENARIO_TEMPLATE_ATTRS = ("message_template", "title_template")
190RESERVED_DELIVERY_NAMES = ["ALL"]
191RESERVED_SCENARIO_NAMES = [SCENARIO_DEFAULT, SCENARIO_NULL]
192RESERVED_DATA_KEYS = [ATTR_DOMAIN, ATTR_SERVICE, "action"]
194CONF_DUPE_CHECK = "dupe_check"
195CONF_DUPE_POLICY = "dupe_policy"
196CONF_TTL = "ttl"
197CONF_SIZE = "size"
198ATTR_DUPE_POLICY_MTSLP = "dupe_policy_message_title_same_or_lower_priority"
199ATTR_DUPE_POLICY_NONE = "dupe_policy_none"
201DATA_SCHEMA = vol.Schema({vol.NotIn(RESERVED_DATA_KEYS): vol.Any(str, int, bool, float, dict, list)})
202MOBILE_DEVICE_SCHEMA = vol.Schema({
203 vol.Optional(CONF_MANUFACTURER): cv.string,
204 vol.Optional(CONF_MODEL): cv.string,
205 vol.Optional(CONF_NOTIFY_ACTION): cv.string,
206 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
207})
208NOTIFICATION_DUPE_SCHEMA = vol.Schema({
209 vol.Optional(CONF_TTL): cv.positive_int,
210 vol.Optional(CONF_SIZE, default=100): cv.positive_int,
211 vol.Optional(CONF_DUPE_POLICY, default=ATTR_DUPE_POLICY_MTSLP): vol.In([ATTR_DUPE_POLICY_MTSLP, ATTR_DUPE_POLICY_NONE]),
212})
213DELIVERY_CUSTOMIZE_SCHEMA = vol.Schema({
214 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
215 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
216 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
217 vol.Optional(CONF_DATA): DATA_SCHEMA,
218})
219LINK_SCHEMA = vol.Schema({
220 vol.Optional(CONF_ID): cv.string,
221 vol.Required(CONF_URL): cv.url,
222 vol.Optional(CONF_ICON): cv.icon,
223 vol.Required(CONF_DESCRIPTION): cv.string,
224 vol.Optional(CONF_NAME): cv.string,
225})
226METHOD_DEFAULTS_SCHEMA = vol.Schema({
227 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
228 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
229 vol.Optional(CONF_ACTION): cv.service,
230 vol.Optional(CONF_TARGETS_REQUIRED): cv.boolean,
231 vol.Optional(CONF_OPTIONS, default=dict): dict,
232 vol.Optional(CONF_DATA): DATA_SCHEMA,
233})
234RECIPIENT_SCHEMA = vol.Schema({
235 vol.Required(CONF_PERSON): cv.entity_id,
236 vol.Optional(CONF_ALIAS): cv.string,
237 vol.Optional(CONF_EMAIL): cv.string,
238 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
239 vol.Optional(CONF_PHONE_NUMBER): cv.string,
240 vol.Optional(CONF_MOBILE_DISCOVERY, default=True): cv.boolean,
241 vol.Optional(CONF_MOBILE_DEVICES, default=list): vol.All(cv.ensure_list, [MOBILE_DEVICE_SCHEMA]),
242 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_CUSTOMIZE_SCHEMA},
243})
244CAMERA_SCHEMA = vol.Schema({
245 vol.Required(CONF_CAMERA): cv.entity_id,
246 vol.Optional(CONF_ALT_CAMERA): vol.All(cv.ensure_list, [cv.entity_id]),
247 vol.Optional(CONF_ALIAS): cv.string,
248 vol.Optional(CONF_URL): cv.url,
249 vol.Optional(CONF_DEVICE_TRACKER): cv.entity_id,
250 vol.Optional(CONF_PTZ_PRESET_DEFAULT, default=1): vol.Any(cv.positive_int, cv.string),
251 vol.Optional(CONF_PTZ_DELAY, default=0): int,
252 vol.Optional(CONF_PTZ_METHOD, default=PTZ_METHOD_ONVIF): vol.In(PTZ_METHOD_VALUES),
253})
254MEDIA_SCHEMA = vol.Schema({
255 vol.Optional(ATTR_MEDIA_CAMERA_ENTITY_ID): cv.entity_id,
256 vol.Optional(ATTR_MEDIA_CAMERA_DELAY, default=0): int,
257 vol.Optional(ATTR_MEDIA_CAMERA_PTZ_PRESET): vol.Any(cv.positive_int, cv.string),
258 # URL fragments allowed
259 vol.Optional(ATTR_MEDIA_CLIP_URL): vol.Any(cv.url, cv.string),
260 vol.Optional(ATTR_MEDIA_SNAPSHOT_URL): vol.Any(cv.url, cv.string),
261 vol.Optional(ATTR_JPEG_FLAGS): dict,
262})
263DELIVERY_SCHEMA = vol.Schema({
264 vol.Optional(CONF_ALIAS): cv.string,
265 vol.Required(CONF_METHOD): vol.In(METHOD_VALUES),
266 vol.Optional(CONF_ACTION): cv.service, # previously 'service:'
267 vol.Optional(CONF_PLATFORM): cv.string,
268 vol.Optional(CONF_TEMPLATE): cv.string,
269 vol.Optional(CONF_DEFAULT, default=False): cv.boolean,
270 vol.Optional(CONF_SELECTION, default=[SELECTION_DEFAULT]): vol.All(cv.ensure_list, [vol.In(SELECTION_VALUES)]),
271 vol.Optional(CONF_TARGET): vol.All(cv.ensure_list, [cv.string]),
272 vol.Optional(CONF_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
273 vol.Optional(CONF_MESSAGE): vol.Any(None, cv.string),
274 vol.Optional(CONF_TITLE): vol.Any(None, cv.string),
275 vol.Optional(CONF_DATA): DATA_SCHEMA,
276 vol.Optional(CONF_ENABLED, default=True): cv.boolean,
277 vol.Optional(CONF_OPTIONS, default=dict): dict,
278 vol.Optional(CONF_PRIORITY, default=PRIORITY_VALUES): vol.All(cv.ensure_list, [vol.In(PRIORITY_VALUES)]),
279 vol.Optional(CONF_OCCUPANCY, default=OCCUPANCY_ALL): vol.In(OCCUPANCY_VALUES),
280 vol.Optional(CONF_CONDITION): cv.CONDITION_SCHEMA,
281})
283SCENARIO_SCHEMA = vol.Schema({
284 vol.Optional(CONF_ALIAS): cv.string,
285 vol.Optional(CONF_CONDITION): cv.CONDITION_SCHEMA,
286 vol.Optional(CONF_MEDIA): MEDIA_SCHEMA,
287 vol.Optional(CONF_ACTION_GROUP_NAMES, default=[]): vol.All(cv.ensure_list, [cv.string]),
288 vol.Optional(CONF_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
289 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)},
290})
291ACTION_CALL_SCHEMA = vol.Schema(
292 {
293 vol.Optional(ATTR_ACTION): cv.string,
294 vol.Optional(ATTR_TITLE): cv.string,
295 vol.Optional(ATTR_ACTION_CATEGORY): cv.string,
296 vol.Optional(ATTR_ACTION_URL): cv.url,
297 vol.Optional(ATTR_ACTION_URL_TITLE): cv.string,
298 },
299 extra=vol.ALLOW_EXTRA,
300)
301ACTION_SCHEMA = vol.Schema(
302 {
303 vol.Exclusive(CONF_ACTION, CONF_ACTION_TEMPLATE): cv.string,
304 vol.Exclusive(CONF_TITLE, CONF_TITLE_TEMPLATE): cv.string,
305 vol.Optional(CONF_URI): cv.url,
306 vol.Optional(CONF_ICON): cv.string,
307 },
308 extra=vol.ALLOW_EXTRA,
309)
312ARCHIVE_SCHEMA = vol.Schema({
313 vol.Optional(CONF_ARCHIVE_PATH): cv.path,
314 vol.Optional(CONF_ENABLED, default=False): cv.boolean,
315 vol.Optional(CONF_ARCHIVE_DAYS, default=3): cv.positive_int,
316 vol.Optional(CONF_ARCHIVE_MQTT_TOPIC): cv.string,
317 vol.Optional(CONF_ARCHIVE_MQTT_QOS, default=0): cv.positive_int,
318 vol.Optional(CONF_ARCHIVE_MQTT_RETAIN, default=True): cv.boolean,
319})
321HOUSEKEEPING_SCHEMA = vol.Schema({
322 vol.Optional(CONF_HOUSEKEEPING_TIME, default="00:00:01"): cv.time,
323})
325PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
326 vol.Optional(CONF_TEMPLATE_PATH, default=TEMPLATE_DIR): cv.path,
327 vol.Optional(CONF_MEDIA_PATH, default=MEDIA_DIR): cv.path,
328 vol.Optional(CONF_ARCHIVE, default={CONF_ENABLED: False}): ARCHIVE_SCHEMA,
329 vol.Optional(CONF_HOUSEKEEPING, default={}): HOUSEKEEPING_SCHEMA,
330 vol.Optional(CONF_DUPE_CHECK, default=dict): NOTIFICATION_DUPE_SCHEMA,
331 vol.Optional(CONF_DELIVERY, default=dict): {cv.string: DELIVERY_SCHEMA},
332 vol.Optional(CONF_ACTION_GROUPS, default=dict): {cv.string: [ACTION_SCHEMA]},
333 vol.Optional(CONF_RECIPIENTS, default=list): vol.All(cv.ensure_list, [RECIPIENT_SCHEMA]),
334 vol.Optional(CONF_LINKS, default=list): vol.All(cv.ensure_list, [LINK_SCHEMA]),
335 vol.Optional(CONF_SCENARIOS, default=dict): {cv.string: SCENARIO_SCHEMA},
336 vol.Optional(CONF_METHODS, default=dict): {cv.string: METHOD_DEFAULTS_SCHEMA},
337 vol.Optional(CONF_CAMERAS, default=list): vol.All(cv.ensure_list, [CAMERA_SCHEMA]),
338})
339SUPERNOTIFY_SCHEMA = PLATFORM_SCHEMA
341ACTION_DATA_SCHEMA = vol.Schema(
342 {
343 vol.Optional(ATTR_DELIVERY): vol.Any(cv.string, [cv.string], {cv.string: vol.Any(None, DELIVERY_CUSTOMIZE_SCHEMA)}),
344 vol.Optional(ATTR_PRIORITY): vol.In(PRIORITY_VALUES),
345 vol.Optional(ATTR_SCENARIOS_REQUIRE): vol.All(cv.ensure_list, [cv.string]),
346 vol.Optional(ATTR_SCENARIOS_APPLY): vol.All(cv.ensure_list, [cv.string]),
347 vol.Optional(ATTR_SCENARIOS_CONSTRAIN): vol.All(cv.ensure_list, [cv.string]),
348 vol.Optional(ATTR_DELIVERY_SELECTION): vol.In(DELIVERY_SELECTION_VALUES),
349 vol.Optional(ATTR_RECIPIENTS): vol.All(cv.ensure_list, [cv.entity_id]),
350 vol.Optional(ATTR_MEDIA): MEDIA_SCHEMA,
351 vol.Optional(ATTR_MESSAGE_HTML): cv.string,
352 vol.Optional(ATTR_ACTION_GROUPS, default=[]): vol.All(cv.ensure_list, [cv.string]),
353 vol.Optional(ATTR_ACTIONS, default=[]): vol.All(cv.ensure_list, [ACTION_CALL_SCHEMA]),
354 vol.Optional(ATTR_DEBUG, default=False): cv.boolean,
355 vol.Optional(ATTR_DATA): vol.Any(None, DATA_SCHEMA),
356 },
357 extra=vol.ALLOW_EXTRA, # allow other data, e.g. the android/ios mobile push
358)
360STRICT_ACTION_DATA_SCHEMA = ACTION_DATA_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA)
363class TargetType(StrEnum):
364 pass
367class GlobalTargetType(TargetType):
368 NONCRITICAL = "NONCRITICAL"
369 EVERYTHING = "EVERYTHING"
372class RecipientType(StrEnum):
373 USER = "USER"
374 EVERYONE = "EVERYONE"
377class QualifiedTargetType(TargetType):
378 METHOD = "METHOD"
379 DELIVERY = "DELIVERY"
380 CAMERA = "CAMERA"
381 PRIORITY = "PRIORITY"
382 ACTION = "ACTION"
385class CommandType(StrEnum):
386 SNOOZE = "SNOOZE"
387 SILENCE = "SILENCE"
388 NORMAL = "NORMAL"
391@dataclass
392class ConditionVariables:
393 """Variables presented to all condition evaluations
395 Attributes
396 ----------
397 applied_scenarios (list[str]): Scenarios that have been applied
398 required_scenarios (list[str]): Scenarios that must be applied
399 constrain_scenarios (list[str]): Only scenarios in this list, or in explicit apply_scenarios, can be applied
400 notification_priority (str): Priority of the notification
401 notification_message (str): Message of the notification
402 notification_title (str): Title of the notification
403 occupancy (list[str]): List of occupancy scenarios
405 """
407 applied_scenarios: list[str] = field(default_factory=list)
408 required_scenarios: list[str] = field(default_factory=list)
409 constrain_scenarios: list[str] = field(default_factory=list)
410 notification_priority: str = PRIORITY_MEDIUM
411 notification_message: str = ""
412 notification_title: str = ""
413 occupancy: list[str] = field(default_factory=list)
415 def __init__(
416 self,
417 applied_scenarios: list[str] | None = None,
418 required_scenarios: list[str] | None = None,
419 constrain_scenarios: list[str] | None = None,
420 delivery_priority: str | None = PRIORITY_MEDIUM,
421 occupiers: dict[str, list[dict[str, Any]]] | None = None,
422 message: str | None = None,
423 title: str | None = None,
424 ) -> None:
425 occupiers = occupiers or {}
426 self.occupancy = []
427 if not occupiers.get(STATE_NOT_HOME) and occupiers.get(STATE_HOME):
428 self.occupancy.append("ALL_HOME")
429 elif occupiers.get(STATE_NOT_HOME) and not occupiers.get(STATE_HOME):
430 self.occupancy.append("ALL_AWAY")
431 if len(occupiers.get(STATE_HOME, [])) == 1:
432 self.occupancy.extend(["LONE_HOME", "SOME_HOME"])
433 elif len(occupiers.get(STATE_HOME, [])) > 1 and occupiers.get(STATE_NOT_HOME):
434 self.occupancy.extend(["MULTI_HOME", "SOME_HOME"])
435 self.applied_scenarios = applied_scenarios or []
436 self.required_scenarios = required_scenarios or []
437 self.constrain_scenarios = constrain_scenarios or []
438 self.notification_priority = delivery_priority or PRIORITY_MEDIUM
439 self.notification_message = message or ""
440 self.notification_title = title or ""
442 def as_dict(self) -> dict[str, Any]:
443 return {
444 "applied_scenarios": self.applied_scenarios,
445 "required_scenarios": self.required_scenarios,
446 "constrain_scenarios": self.constrain_scenarios,
447 "notification_message": self.notification_message,
448 "notification_title": self.notification_title,
449 "occupancy": self.occupancy,
450 }