Skip to content

Commit f75c53f

Browse files
committed
Implement notification text templates and update related settings for customizable notifications
1 parent 6f7d2c3 commit f75c53f

File tree

7 files changed

+553
-9
lines changed

7 files changed

+553
-9
lines changed

.github/skills/code-standards/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Use timeNowUTC(as_string=False) for datetime operations (scheduling, comparisons
6464

6565
## String Sanitization
6666

67-
Use sanitizers from `server/helper.py` before storing user input.
67+
Use sanitizers from `server/helper.py` before storing user input. MAC addresses are always lowercased and normalized. IP addresses should be validated.
6868

6969
## Devcontainer Constraints
7070

docs/NOTIFICATIONS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Notifications 📧
22

3+
> [!TIP]
4+
> Want to customize how devices appear in text notifications? See [Notification Text Templates](NOTIFICATION_TEMPLATES.md).
5+
36
There are 4 ways how to influence notifications:
47

58
1. On the device itself

docs/NOTIFICATION_TEMPLATES.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Notification Text Templates
2+
3+
> Customize how devices and events appear in **text** notifications (email previews, push notifications, Apprise messages).
4+
5+
By default, NetAlertX formats each device as a vertical list of `Header: Value` pairs. Text templates let you define a **single-line format per device** using `{FieldName}` placeholders — ideal for mobile notification previews and high-volume alerts.
6+
7+
HTML email tables are **not affected** by these templates.
8+
9+
## Quick Start
10+
11+
1. Go to **Settings → Notification Processing**.
12+
2. Set a template string for the section you want to customize, e.g.:
13+
- **Text Template: New Devices**`{Device name} ({MAC}) - {IP}`
14+
3. Save. The next notification will use your format.
15+
16+
**Before (default):**
17+
```
18+
🆕 New devices
19+
---------
20+
MAC: aa:bb:cc:dd:ee:ff
21+
Datetime: 2025-01-15 10:30:00
22+
IP: 192.168.1.42
23+
Event Type: New Device
24+
Device name: MyPhone
25+
Comments:
26+
```
27+
28+
**After (with template `{Device name} ({MAC}) - {IP}`):**
29+
```
30+
🆕 New devices
31+
---------
32+
MyPhone (aa:bb:cc:dd:ee:ff) - 192.168.1.42
33+
```
34+
35+
## Settings Reference
36+
37+
| Setting | Type | Default | Description |
38+
|---------|------|---------|-------------|
39+
| `NTFPRCS_TEXT_SECTION_HEADERS` | Boolean | `true` | Show/hide section titles (e.g. `🆕 New devices \n---------`). |
40+
| `NTFPRCS_TEXT_TEMPLATE_new_devices` | String | *(empty)* | Template for new device rows. |
41+
| `NTFPRCS_TEXT_TEMPLATE_down_devices` | String | *(empty)* | Template for down device rows. |
42+
| `NTFPRCS_TEXT_TEMPLATE_down_reconnected` | String | *(empty)* | Template for reconnected device rows. |
43+
| `NTFPRCS_TEXT_TEMPLATE_events` | String | *(empty)* | Template for event rows. |
44+
| `NTFPRCS_TEXT_TEMPLATE_plugins` | String | *(empty)* | Template for plugin event rows. |
45+
46+
When a template is **empty**, the section uses the original vertical `Header: Value` format (full backward compatibility).
47+
48+
## Template Syntax
49+
50+
Use `{FieldName}` to insert a value from the notification data. Field names are **case-sensitive** and must match the column names exactly.
51+
52+
```
53+
{Device name} ({MAC}) connected at {Datetime}
54+
```
55+
56+
- No loops, conditionals, or nesting — just simple string replacement.
57+
- If a `{FieldName}` does not exist in the data, it is left as-is in the output (safe failure). For example, `{NonExistent}` renders literally as `{NonExistent}`.
58+
59+
## Variable Availability by Section
60+
61+
Each section has different available fields because they come from different database queries.
62+
63+
### `new_devices` and `events`
64+
65+
| Variable | Description |
66+
|----------|-------------|
67+
| `{MAC}` | Device MAC address |
68+
| `{Datetime}` | Event timestamp |
69+
| `{IP}` | Device IP address |
70+
| `{Event Type}` | Type of event (e.g. `New Device`, `Connected`) |
71+
| `{Device name}` | Device display name |
72+
| `{Comments}` | Device comments |
73+
74+
**Example:** `{Device name} ({MAC}) - {IP} [{Event Type}]`
75+
76+
### `down_devices` and `down_reconnected`
77+
78+
| Variable | Description |
79+
|----------|-------------|
80+
| `{devName}` | Device display name |
81+
| `{eve_MAC}` | Device MAC address |
82+
| `{devVendor}` | Device vendor/manufacturer |
83+
| `{eve_IP}` | Device IP address |
84+
| `{eve_DateTime}` | Event timestamp |
85+
| `{eve_EventType}` | Type of event |
86+
87+
**Example:** `{devName} ({eve_MAC}) {devVendor} - went down at {eve_DateTime}`
88+
89+
### `plugins`
90+
91+
| Variable | Description |
92+
|----------|-------------|
93+
| `{Plugin}` | Plugin code name |
94+
| `{Object_PrimaryId}` | Primary identifier of the object |
95+
| `{Object_SecondaryId}` | Secondary identifier |
96+
| `{DateTimeChanged}` | Timestamp of change |
97+
| `{Watched_Value1}` | First watched value |
98+
| `{Watched_Value2}` | Second watched value |
99+
| `{Watched_Value3}` | Third watched value |
100+
| `{Watched_Value4}` | Fourth watched value |
101+
| `{Status}` | Plugin event status |
102+
103+
**Example:** `{Plugin}: {Object_PrimaryId} - {Status}`
104+
105+
> [!NOTE]
106+
> Field names differ between sections because they come from different SQL queries. A template configured for `new_devices` cannot use `{devName}` — that field is only available in `down_devices` and `down_reconnected`.
107+
108+
## Section Headers Toggle
109+
110+
Set **Text Section Headers** (`NTFPRCS_TEXT_SECTION_HEADERS`) to `false` to remove the section title separators from text notifications. This is useful when you want compact output without the `🆕 New devices \n---------` banners.

front/plugins/notification_processing/config.json

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,150 @@
180180
"string": "You can specify a SQL where condition to filter out Events from notifications. For example <code>AND devLastIP NOT LIKE '192.168.3.%'</code> will always exclude any Event notifications for all devices with the IP starting with <code>192.168.3.%</code>."
181181
}
182182
]
183+
},
184+
{
185+
"function": "TEXT_SECTION_HEADERS",
186+
"type": {
187+
"dataType": "boolean",
188+
"elements": [
189+
{ "elementType": "input", "elementOptions": [{ "type": "checkbox" }], "transformers": [] }
190+
]
191+
},
192+
"default_value": true,
193+
"options": [],
194+
"localized": ["name", "description"],
195+
"name": [
196+
{
197+
"language_code": "en_us",
198+
"string": "Text Section Headers"
199+
}
200+
],
201+
"description": [
202+
{
203+
"language_code": "en_us",
204+
"string": "Enable or disable section titles (e.g. <code>🆕 New devices \\n---------</code>) in text notifications. Enabled by default for backward compatibility."
205+
}
206+
]
207+
},
208+
{
209+
"function": "TEXT_TEMPLATE_new_devices",
210+
"type": {
211+
"dataType": "string",
212+
"elements": [
213+
{ "elementType": "input", "elementOptions": [], "transformers": [] }
214+
]
215+
},
216+
"default_value": "",
217+
"options": [],
218+
"localized": ["name", "description"],
219+
"name": [
220+
{
221+
"language_code": "en_us",
222+
"string": "Text Template: New Devices"
223+
}
224+
],
225+
"description": [
226+
{
227+
"language_code": "en_us",
228+
"string": "Custom text template for new device notifications. Use <code>{FieldName}</code> placeholders, e.g. <code>{Device name} ({MAC}) - {IP}</code>. Leave empty for default formatting. Available fields: <code>{MAC}</code>, <code>{Datetime}</code>, <code>{IP}</code>, <code>{Event Type}</code>, <code>{Device name}</code>, <code>{Comments}</code>."
229+
}
230+
]
231+
},
232+
{
233+
"function": "TEXT_TEMPLATE_down_devices",
234+
"type": {
235+
"dataType": "string",
236+
"elements": [
237+
{ "elementType": "input", "elementOptions": [], "transformers": [] }
238+
]
239+
},
240+
"default_value": "",
241+
"options": [],
242+
"localized": ["name", "description"],
243+
"name": [
244+
{
245+
"language_code": "en_us",
246+
"string": "Text Template: Down Devices"
247+
}
248+
],
249+
"description": [
250+
{
251+
"language_code": "en_us",
252+
"string": "Custom text template for down device notifications. Use <code>{FieldName}</code> placeholders, e.g. <code>{devName} ({eve_MAC}) - {eve_IP}</code>. Leave empty for default formatting. Available fields: <code>{devName}</code>, <code>{eve_MAC}</code>, <code>{devVendor}</code>, <code>{eve_IP}</code>, <code>{eve_DateTime}</code>, <code>{eve_EventType}</code>."
253+
}
254+
]
255+
},
256+
{
257+
"function": "TEXT_TEMPLATE_down_reconnected",
258+
"type": {
259+
"dataType": "string",
260+
"elements": [
261+
{ "elementType": "input", "elementOptions": [], "transformers": [] }
262+
]
263+
},
264+
"default_value": "",
265+
"options": [],
266+
"localized": ["name", "description"],
267+
"name": [
268+
{
269+
"language_code": "en_us",
270+
"string": "Text Template: Reconnected"
271+
}
272+
],
273+
"description": [
274+
{
275+
"language_code": "en_us",
276+
"string": "Custom text template for reconnected device notifications. Use <code>{FieldName}</code> placeholders. Leave empty for default formatting. Available fields: <code>{devName}</code>, <code>{eve_MAC}</code>, <code>{devVendor}</code>, <code>{eve_IP}</code>, <code>{eve_DateTime}</code>, <code>{eve_EventType}</code>."
277+
}
278+
]
279+
},
280+
{
281+
"function": "TEXT_TEMPLATE_events",
282+
"type": {
283+
"dataType": "string",
284+
"elements": [
285+
{ "elementType": "input", "elementOptions": [], "transformers": [] }
286+
]
287+
},
288+
"default_value": "",
289+
"options": [],
290+
"localized": ["name", "description"],
291+
"name": [
292+
{
293+
"language_code": "en_us",
294+
"string": "Text Template: Events"
295+
}
296+
],
297+
"description": [
298+
{
299+
"language_code": "en_us",
300+
"string": "Custom text template for event notifications. Use <code>{FieldName}</code> placeholders, e.g. <code>{Device name} ({MAC}) {Event Type} at {Datetime}</code>. Leave empty for default formatting. Available fields: <code>{MAC}</code>, <code>{Datetime}</code>, <code>{IP}</code>, <code>{Event Type}</code>, <code>{Device name}</code>, <code>{Comments}</code>."
301+
}
302+
]
303+
},
304+
{
305+
"function": "TEXT_TEMPLATE_plugins",
306+
"type": {
307+
"dataType": "string",
308+
"elements": [
309+
{ "elementType": "input", "elementOptions": [], "transformers": [] }
310+
]
311+
},
312+
"default_value": "",
313+
"options": [],
314+
"localized": ["name", "description"],
315+
"name": [
316+
{
317+
"language_code": "en_us",
318+
"string": "Text Template: Plugins"
319+
}
320+
],
321+
"description": [
322+
{
323+
"language_code": "en_us",
324+
"string": "Custom text template for plugin event notifications. Use <code>{FieldName}</code> placeholders, e.g. <code>{Plugin}: {Object_PrimaryId} - {Status}</code>. Leave empty for default formatting. Available fields: <code>{Plugin}</code>, <code>{Object_PrimaryId}</code>, <code>{Object_SecondaryId}</code>, <code>{DateTimeChanged}</code>, <code>{Watched_Value1}</code>, <code>{Watched_Value2}</code>, <code>{Watched_Value3}</code>, <code>{Watched_Value4}</code>, <code>{Status}</code>."
325+
}
326+
]
183327
}
184328
],
185329

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ nav:
5353
- Advanced guides:
5454
- Remote Networks: REMOTE_NETWORKS.md
5555
- Notifications Guide: NOTIFICATIONS.md
56+
- Notification Text Templates: NOTIFICATION_TEMPLATES.md
5657
- Custom PUID/GUID: PUID_PGID_SECURITY.md
5758
- Name Resolution: NAME_RESOLUTION.md
5859
- Authelia: AUTHELIA.md

