Coverage for custom_components/supernotify/envelope.py: 97%
74 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# mypy: disable-error-code="name-defined"
3import copy
4import logging
5import time
6import typing
7from pathlib import Path
8from typing import Any
10from . import ATTR_TIMESTAMP, CONF_MESSAGE, CONF_TITLE, PRIORITY_MEDIUM
11from .media_grab import grab_image
12from .model import Target
14if typing.TYPE_CHECKING:
15 from custom_components.supernotify.common import CallRecord
17 from .delivery import Delivery
18 from .notification import Notification
20_LOGGER = logging.getLogger(__name__)
23class Envelope:
24 """Wrap a notification with a specific set of targets and service data possibly customized for those targets"""
26 def __init__(
27 self,
28 delivery: "Delivery",
29 notification: "Notification | None" = None,
30 target: Target | None = None, # targets only for this delivery
31 data: dict[str, Any] | None = None, # notification data customized for this delivery
32 ) -> None:
33 self.target: Target = target or Target()
34 self.delivery_name: str = delivery.name
35 self.delivery: Delivery = delivery
36 self._notification = notification
37 self.notification_id = None
38 self.media = None
39 self.action_groups = None
40 self.priority = PRIORITY_MEDIUM
41 self.message: str | None = None
42 self.title: str | None = None
43 self.message_html: str | None = None
44 self.data: dict[str, Any] = {}
45 self.actions: list[dict[str, Any]] = []
46 delivery_config_data: dict[str, Any] = {}
47 if notification:
48 self.notification_id = notification.id
49 self.media = notification.media
50 self.action_groups = notification.action_groups
51 self.actions = notification.actions
52 self.priority = notification.priority
53 self.message = notification.message(delivery.name)
54 self.message_html = notification.message_html
55 self.title = notification.title(delivery.name)
56 delivery_config_data = notification.delivery_data(delivery.name)
58 if data:
59 self.data = copy.deepcopy(delivery_config_data) if delivery_config_data else {}
60 self.data |= data
61 else:
62 self.data = delivery_config_data if delivery_config_data else {}
64 self.delivered: int = 0
65 self.errored: int = 0
66 self.skipped: int = 0
67 self.calls: list[CallRecord] = []
68 self.failed_calls: list[CallRecord] = []
69 self.delivery_error: list[str] | None = None
71 async def grab_image(self) -> Path | None:
72 """Grab an image from a camera, snapshot URL, MQTT Image etc"""
73 image_path: Path | None = None
74 if self._notification:
75 image_path = await grab_image(self._notification, self.delivery_name, self._notification.context)
76 return image_path
78 def core_action_data(self) -> dict[str, Any]:
79 """Build the core set of `service_data` dict to pass to underlying notify service"""
80 data: dict[str, Any] = {}
81 # message is mandatory for notify platform
82 data[CONF_MESSAGE] = self.message or ""
83 timestamp = self.data.get(ATTR_TIMESTAMP)
84 if timestamp:
85 data[CONF_MESSAGE] = f"{data[CONF_MESSAGE]} [{time.strftime(timestamp, time.localtime())}]"
86 if self.title:
87 data[CONF_TITLE] = self.title
88 return data
90 def contents(self, minimal: bool = True) -> dict[str, typing.Any]:
91 exclude_attrs = ["_notification"]
92 if minimal:
93 exclude_attrs.extend("resolved")
94 json_ready = {k: v for k, v in self.__dict__.items() if k not in exclude_attrs}
95 json_ready["calls"] = [call.contents() for call in self.calls]
96 json_ready["failedcalls"] = [call.contents() for call in self.failed_calls]
97 return json_ready
99 def __eq__(self, other: Any | None) -> bool:
100 """Specialized equality check for subset of attributes"""
101 if other is None or not isinstance(other, Envelope):
102 return False
103 return bool(
104 self.target == other.target
105 and self.delivery_name == other.delivery_name
106 and self.data == other.data
107 and self.notification_id == other.notification_id
108 )
110 def __repr__(self) -> str:
111 """Return a concise string representation of the Envelope.
113 The returned string includes the envelope's message, title, and delivery name
114 in the form: Envelope(message=<message>,title=<title>,delivery=<delivery_name>).
116 Primarily intended for debugging and logging; note that attribute values are
117 inserted directly and may not be quoted or escaped.
118 """
119 return f"Envelope(message={self.message},title={self.title},delivery={self.delivery_name})"