Skip to content

Commit 25a2cfc

Browse files
committed
Improve Apprise notification machinery. Add apprise_multi plugin.
1 parent b927a61 commit 25a2cfc

File tree

7 files changed

+311
-32
lines changed

7 files changed

+311
-32
lines changed

CHANGES.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ in progress
1010
This helps for service plugins like Apprise to make the configuration
1111
snippet more compact. Now, service configurations can omit the ``targets``
1212
option altogether.
13-
- Apprise service: Accept omitted/empty `addrs` attribute.
14-
- Apprise service: Improve query parameter serialization.
13+
- ``apprise_single`` service: Accept omitted/empty `addrs` attribute.
14+
- ``apprise_single`` service: Improve query parameter serialization.
15+
- ``apprise_multi`` service: New plugin. Thanks, @psyciknz!
1516

1617

1718
2021-10-17 0.27.0

HANDBOOK.md

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,8 @@ _mqttwarn_ supports a number of services (listed alphabetically below):
323323
* [alexa-notify-me](#alexa-notify-me)
324324
* [amqp](#amqp)
325325
* [apns](#apns)
326-
* [apprise](#apprise)
326+
* [apprise_single](#apprise_single)
327+
* [apprise_multi](#apprise_multi)
327328
* [asterisk](#asterisk)
328329
* [autoremote](#autoremote)
329330
* [carbon](#carbon)
@@ -513,26 +514,24 @@ would thus emit the APNS notification to the specified device.
513514
Requires [PyAPNs](https://github.com/djacobs/PyAPNs)
514515
515516
516-
### `apprise`
517+
### `apprise_single`
517518
518-
The `apprise` service interacts with the [Apprise] Python module,
519+
The `apprise_single` service interacts with the [Apprise] Python module,
519520
which in turn can talk to a plethora of popular notification services.
520521
Please read their documentation about more details.
521522
522-
The following discussion assumes a payload like this is published via MQTT:
523+
The following configuration snippet example expects a payload like this to be
524+
published to the MQTT broker:
523525
```bash
524-
echo '{"device": "foobar"}' | mosquitto_pub -t 'apprise/foo' -l
526+
echo '{"device": "foobar", "name": "temperature", "number": 42.42}' | mosquitto_pub -t 'apprise/single/foo' -l
525527
```
526528
527-
This configuration snippet will activate two service plugins
528-
`apprise-mail` and `apprise-json`, both using the Apprise module.
529-
530529
```ini
531530
[defaults]
532531
launch = apprise-mail, apprise-json, apprise-discord
533532
534533
[config:apprise-mail]
535-
; Submit emails for notifying users.
534+
; Dispatch message as e-mail.
536535
; https://github.com/caronc/apprise/wiki/Notify_email
537536
module = 'apprise'
538537
baseuri = 'mailtos://smtp_username:[email protected]'
@@ -543,26 +542,66 @@ targets = {
543542
}
544543
545544
[config:apprise-json]
546-
; Post message to HTTP endpoint, in JSON format.
545+
; Dispatch message to HTTP endpoint, in JSON format.
547546
; https://github.com/caronc/apprise/wiki/Notify_Custom_JSON
548547
module = 'apprise'
549548
baseuri = 'json://localhost:1234/mqtthook'
550549
551550
[config:apprise-discord]
552-
; Post message to Discord channel, via Webhook.
551+
; Dispatch message to Discord channel, via Webhook.
553552
; https://github.com/caronc/apprise/wiki/Notify_discord
554553
; https://discord.com/developers/docs/resources/webhook
555554
; discord://{WebhookID}/{WebhookToken}/
556555
module = 'apprise'
557556
baseuri = 'discord://4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js'
558557
559-
[apprise-test]
560-
topic = apprise/#
558+
[apprise-single-test]
559+
topic = apprise/single/#
561560
targets = apprise-mail:demo, apprise-json, apprise-discord
562561
format = Alarm from {device}: {payload}
563562
title = Alarm from {device}
564563
```
565564
565+
### `apprise_multi`
566+
567+
The `apprise_multi` service interacts with the [Apprise] Python module,
568+
which in turn can talk to a plethora of popular notification services.
569+
Please read their documentation about more details.
570+
571+
The idea behind this variant is to publish messages to different Apprise
572+
plugins within a single configuration snippet, containing multiple recipients.
573+
574+
The following configuration snippet example expects a payload like this to be
575+
published to the MQTT broker:
576+
```bash
577+
echo '{"device": "foobar", "name": "temperature", "number": 42.42}' | mosquitto_pub -t 'apprise/multi/foo' -l
578+
```
579+
580+
```ini
581+
[defaults]
582+
launch = apprise-multi
583+
584+
[config:apprise-multi]
585+
; Dispatch message to multiple Apprise plugins.
586+
module = 'apprise_multi'
587+
targets = {
588+
'demo-http' : [ { 'baseuri': 'json://localhost:1234/mqtthook' }, { 'baseuri': 'json://daq.example.org:5555/foobar' } ],
589+
'demo-discord' : [ { 'baseuri': 'discord://4174216298/JHMHI8qBe7bk2ZwO5U711o3dV_js' } ],
590+
'demo-mailto' : [ {
591+
'baseuri': 'mailtos://smtp_username:[email protected]',
592+
'recipients': ['[email protected]', '[email protected]'],
593+
'sender': '[email protected]',
594+
'sender_name': 'Example Monitoring',
595+
} ],
596+
}
597+
598+
[apprise-multi-test]
599+
topic = apprise/multi/#
600+
targets = apprise-multi:demo-http, apprise-multi:demo-discord, apprise-multi:demo-mailto
601+
format = Alarm from {device}: {payload}
602+
title = Alarm from {device}
603+
```
604+
566605
[Apprise]: https://github.com/caronc/apprise
567606
568607

mqttwarn/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class ProcessorItem:
1212

1313
service: str = None
1414
target: str = None
15-
config: Dict = None
15+
config: Dict = field(default_factory=dict)
1616
addrs: List[str] = field(default_factory=list)
1717
priority: int = None
1818
topic: str = None

mqttwarn/services/apprise_multi.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- coding: utf-8 -*-
2+
3+
__author__ = 'Andreas Motl <[email protected]>'
4+
__copyright__ = 'Copyright 2021 Andreas Motl'
5+
__license__ = 'Eclipse Public License - v 1.0 (http://www.eclipse.org/legal/epl-v10.html)'
6+
7+
# https://github.com/caronc/apprise#developers
8+
from urllib.parse import urlencode
9+
from collections import OrderedDict
10+
11+
import apprise
12+
13+
14+
def plugin(srv, item):
15+
"""Send a message to multiple Apprise plugins."""
16+
17+
srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target)
18+
19+
addresses = item.addrs
20+
title = item.title
21+
body = item.message
22+
23+
try:
24+
srv.logging.debug("Sending notification to Apprise. target=%s, addresses=%s" % (item.target, addresses))
25+
26+
# Create an Apprise instance.
27+
apobj = apprise.Apprise(asset=apprise.AppriseAsset(async_mode=False))
28+
29+
for address in addresses:
30+
baseuri = address["baseuri"]
31+
32+
# Collect URL parameters.
33+
params = OrderedDict()
34+
35+
if "recipients" in address:
36+
to = ','.join(address["recipients"])
37+
if to:
38+
params["to"] = to
39+
40+
if "sender" in address:
41+
params["from"] = address["sender"]
42+
if "sender_name" in address:
43+
params["name"] = address["sender_name"]
44+
45+
# Add notification services by server url.
46+
uri = baseuri
47+
if params:
48+
uri += '?' + urlencode(params)
49+
srv.logging.info("Adding notification to: {}".format(uri))
50+
apobj.add(uri)
51+
52+
# Submit notification.
53+
outcome = apobj.notify(
54+
body=body,
55+
title=title,
56+
)
57+
58+
if outcome:
59+
srv.logging.info("Successfully sent message using Apprise")
60+
return True
61+
62+
else:
63+
srv.logging.error("Sending message using Apprise failed")
64+
return False
65+
66+
except Exception as e:
67+
srv.logging.error("Sending message using Apprise failed. target=%s, error=%s" % (item.target, e))
68+
return False
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
def plugin(srv, item):
15-
"""Send a message to Apprise plugin(s)."""
15+
"""Send a message to a single Apprise plugin."""
1616

1717
srv.logging.debug("*** MODULE=%s: service=%s, target=%s", __file__, item.service, item.target)
1818

@@ -60,5 +60,5 @@ def plugin(srv, item):
6060
return False
6161

6262
except Exception as e:
63-
srv.logging.error("Error sending message to %s: %s" % (item.target, e))
63+
srv.logging.error("Sending message using Apprise failed. target=%s, error=%s" % (item.target, e))
6464
return False
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# -*- coding: utf-8 -*-
2+
# (c) 2021 The mqttwarn developers
3+
import logging
4+
from unittest import mock
5+
from unittest.mock import call
6+
7+
from mqttwarn.model import ProcessorItem as Item
8+
from mqttwarn.util import load_module_by_name
9+
from surrogate import surrogate
10+
11+
12+
@surrogate("apprise")
13+
@mock.patch("apprise.Apprise", create=True)
14+
@mock.patch("apprise.AppriseAsset", create=True)
15+
def test_apprise_multi_basic_success(apprise_asset, apprise_mock, srv, caplog):
16+
17+
with caplog.at_level(logging.DEBUG):
18+
19+
module = load_module_by_name("mqttwarn.services.apprise_multi")
20+
21+
item = Item(
22+
addrs=[
23+
{"baseuri": "json://localhost:1234/mqtthook"},
24+
{"baseuri": "json://daq.example.org:5555/foobar"},
25+
],
26+
title="⚽ Message title ⚽",
27+
message="⚽ Notification message ⚽",
28+
)
29+
30+
outcome = module.plugin(srv, item)
31+
32+
assert apprise_mock.mock_calls == [
33+
call(asset=mock.ANY),
34+
call().add("json://localhost:1234/mqtthook"),
35+
call().add("json://daq.example.org:5555/foobar"),
36+
call().notify(body="⚽ Notification message ⚽", title="⚽ Message title ⚽"),
37+
call().notify().__bool__(),
38+
]
39+
40+
assert outcome is True
41+
assert (
42+
"Sending notification to Apprise. target=None, addresses=[{'baseuri': 'json://localhost:1234/mqtthook'}, {'baseuri': 'json://daq.example.org:5555/foobar'}]"
43+
in caplog.messages
44+
)
45+
assert "Successfully sent message using Apprise" in caplog.messages
46+
47+
48+
@surrogate("apprise")
49+
@mock.patch("apprise.Apprise", create=True)
50+
@mock.patch("apprise.AppriseAsset", create=True)
51+
def test_apprise_multi_mailto_success(apprise_asset, apprise_mock, srv, caplog):
52+
53+
with caplog.at_level(logging.DEBUG):
54+
55+
module = load_module_by_name("mqttwarn.services.apprise_multi")
56+
57+
item = Item(
58+
addrs=[
59+
{
60+
"baseuri": "mailtos://smtp_username:[email protected]",
61+
"recipients": ["[email protected]", "[email protected]"],
62+
"sender": "[email protected]",
63+
"sender_name": "Example Monitoring",
64+
}
65+
],
66+
title="⚽ Message title ⚽",
67+
message="⚽ Notification message ⚽",
68+
)
69+
70+
outcome = module.plugin(srv, item)
71+
72+
assert apprise_mock.mock_calls == [
73+
call(asset=mock.ANY),
74+
call().add(
75+
"mailtos://smtp_username:[email protected]?to=foo%40example.org%2Cbar%40example.org&from=monitoring%40example.org&name=Example+Monitoring"
76+
),
77+
call().notify(body="⚽ Notification message ⚽", title="⚽ Message title ⚽"),
78+
call().notify().__bool__(),
79+
]
80+
81+
assert outcome is True
82+
assert (
83+
"Sending notification to Apprise. target=None, addresses=[{'baseuri': 'mailtos://smtp_username:[email protected]', 'recipients': ['[email protected]', '[email protected]'], 'sender': '[email protected]', 'sender_name': 'Example Monitoring'}]"
84+
in caplog.messages
85+
)
86+
assert "Successfully sent message using Apprise" in caplog.messages
87+
88+
89+
@surrogate("apprise")
90+
def test_apprise_multi_failure_notify(srv, caplog):
91+
92+
with caplog.at_level(logging.DEBUG):
93+
94+
mock_connection = mock.MagicMock()
95+
96+
# Make the call to `notify` signal failure.
97+
def error(*args, **kwargs):
98+
return False
99+
100+
mock_connection.notify = error
101+
102+
with mock.patch(
103+
"apprise.Apprise", side_effect=[mock_connection], create=True
104+
) as mock_client:
105+
with mock.patch("apprise.AppriseAsset", create=True) as mock_asset:
106+
module = load_module_by_name("mqttwarn.services.apprise_multi")
107+
108+
item = Item(
109+
addrs=[{"baseuri": "json://localhost:1234/mqtthook"}],
110+
title="⚽ Message title ⚽",
111+
message="⚽ Notification message ⚽",
112+
)
113+
114+
outcome = module.plugin(srv, item)
115+
116+
assert mock_client.mock_calls == [
117+
mock.call(asset=mock.ANY),
118+
]
119+
assert mock_connection.mock_calls == [
120+
call.add("json://localhost:1234/mqtthook"),
121+
]
122+
123+
assert outcome is False
124+
assert (
125+
"Sending notification to Apprise. target=None, addresses=[{'baseuri': 'json://localhost:1234/mqtthook'}]"
126+
in caplog.messages
127+
)
128+
assert "Sending message using Apprise failed" in caplog.messages
129+
130+
131+
@surrogate("apprise")
132+
def test_apprise_multi_error(srv, caplog):
133+
134+
with caplog.at_level(logging.DEBUG):
135+
136+
mock_connection = mock.MagicMock()
137+
138+
# Make the call to `notify` raise an exception.
139+
def error(*args, **kwargs):
140+
raise Exception("something failed")
141+
142+
mock_connection.notify = error
143+
144+
with mock.patch(
145+
"apprise.Apprise", side_effect=[mock_connection], create=True
146+
) as mock_client:
147+
with mock.patch("apprise.AppriseAsset", create=True) as mock_asset:
148+
module = load_module_by_name("mqttwarn.services.apprise_multi")
149+
150+
item = Item(
151+
addrs=[{"baseuri": "json://localhost:1234/mqtthook"}],
152+
title="⚽ Message title ⚽",
153+
message="⚽ Notification message ⚽",
154+
)
155+
156+
outcome = module.plugin(srv, item)
157+
158+
assert mock_client.mock_calls == [
159+
mock.call(asset=mock.ANY),
160+
]
161+
assert mock_connection.mock_calls == [
162+
call.add("json://localhost:1234/mqtthook"),
163+
]
164+
165+
assert outcome is False
166+
assert (
167+
"Sending notification to Apprise. target=None, addresses=[{'baseuri': 'json://localhost:1234/mqtthook'}]"
168+
in caplog.messages
169+
)
170+
assert (
171+
"Sending message using Apprise failed. target=None, error=something failed"
172+
in caplog.messages
173+
)

0 commit comments

Comments
 (0)