diff --git a/README.md b/README.md index 709d8b5..844c321 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ export AUTO_NTFY_DONE_IGNORE="vim screen meld" implementation that doesn\'t have additional dependencies - [Rocket.Chat](https://Rocket.Chat) support requires installing as `pip install ntfy[rocketchat]` + - [MQTT](https://mqtt.org) support requires installing as + `pip install ntfy[mqtt]` To install multiple extras, separate with commas: e.g., `pip install ntfy[pid,emoji]`. @@ -441,6 +443,7 @@ Required parameters: You must either specify `token`, or `userId` and `password`. + [Webpush](https://github.com/dschep/ntfy-webpush) - `ntfy_webpush` \~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~ Webpush support is provded by an external ntfy module, install like @@ -458,6 +461,24 @@ Required parameters: For more info, see [ntfy-webpush]{#ntfy-webpush} \\`\_ +### [MQTT](https://mqtt.org) - `mqtt` +------------------------------------------------------------- +Requires extras, install like this: `pip install ntfy[mqtt]`. + +Required parameters: + +: - `hostname` - The MQTT broker hostname + - `topic` - The MQTT topic to publish to + +Optional parameters: + +: - `port` - The MQTT broker port (defaults to 1883) + - `qos` - The Quality of Service level (defaults to 1) + - `username` - The MQTT username + - `password` - The MQTT password + + + ### 3rd party backends To use or implement your own backends, specify the full path of the diff --git a/ntfy/backends/mqtt.py b/ntfy/backends/mqtt.py new file mode 100644 index 0000000..ca78826 --- /dev/null +++ b/ntfy/backends/mqtt.py @@ -0,0 +1,42 @@ +import json +import logging +import paho.mqtt.publish as publish + +def notify(title, message, **kwargs): + """ + Publishes a notification to an MQTT topic. + """ + hostname = kwargs.get("hostname") + topic = kwargs.get("topic") + + if not hostname or not topic: + logging.error("MQTT backend requires 'hostname' and 'topic' parameters in the configuration.") + return + + port = int(kwargs.get("port", 1883)) + qos = int(kwargs.get("qos", 1)) + retcode = kwargs.get("retcode") + username = kwargs.get("username") + password = kwargs.get("password") + + payload = json.dumps({ + "title": title, + "message": message, + "retcode": retcode + }) + + auth = None + if username: + auth = {'username': username, 'password': password} + + try: + publish.single( + topic=topic, + payload=payload, + hostname=hostname, + port=port, + qos=qos, + auth=auth + ) + except Exception as e: + logging.error("Failed to publish MQTT message: %s", e) \ No newline at end of file diff --git a/setup.py b/setup.py index eac4538..9455c86 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ 'slack':['slack_sdk'], 'rocketchat':['rocketchat-API'], 'matrix':['matrix_client'], + 'mqtt': ['paho-mqtt'], } test_deps = ['mock', 'sleekxmpp', 'emoji', 'psutil'] diff --git a/tests/test_mqtt.py b/tests/test_mqtt.py new file mode 100644 index 0000000..e2c9ce6 --- /dev/null +++ b/tests/test_mqtt.py @@ -0,0 +1,58 @@ +import json +from unittest import TestCase +from mock import patch +from ntfy.backends.mqtt import notify + +class TestMQTT(TestCase): + @patch('paho.mqtt.publish.single') + def test_basic(self, mock_publish): + notify('title', 'message', hostname='localhost', topic='test') + mock_publish.assert_called_once_with( + topic='test', + payload=json.dumps({'title': 'title', 'message': 'message', 'retcode': None}), + hostname='localhost', + port=1883, + qos=1, + auth=None + ) + + @patch('paho.mqtt.publish.single') + def test_full(self, mock_publish): + notify( + 'title', + 'message', + hostname='example.com', + topic='ntfy', + port=8883, + qos=2, + username='user', + password='pass', + retcode=0 + ) + mock_publish.assert_called_once_with( + topic='ntfy', + payload=json.dumps({'title': 'title', 'message': 'message', 'retcode': 0}), + hostname='example.com', + port=8883, + qos=2, + auth={'username': 'user', 'password': 'pass'} + ) + + @patch('paho.mqtt.publish.single') + @patch('logging.error') + def test_missing_config(self, mock_log, mock_publish): + notify('title', 'message', hostname='localhost') + mock_publish.assert_not_called() + mock_log.assert_called_once() + + mock_log.reset_mock() + notify('title', 'message', topic='test') + mock_publish.assert_not_called() + mock_log.assert_called_once() + + @patch('paho.mqtt.publish.single') + @patch('logging.error') + def test_publish_error(self, mock_log, mock_publish): + mock_publish.side_effect = Exception('MQTT Error') + notify('title', 'message', hostname='localhost', topic='test') + mock_log.assert_called_once()