Skip to content

Commit 2b90c29

Browse files
committed
[ntfy] Add dedicated service plugin ntfy
1 parent d9b495b commit 2b90c29

18 files changed

+739
-51
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ in progress
1414
- [file] Allow writing of binary content. Thanks, @sevmonster.
1515
- [ux] Rename subcommand ``mqttwarn make-samplefuncs`` to ``mqttwarn make-udf``,
1616
and adjust naming.
17+
- [ntfy] Add dedicated service plugin ``ntfy``
1718

1819

1920
2023-04-11 0.33.0

README.rst

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,18 +183,22 @@ you an idea how to pass relevant information on the command line using JSON::
183183
# Launch "pushover" service plugin
184184
mqttwarn --plugin=pushover --options='{"title": "About", "message": "Hello world", "addrs": ["userkey", "token"], "priority": 6}'
185185

186-
# Launch "ssh" service plugin from the command line
186+
# Launch "ntfy" service plugin
187+
mqttwarn --plugin=ntfy --options='{"addrs": {"url": "http://localhost:5555/testdrive"}, "title": "Example notification", "message": "Hello world"}' --data='{"tags": "foo,bar,äöü", "priority": "high"}'
188+
189+
# Launch "ntfy" service plugin, and add remote attachment
190+
mqttwarn --plugin=ntfy --options='{"addrs": {"url": "http://localhost:5555/testdrive"}, "title": "Example notification", "message": "Hello world"}' --data='{"attach": "https://unsplash.com/photos/spdQ1dVuIHw/download?w=320", "filename": "goat.jpg"}'
191+
192+
# Launch "ntfy" service plugin, and add attachment from local filesystem
193+
mqttwarn --plugin=ntfy --options='{"addrs": {"url": "http://localhost:5555/testdrive", "attachment": "goat.jpg"}, "title": "Example notification", "message": "Hello world"}'
194+
195+
# Launch "ssh" service plugin
187196
mqttwarn --plugin=ssh --config='{"host": "ssh.example.org", "port": 22, "user": "foo", "password": "bar"}' --options='{"addrs": ["command with substitution %s"], "payload": "{\"args\": \"192.168.0.1\"}"}'
188197

189-
# Launch "cloudflare_zone" service plugin from "mqttwarn-contrib", passing "--config" parameters via command line
198+
# Launch "cloudflare_zone" service plugin from "mqttwarn-contrib"
190199
pip install mqttwarn-contrib
191200
mqttwarn --plugin=mqttwarn_contrib.services.cloudflare_zone --config='{"auth-email": "foo", "auth-key": "bar"}' --options='{"addrs": ["0815", "www.example.org", ""], "message": "192.168.0.1"}'
192201

193-
# Submit notification to "ntfy", using Apprise service plugin.
194-
mqttwarn --plugin=apprise \
195-
--config='{"baseuri": "ntfy://user:password@ntfy.example.org/topic1/topic2"}' \
196-
--options='{"addrs": [], "title": "Example notification", "message": "Hello world"}'
197-
198202

199203
Also, the ``--config-file`` parameter can be used to optionally specify the
200204
path to a configuration file.

docs/notifier-catalog.md

Lines changed: 151 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -223,22 +223,22 @@ template arguments. mqttwarn supports propagating them from either the
223223
``baseuri`` configuration setting, or from its data dictionary to the Apprise
224224
plugin invocation.
225225

226-
So, for example, you can propagate parameters to the [Apprise Ntfy plugin]
227-
by either pre-setting them as URL query parameters, like
226+
So, for example, you can propagate parameters to the [Apprise JSON HTTP POST
227+
Notifications plugin] by either pre-setting them as URL query parameters, like
228228
```
229-
ntfy://user:password@ntfy.example.org/topic1/topic2?email=test@example.org
229+
json://localhost/?:sound=oceanwave
230230
```
231231
or by submitting them within a JSON-formatted MQTT message, like
232232
```json
233-
{"priority": "high", "tags": "foo,bar", "click": "https://httpbin.org/headers"}
233+
{":sound": "oceanwave", "tags": "foo,bar", "click": "https://httpbin.org/headers"}
234234
```
235235

236236

237237
[Apprise]: https://github.com/caronc/apprise
238238
[Apprise documentation]: https://github.com/caronc/apprise/wiki
239239
[Apprise URL Basics]: https://github.com/caronc/apprise/wiki/URLBasics
240240
[Apprise Notification Services]: https://github.com/caronc/apprise/wiki#notification-services
241-
[Apprise Ntfy plugin]: https://github.com/caronc/apprise/wiki/Notify_ntfy
241+
[Apprise JSON HTTP POST Notifications plugin]: https://github.com/caronc/apprise/wiki/Notify_Custom_JSON
242242

243243