server/models/notification_instance.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import re
23
import uuid
34
import socket
45
from yattag import indent
@@ -345,8 +346,16 @@ def construct_notifications(JSON, section):
345346
build_direction = "TOP_TO_BOTTOM"
346347
text_line = "{}\t{}\n"
347348

349+
# Read template settings
350+
show_headers = get_setting_value("NTFPRCS_TEXT_SECTION_HEADERS")
351+
if show_headers is None or show_headers == "":
352+
show_headers = True
353+
text_template = get_setting_value(f"NTFPRCS_TEXT_TEMPLATE_{section}") or ""
354+
348355
if len(jsn) > 0:
349-
text = tableTitle + "\n---------\n"
356+
# Section header (text)
357+
if show_headers:
358+
text = tableTitle + "\n---------\n"
350359

351360
# Convert a JSON into an HTML table
352361
html = convert(
@@ -363,13 +372,24 @@ def construct_notifications(JSON, section):
363372
)
364373

365374
# prepare text-only message
366-
for device in jsn:
367-
for header in headers:
368-
padding = ""
369-
if len(header) < 4:
370-
padding = "\t"
371-
text += text_line.format(header + ": " + padding, device[header])
372-
text += "\n"
375+
if text_template:
376+
# Custom template: replace {FieldName} placeholders per device
377+
for device in jsn:
378+
line = re.sub(
379+
r'\{(.+?)\}',
380+
lambda m: str(device.get(m.group(1), m.group(0))),
381+
text_template,
382+
)
383+
text += line + "\n"
384+
else:
385+
# Legacy fallback: vertical Header: Value list
386+
for device in jsn:
387+
for header in headers:
388+
padding = ""
389+
if len(header) < 4:
390+
padding = "\t"
391+
text += text_line.format(header + ": " + padding, device[header])
392+
text += "\n"
373393

374394
# Format HTML table headers
375395
for header in headers:

0 commit comments

Comments
 (0)