mqttwrapper 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mqttwrapper/__init__.py +1 -0
- mqttwrapper/hbmqtt_backend.py +43 -0
- mqttwrapper/paho_backend.py +64 -0
- mqttwrapper/run.py +11 -0
- mqttwrapper-0.2.0.dist-info/METADATA +193 -0
- mqttwrapper-0.2.0.dist-info/RECORD +9 -0
- mqttwrapper-0.2.0.dist-info/WHEEL +5 -0
- mqttwrapper-0.2.0.dist-info/licenses/LICENSE +21 -0
- mqttwrapper-0.2.0.dist-info/top_level.txt +1 -0
mqttwrapper/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .paho_backend import run_script
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import asyncio
|
|
4
|
+
from warnings import warn
|
|
5
|
+
|
|
6
|
+
from hbmqtt.client import MQTTClient, ClientException
|
|
7
|
+
from hbmqtt.mqtt.constants import QOS_0
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger("mqttwrapper.hbmqtt_backend")
|
|
10
|
+
|
|
11
|
+
warn("hbmqtt backend is deprecated and will be removed in the future.", DeprecationWarning)
|
|
12
|
+
log.warn("hbmqtt backend is deprecated and will be removed in the future.")
|
|
13
|
+
|
|
14
|
+
async def mqtt_loop(broker, topics, callback, context_callback, **kwargs):
|
|
15
|
+
if context_callback is not None:
|
|
16
|
+
kwargs.update(await context_callback())
|
|
17
|
+
client = MQTTClient()
|
|
18
|
+
await client.connect(broker, cleansession=True)
|
|
19
|
+
log.debug("Connected")
|
|
20
|
+
await client.subscribe([(topic, QOS_0) for topic in topics])
|
|
21
|
+
log.debug("Subscribed to %s", topics)
|
|
22
|
+
try:
|
|
23
|
+
while True:
|
|
24
|
+
message = await client.deliver_message()
|
|
25
|
+
packet = message.publish_packet
|
|
26
|
+
topic = packet.variable_header.topic_name
|
|
27
|
+
payload = bytes(packet.payload.data)
|
|
28
|
+
replies = await callback(topic, payload, **kwargs)
|
|
29
|
+
if replies is not None:
|
|
30
|
+
for reply_topic, reply_payload in replies:
|
|
31
|
+
await client.publish(reply_topic, reply_payload)
|
|
32
|
+
except ClientException:
|
|
33
|
+
log.exception("hbmqtt.client.ClientException:")
|
|
34
|
+
raise
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_script(callback, broker=None, topics=None, context_callback=None, **kwargs):
|
|
38
|
+
if not broker:
|
|
39
|
+
broker = os.environ['MQTT_BROKER']
|
|
40
|
+
if not topics:
|
|
41
|
+
topics = os.environ['MQTT_TOPICS'].split(",")
|
|
42
|
+
|
|
43
|
+
asyncio.get_event_loop().run_until_complete(mqtt_loop(broker, topics, callback, context_callback, **kwargs))
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import paho.mqtt.client as mqtt
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
log = logging.getLogger("mqttwrapper.paho_backend")
|
|
7
|
+
|
|
8
|
+
def on_connect(client, userdata, flags, rc):
|
|
9
|
+
log.debug("Connected")
|
|
10
|
+
for topic in userdata['topics']:
|
|
11
|
+
client.subscribe(topic)
|
|
12
|
+
log.debug("Subscribed to %s", topic)
|
|
13
|
+
|
|
14
|
+
def on_message(client, userdata, msg):
|
|
15
|
+
log.debug("Received message on %s", msg.topic)
|
|
16
|
+
if msg.retain and userdata['ignore_retained']:
|
|
17
|
+
log.debug("Ignoring retained message")
|
|
18
|
+
return
|
|
19
|
+
replies = None
|
|
20
|
+
try:
|
|
21
|
+
replies = userdata['callback'](msg.topic, msg.payload, **userdata['kwargs'])
|
|
22
|
+
except Exception as e:
|
|
23
|
+
log.error("Callback caused exception", exc_info = True)
|
|
24
|
+
log.debug("Callback completed.")
|
|
25
|
+
if replies is not None:
|
|
26
|
+
replies = list(replies)
|
|
27
|
+
log.debug("Received %s replies", len(replies))
|
|
28
|
+
for reply in replies:
|
|
29
|
+
try:
|
|
30
|
+
topic, payload, retain = reply
|
|
31
|
+
except ValueError:
|
|
32
|
+
topic, payload = reply
|
|
33
|
+
retain = False
|
|
34
|
+
log.debug("Publishing '%s' to %s (retain: %s)", payload, topic, retain)
|
|
35
|
+
client.publish(topic, payload, retain=retain)
|
|
36
|
+
log.debug("Published '%s' to %s (retain: %s)", payload, topic, retain)
|
|
37
|
+
|
|
38
|
+
def mqtt_thread(client):
|
|
39
|
+
client.loop_forever()
|
|
40
|
+
|
|
41
|
+
def run_script(callback, broker=None, topics=None, ignore_retained=False, blocking=True, **kwargs):
|
|
42
|
+
if not broker:
|
|
43
|
+
broker = os.environ['MQTT_BROKER']
|
|
44
|
+
if not topics:
|
|
45
|
+
topics = os.environ['MQTT_TOPICS'].split(",")
|
|
46
|
+
|
|
47
|
+
userdata = {
|
|
48
|
+
'topics': topics,
|
|
49
|
+
'callback': callback,
|
|
50
|
+
'ignore_retained': ignore_retained,
|
|
51
|
+
'kwargs': kwargs
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, userdata=userdata)
|
|
55
|
+
client.on_connect = on_connect
|
|
56
|
+
client.on_message = on_message
|
|
57
|
+
if "//" in broker: # mqtt:// prefix is optional (NB mqtts:// is not supported)
|
|
58
|
+
broker = broker.split("//")[1]
|
|
59
|
+
client.connect(broker)
|
|
60
|
+
if blocking:
|
|
61
|
+
client.loop_forever()
|
|
62
|
+
else:
|
|
63
|
+
t = threading.Thread(target=mqtt_thread, args=(client,), daemon=True)
|
|
64
|
+
t.start()
|
mqttwrapper/run.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from .paho_backend import run_script
|
|
4
|
+
|
|
5
|
+
if __name__ == "__main__":
|
|
6
|
+
try:
|
|
7
|
+
from callback import callback
|
|
8
|
+
run_script(callback)
|
|
9
|
+
except ImportError:
|
|
10
|
+
print("""Couldn't import callback function from callback module.
|
|
11
|
+
Please ensure there is a function called 'callback' defined in a file called callback.py in the current directory.""", file=sys.stderr)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mqttwrapper
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MQTT utility scripts made easy
|
|
5
|
+
Home-page: https://github.com/davea/mqttwrapper
|
|
6
|
+
Author: Dave Arter
|
|
7
|
+
Author-email: pypi@davea.me
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: mqtt
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.5
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: paho-mqtt>=2.0.0
|
|
20
|
+
Provides-Extra: hbmqtt
|
|
21
|
+
Requires-Dist: hbmqtt; extra == "hbmqtt"
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: keywords
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: provides-extra
|
|
31
|
+
Dynamic: requires-dist
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
mqttwrapper
|
|
35
|
+
===========
|
|
36
|
+
|
|
37
|
+
A little glue package to make it simple to quickly put together scripts that
|
|
38
|
+
bridge MQTT and other libraries. See examples below.
|
|
39
|
+
|
|
40
|
+
Installation
|
|
41
|
+
------------
|
|
42
|
+
|
|
43
|
+
Install from PyPI::
|
|
44
|
+
|
|
45
|
+
$ pip install mqttwrapper
|
|
46
|
+
|
|
47
|
+
By default ``paho-mqtt`` will be used as the MQTT library, but you can use
|
|
48
|
+
``hbmqtt`` if you wish, though support for this is deprecated and will be
|
|
49
|
+
removed in the future. To install, use::
|
|
50
|
+
|
|
51
|
+
$ pip install mqttwrapper[hbmqtt]
|
|
52
|
+
|
|
53
|
+
Usage
|
|
54
|
+
-----
|
|
55
|
+
|
|
56
|
+
``mqttwrapper.run_script`` handles the setup/maintenance of the MQTT connection
|
|
57
|
+
and subscriptions to topics, and calls a callback function when messages are
|
|
58
|
+
received. It can run in blocking mode where it will take care of the MQTT
|
|
59
|
+
client handling or in non blocking mode where a new thread takes care this
|
|
60
|
+
leaving your main thread free for other things.
|
|
61
|
+
|
|
62
|
+
The simplest script looks something like this::
|
|
63
|
+
|
|
64
|
+
from mqttwrapper import run_script
|
|
65
|
+
|
|
66
|
+
def callback(topic, payload):
|
|
67
|
+
print("Received payload {} on topic {}".format(payload, topic))
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
run_script(callback, broker="mqtt://127.0.0.1", topics=["my/awesome/topic1", "another/awesome/topic2"])
|
|
71
|
+
|
|
72
|
+
or in non blocking mode::
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
run_script(callback, broker="mqtt://127.0.0.1", topics=["my/awesome/topic1", "another/awesome/topic2"], blocking=False)
|
|
76
|
+
while True:
|
|
77
|
+
echo "Alive and kicking"
|
|
78
|
+
time.sleep(1)
|
|
79
|
+
|
|
80
|
+
Any extra keyword arguments passed to ``run_script`` are passed back to the
|
|
81
|
+
callback function when it's called::
|
|
82
|
+
|
|
83
|
+
from mqttwrapper import run_script
|
|
84
|
+
|
|
85
|
+
def callback(topic, payload, context, foo):
|
|
86
|
+
assert context == "hello"
|
|
87
|
+
assert foo == "bar"
|
|
88
|
+
print("Received payload {} on topic {}".format(payload, topic))
|
|
89
|
+
|
|
90
|
+
def main():
|
|
91
|
+
run_script(callback, broker="mqtt://127.0.0.1", topics=["my/awesome/topic1", "another/awesome/topic2"], context="hello", foo="bar")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
If you don't need any context passed to your callback, just the topic and
|
|
95
|
+
payload of messages, you can run in 'importless' mode by creating a `callback`
|
|
96
|
+
module which has a `callback` function defined, then execute the
|
|
97
|
+
`mqttwrapper.run` module::
|
|
98
|
+
|
|
99
|
+
$ cat callback.py
|
|
100
|
+
def callback(topic, payload):
|
|
101
|
+
print(topic, payload)
|
|
102
|
+
|
|
103
|
+
$ python -m mqttwrapper.run
|
|
104
|
+
|
|
105
|
+
Retained messages
|
|
106
|
+
-----------------
|
|
107
|
+
|
|
108
|
+
You can ignore MQTT retained messages by passing ``ignore_retained=True`` when
|
|
109
|
+
calling ``run_script``. NB this is currently not supported by the hbmqtt
|
|
110
|
+
backend.
|
|
111
|
+
|
|
112
|
+
Publishing messages from the callback
|
|
113
|
+
-------------------------------------
|
|
114
|
+
|
|
115
|
+
Sometimes your callback function might want to publish its own MQTT messages,
|
|
116
|
+
perhaps in reply to or an acknowledgement of a received message. This is
|
|
117
|
+
possible by returning an iterable of ``(topic, payload)`` tuples from the callback,
|
|
118
|
+
e.g.::
|
|
119
|
+
|
|
120
|
+
def callback(topic, payload):
|
|
121
|
+
print("Received payload {} on topic {}".format(payload, topic))
|
|
122
|
+
return [
|
|
123
|
+
("{}/ack".format(topic), payload)
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
``mqttwrapper`` will take care of publishing these messages to the broker.
|
|
128
|
+
|
|
129
|
+
These tuples can optionally take the form ``(topic, payload, True)`` if you want
|
|
130
|
+
the published message to be marked as retained.
|
|
131
|
+
|
|
132
|
+
Configuration
|
|
133
|
+
-------------
|
|
134
|
+
|
|
135
|
+
``broker`` and ``topics`` can be omitted from the ``run_script`` call and
|
|
136
|
+
environment variables used instead:
|
|
137
|
+
|
|
138
|
+
- ``MQTT_BROKER``: MQTT broker to connect to, e.g. ``mqtt://127.0.0.1/``
|
|
139
|
+
- ``MQTT_TOPICS``: Comma-separated list of topics to subscribe to, e.g. ``my/topic1,my/topic2``
|
|
140
|
+
|
|
141
|
+
asyncio/hbmqtt
|
|
142
|
+
--------------
|
|
143
|
+
|
|
144
|
+
NB This feature is deprecated and will be removed in the future.
|
|
145
|
+
|
|
146
|
+
The asyncio-powered ``hbmqtt`` MQTT library can be used instead, if you like::
|
|
147
|
+
|
|
148
|
+
from mqttwrapper.hbmqtt_backend import run_script
|
|
149
|
+
|
|
150
|
+
async def callback(topic, payload):
|
|
151
|
+
print("Received payload {} on topic {}".format(payload, topic))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
Note that your callback must be an awaitable in this case.
|
|
155
|
+
|
|
156
|
+
Your callback may require context arguments which themselves are async objects
|
|
157
|
+
or awaitables which poses a challenge: how to set these up outside of an asyncio
|
|
158
|
+
event loop before calling ``run_script``? In this case, you can pass a
|
|
159
|
+
``context_callback`` awaitable as a kwarg to ``run_script``. This is run at the
|
|
160
|
+
start of the MQTT loop, and should return a dict which will be merged into the
|
|
161
|
+
kwargs which are passed to your callback. For example::
|
|
162
|
+
|
|
163
|
+
from mqttwrapper.hbmqtt_backend import run_script
|
|
164
|
+
|
|
165
|
+
async def setup_db():
|
|
166
|
+
return {
|
|
167
|
+
"query_db": query_db
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async def query_db(value):
|
|
171
|
+
# pretend this is some slow DB query, for example.
|
|
172
|
+
await asyncio.sleep(3)
|
|
173
|
+
return value * 2
|
|
174
|
+
|
|
175
|
+
async def callback(topic, payload, query_db):
|
|
176
|
+
db_result = await query_db(int(payload))
|
|
177
|
+
print("Received payload {} on topic {}, db result: {}".format(payload, topic, db_result))
|
|
178
|
+
|
|
179
|
+
def main():
|
|
180
|
+
run_script(callback, context_callback=setup_db)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
NB hbmqtt's reconnection handling does not resubscribe to topics upon
|
|
184
|
+
reconnection, and ``mqttwrapper`` does not yet work around this.
|
|
185
|
+
|
|
186
|
+
Examples
|
|
187
|
+
--------
|
|
188
|
+
|
|
189
|
+
- rxv2mqtt_
|
|
190
|
+
- tradfri-mqtt_ (uses asyncio)
|
|
191
|
+
|
|
192
|
+
.. _rxv2mqtt: https://github.com/davea/rxv2mqtt/blob/master/main.py
|
|
193
|
+
.. _tradfri-mqtt: https://github.com/davea/tradfri-mqtt/blob/master/main.py
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
mqttwrapper/__init__.py,sha256=Hm2aRNZ--y78D7ec7IGQCHk_Zf-VE1sF4p0FnRkHokw,37
|
|
2
|
+
mqttwrapper/hbmqtt_backend.py,sha256=mmDx2pT4fNrwcvu0CQ86X00kP26KyUAcHkm9h7YZz-4,1648
|
|
3
|
+
mqttwrapper/paho_backend.py,sha256=_QKY88_j_dW3rOlHI7Xe3IeTpiPiysFJXz0znhG68CU,2183
|
|
4
|
+
mqttwrapper/run.py,sha256=0ulC7Z6B-EDUDgFx3oYncRrmNlLB2y0o7j1EDmYPmiA,386
|
|
5
|
+
mqttwrapper-0.2.0.dist-info/licenses/LICENSE,sha256=uWqObWrb6MDtetS3K8TmVXuB0w0lSKXSA4ByGm6Fy00,1067
|
|
6
|
+
mqttwrapper-0.2.0.dist-info/METADATA,sha256=pRoUv_3e-Hz7YoBh2GnwbJDyAgdWX4oOe7V_0sBIigg,6095
|
|
7
|
+
mqttwrapper-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
mqttwrapper-0.2.0.dist-info/top_level.txt,sha256=Jqxgldb4d5BWTJir7PIzTe5lYpDHx_MinoDnnECwMCw,12
|
|
9
|
+
mqttwrapper-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Dave Arter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mqttwrapper
|