244244
### `apprise_single`
@@ -256,7 +256,7 @@ Apprise to E-Mail, an HTTP endpoint, and a Discord channel.
256256

257257
```ini
258258
[defaults]
259-
launch = apprise-mail, apprise-json, apprise-discord, apprise-ntfy
259+
launch = apprise-mail, apprise-json, apprise-discord
260260

261261
[config:apprise-mail]
262262
; Dispatch message as e-mail.
@@ -283,16 +283,9 @@ baseuri = 'json://localhost:1234/mqtthook'
283283
module = 'apprise_single'
284284
baseuri = 'discord://4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js'
285285

286-
[config:apprise-ntfy]
287-
; Dispatch message to ntfy.
288-
; https://github.com/caronc/apprise/wiki/URLBasics
289-
; https://github.com/caronc/apprise/wiki/Notify_ntfy
290-
module = 'apprise_single'
291-
baseuri = 'ntfy://user:password@ntfy.example.org/topic1/topic2'
292-
293286
[apprise-single-test]
294287
topic = apprise/single/#
295-
targets = apprise-mail:demo, apprise-json, apprise-discord, apprise-ntfy
288+
targets = apprise-mail:demo, apprise-json, apprise-discord
296289
format = Alarm from {device}: {payload}
297290
title = Alarm from {device}
298291
```
@@ -325,7 +318,6 @@ module = 'apprise_multi'
325318
targets = {
326319
'demo-http' : [ { 'baseuri': 'json://localhost:1234/mqtthook' }, { 'baseuri': 'json://daq.example.org:5555/foobar' } ],
327320
'demo-discord' : [ { 'baseuri': 'discord://4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js' } ],
328-
'demo-ntfy' : [ { 'baseuri': 'ntfy://user:password@ntfy.example.org/topic1/topic2' } ],
329321
'demo-mailto' : [ {
330322
'baseuri': 'mailtos://smtp_username:smtp_password@mail.example.org',
331323
'recipients': ['foo@example.org', 'bar@example.org'],
@@ -336,7 +328,7 @@ targets = {
336328

337329
[apprise-multi-test]
338330
topic = apprise/multi/#
339-
targets = apprise-multi:demo-http, apprise-multi:demo-discord, apprise-multi:demo-mailto, apprise-multi:demo-ntfy
331+
targets = apprise-multi:demo-http, apprise-multi:demo-discord, apprise-multi:demo-mailto
340332
format = Alarm from {device}: {payload}
341333
title = Alarm from {device}
342334
```
@@ -1746,10 +1738,151 @@ Requires:
17461738

17471739
### `ntfy`
17481740

1749-
Support for [ntfy] is provided through Apprise, see [apprise_single](#apprise_single)
1750-
and [apprise_multi](#apprise_multi).
1741+
> [ntfy] (pronounce: _notify_) is a simple HTTP-based [pub-sub] notification service.
1742+
> It allows you to send notifications to your phone or desktop via scripts from
1743+
> any computer, entirely without signup, cost or setup.
1744+
> [ntfy is also open source](https://github.com/binwiederhier/ntfy), if you want to
1745+
> run an instance on your own premises.
1746+
1747+
ntfy uses topics to address communication channels. This topic is part of the
1748+
HTTP API URL.
1749+
1750+
To use the hosted variant on `ntfy.sh`, just provide an URL including the topic.
1751+
```ini
1752+
[config:ntfy]
1753+
targets = {
1754+
'test': 'https://ntfy.sh/testdrive',
1755+
}
1756+
```
1757+
1758+
When running your own instance, you would use a custom URL here.
1759+
```ini
1760+
[config:ntfy]
1761+
targets = {
1762+
'test': 'http://username:password@localhost:5555/testdrive',
1763+
}
1764+
```
1765+
1766+
In order to specify more options, please wrap your ntfy URL into a dictionary
1767+
under the `url` key. This way, additional options can be added.
1768+
```ini
1769+
[config:ntfy]
1770+
targets = {
1771+
'test': {
1772+
'url': 'https://ntfy.sh/testdrive',
1773+
},
1774+
}
1775+
```
1776+
1777+
:::{important}
1778+
[ntfy publishing options] outlines different ways to marshal data to the ntfy
1779+
HTTP API. mqttwarn is using HTTP headers for serializing values, because the
1780+
HTTP body will already be used for the attachment file. Because of this, you
1781+
are not able to use UTF-8 characters within your message text, they will be
1782+
replaced by placeholder characters like `?`.
1783+
:::
1784+
1785+
{#ntfy-remote-attachments}
1786+
#### Remote attachments
1787+
In order to submit notifications with an attachment file at a remote location,
1788+
use the `attach` field. Optionally, the `filename` field can be used to assign
1789+
a different name to the file.
1790+
```ini
1791+
[config:ntfy]
1792+
targets = {
1793+
'test': {
1794+
'url': 'https://ntfy.sh/testdrive',
1795+
'attach': 'https://unsplash.com/photos/spdQ1dVuIHw/download?w=320',
1796+
'filename': 'goat.jpg',
1797+
},
1798+
}
1799+
```
1800+
1801+
{#ntfy-local-attachments}
1802+
#### Local attachments
1803+
By using the `attachment` option, you can add an attachment to your message, local
1804+
to the machine mqttwarn is running on. The file will be uploaded when submitting
1805+
the notification, and ntfy will serve it for clients so that you don't have to. In
1806+
order to address the file, you can provide a path template, where the transformation
1807+
data will also get interpolated into.
1808+
```ini
1809+
[config:ntfy]
1810+
targets = {
1811+
'test': {
1812+
'url': 'https://ntfy.sh/testdrive',
1813+
'attachment': '/tmp/ntfy-attachment-{slot}-{label}.png',
1814+
}
1815+
}
1816+
```
1817+
:::{important}
1818+
In order to allow users to **upload** and attach files to notifications, you will
1819+
need to enable the corresponding ntfy feature by simply configuring an attachment
1820+
cache directory and a base URL (`attachment-cache-dir`, `base-url`), see
1821+
[ntfy stored attachments].
1822+
:::
1823+
:::{note}
1824+
When mqttwarn processes a message, and accessing the file raises an error, it gets
1825+
handled gracefully. In this way, notifications will be triggered even when attaching
1826+
the file fails for whatever reasons.
1827+
:::
1828+
1829+
#### Publishing options
1830+
You can use all the available [ntfy publishing options], by using the corresponding
1831+
option names listed within `NTFY_FIELD_NAMES`, which are: `message`, `title`, `tags`,
1832+
`priority`, `actions`, `click`, `attach`, `filename`, `delay`, and `email`.
1833+
1834+
You can obtain ntfy option fields from _three_ contexts in total, as implemented
1835+
by the `obtain_ntfy_fields` function. Effectively, that means that you can place
1836+
them either within the `targets` address descriptor, within the configuration
1837+
section, or submit them using a JSON MQTT message and a corresponding decoder
1838+
function into the transformation data dictionary.
1839+
1840+
For example, you can always send a `priority` field using MQTT/JSON, or use one of
1841+
those configuration snippets, which are equivalent.
1842+
```ini
1843+
[config:ntfy]
1844+
targets = {
1845+
'test': {
1846+
'url': 'https://ntfy.sh/testdrive',
1847+
'priority': 'high',
1848+
}
1849+
}
1850+
```
1851+
```ini
1852+
[config:ntfy]
1853+
targets = {
1854+
'test': {
1855+
'url': 'https://ntfy.sh/testdrive',
1856+
}
1857+
}
1858+
priority = high
1859+
```
1860+
1861+
The highest precedence takes data coming in from the transformation data dictionary,
1862+
followed by option fields coming in from the per-recipient `targets` address descriptor,
1863+
followed by option fields defined on the `[config:ntfy]` configuration section.
1864+
1865+
#### Examples
1866+
1867+
1. This is another way to write the "[remote attachments](#ntfy-remote-attachments)"
1868+
example, where all ntfy options are located on the configuration section, so they
1869+
will apply for all configured target addresses.
1870+
```ini
1871+
[config:ntfy]
1872+
targets = {'test': 'https://ntfy.sh/testdrive'}
1873+
attach = https://unsplash.com/photos/spdQ1dVuIHw/download?w=320
1874+
filename = goat.jpg
1875+
```
1876+
1877+
2. The tutorial [](#processing-frigate-events) explains how to configure mqttwarn to
1878+
notify the user with events emitted by Frigate, a network video recorder (NVR)
1879+
with realtime local object detection for IP cameras.
1880+
17511881

17521882
[ntfy]: https://ntfy.sh/
1883+
[ntfy publishing options]: https://docs.ntfy.sh/publish/
1884+
[ntfy stored attachments]: https://docs.ntfy.sh/config/#attachments
1885+
[pub-sub]: https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
17531886

17541887

17551888
### `desktopnotify`

mqttwarn/commands.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def run():
2626
Usage:
2727
{program} [make-config]
2828
{program} [make-udf]
29-
{program} [--config=] [--config-file=] [--plugin=] [--options=]
29+
{program} [--config=] [--config-file=] [--plugin=] [--options=] [--data=]
3030
{program} --version
3131
{program} (-h | --help)
3232
@@ -39,8 +39,8 @@ def run():
3939
[--plugin=] The plugin name to load. This can either be a
4040
full qualified Python package/module name or a
4141
path to a Python file.
42-
[--options=] Configuration options to propagate to the plugin
43-
entrypoint.
42+
[--options=] Configuration options to propagate to the plugin entrypoint.
43+
[--data=] Data to propagate to the plugin entrypoint.
4444
4545
Bootstrapping options:
4646
make-config Dump configuration file blueprint to STDOUT,
@@ -76,22 +76,23 @@ def run():
7676

7777
# Decode arguments
7878
arg_plugin = options["--plugin"]
79-
arg_options = json.loads(options["--options"])
79+
arg_options = options["--options"] and json.loads(options["--options"]) or {}
80+
arg_data = options["--data"] and json.loads(options["--data"]) or {}
8081
arg_config = None
8182
if "--config" in options and options["--config"] is not None:
8283
arg_config = json.loads(options["--config"])
8384

8485
# Launch service plugin in standalone mode
8586
launch_plugin_standalone(
86-
arg_plugin, arg_options, configfile=options.get("--config-file"), config_more=arg_config
87+
arg_plugin, arg_options, arg_data, configfile=options.get("--config-file"), config_more=arg_config
8788
)
8889

8990
# Run mqttwarn in service mode when no command line arguments are given
9091
else:
9192
run_mqttwarn()
9293

9394

94-
def launch_plugin_standalone(plugin, options, configfile=None, config_more=None):
95+
def launch_plugin_standalone(plugin, options, data, configfile=None, config_more=None):
9596

9697
# Optionally load configuration file
9798
does_not_exist = False
@@ -120,7 +121,7 @@ def launch_plugin_standalone(plugin, options, configfile=None, config_more=None)
120121
logger.info('Running service plugin "{}" with options "{}"'.format(plugin, options))
121122

122123
# Launch service plugin
123-
run_plugin(config=config, name=plugin, options=options)
124+
run_plugin(config=config, name=plugin, options=options, data=data)
124125

125126

126127
def run_mqttwarn():

mqttwarn/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@ def bootstrap(config=None, scriptname=None):
784784
SCRIPTNAME = scriptname
785785

786786

787-
def run_plugin(config=None, name=None, options=None):
787+
def run_plugin(config=None, name=None, options=None, data=None):
788788
"""
789789
Run service plugins directly without the
790790
dispatching and transformation machinery.
@@ -817,7 +817,7 @@ def run_plugin(config=None, name=None, options=None):
817817
item.config = config.config("config:" + name)
818818
item.service = srv
819819
item.target = "mqttwarn"
820-
item.data = {} # FIXME
820+
item.data = data or {}
821821

822822
# Launch plugin
823823
module = service_plugins[name]["module"]

mqttwarn/model.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def enum(self):
4040
# Covering old- and new-style configuration layouts. `addrs` has
4141
# originally been a list of strings, has been expanded to be a
4242
# list of dictionaries (Apprise), and to be a dictionary (Pushsafer).
43-
addrs_type = Union[List[Union[str, Dict[str, str]]], Dict[str, str]]
43+
addrs_type = Union[List[Union[str, Dict[str, str]]], Dict[str, str], str]
4444

4545

4646
@dataclass
@@ -52,6 +52,7 @@ class ProcessorItem:
5252
service: Optional[str] = None
5353
target: Optional[str] = None
5454
config: Dict = field(default_factory=dict)
55+
# TODO: `addrs` can also be a string or dictionary now.
5556
addrs: addrs_type = field(default_factory=list) # type: ignore[assignment]
5657
priority: Optional[int] = None
5758
topic: Optional[str] = None

mqttwarn/services/apprise_multi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def plugin(srv, item):
4242
# Collect URL parameters.
4343
params = OrderedDict()
4444

45-
# Obtain and apply all possible Ntfy parameters from data dictionary.
45+
# Obtain and apply all possible Apprise parameters from data dictionary.
4646
params.update(obtain_apprise_arguments(item, APPRISE_ALL_ARGUMENT_NAMES))
4747

4848
# Apply addressee information.

mqttwarn/services/apprise_single.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def plugin(srv, item):
4343
# Collect URL parameters.
4444
params = OrderedDict()
4545

46-
# Obtain and apply all possible Ntfy parameters from data dictionary.
46+
# Obtain and apply all possible Apprise parameters from data dictionary.
4747
params.update(obtain_apprise_arguments(item, APPRISE_ALL_ARGUMENT_NAMES))
4848

4949
# Apply addressee information.

mqttwarn/services/apprise_util.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ def get_all_template_argument_names():
3030
def obtain_apprise_arguments(item: ProcessorItem, arg_names: list) -> dict:
3131
"""
3232
Obtain eventual Apprise parameters from data dictionary.
33-
34-
https://github.com/caronc/apprise/wiki/Notify_ntfy#parameter-breakdown
3533
"""
3634
params = dict()
3735
for arg_name in arg_names:

0 commit comments

Comments
 (0)