forked from platypush/platypush
254 lines
11 KiB
Python
254 lines
11 KiB
Python
import io
|
|
import json
|
|
import os
|
|
import threading
|
|
|
|
from typing import Any, Optional, IO
|
|
|
|
from platypush.config import Config
|
|
from platypush.message import Message
|
|
from platypush.plugins import Plugin, action
|
|
|
|
|
|
class MqttPlugin(Plugin):
|
|
"""
|
|
This plugin allows you to send custom message to a message queue compatible
|
|
with the MQTT protocol, see http://mqtt.org/
|
|
|
|
Requires:
|
|
|
|
* **paho-mqtt** (``pip install paho-mqtt``)
|
|
|
|
"""
|
|
|
|
def __init__(self, host=None, port=1883, tls_cafile=None,
|
|
tls_certfile=None, tls_keyfile=None,
|
|
tls_version=None, tls_ciphers=None, tls_insecure=False,
|
|
username=None, password=None, client_id=None, timeout=None, **kwargs):
|
|
"""
|
|
:param host: If set, MQTT messages will by default routed to this host unless overridden in `send_message` (default: None)
|
|
:type host: str
|
|
|
|
:param port: If a default host is set, specify the listen port (default: 1883)
|
|
:type port: int
|
|
|
|
:param tls_cafile: If a default host is set and requires TLS/SSL, specify the certificate authority file (default: None)
|
|
:type tls_cafile: str
|
|
|
|
:param tls_certfile: If a default host is set and requires TLS/SSL, specify the certificate file (default: None)
|
|
:type tls_certfile: str
|
|
|
|
:param tls_keyfile: If a default host is set and requires TLS/SSL, specify the key file (default: None)
|
|
:type tls_keyfile: str
|
|
|
|
:param tls_version: If TLS/SSL is enabled on the MQTT server and it requires a certain TLS version, specify it
|
|
here (default: None). Supported versions: ``tls`` (automatic), ``tlsv1``, ``tlsv1.1``, ``tlsv1.2``.
|
|
:type tls_version: str
|
|
|
|
:param tls_ciphers: If a default host is set and requires TLS/SSL, specify the supported ciphers (default: None)
|
|
:type tls_ciphers: str
|
|
|
|
:param tls_insecure: Set to True to ignore TLS insecure warnings (default: False).
|
|
:type tls_insecure: bool
|
|
|
|
:param username: If a default host is set and requires user authentication, specify the username ciphers (default: None)
|
|
:type username: str
|
|
|
|
:param password: If a default host is set and requires user authentication, specify the password ciphers (default: None)
|
|
:type password: str
|
|
|
|
:param client_id: ID used to identify the client on the MQTT server (default: None).
|
|
If None is specified then ``Config.get('device_id')`` will be used.
|
|
:type client_id: str
|
|
|
|
:param timeout: Client timeout in seconds (default: None).
|
|
:type timeout: int
|
|
"""
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
self.host = host
|
|
self.port = port
|
|
self.username = username
|
|
self.password = password
|
|
self.client_id = client_id or Config.get('device_id')
|
|
self.tls_cafile = self._expandpath(tls_cafile) if tls_cafile else None
|
|
self.tls_certfile = self._expandpath(tls_certfile) if tls_certfile else None
|
|
self.tls_keyfile = self._expandpath(tls_keyfile) if tls_keyfile else None
|
|
self.tls_version = self.get_tls_version(tls_version)
|
|
self.tls_insecure = tls_insecure
|
|
self.tls_ciphers = tls_ciphers
|
|
self.timeout = timeout
|
|
|
|
@staticmethod
|
|
def get_tls_version(version: Optional[str] = None):
|
|
import ssl
|
|
if not version:
|
|
return None
|
|
|
|
if type(version) == type(ssl.PROTOCOL_TLS):
|
|
return version
|
|
|
|
if isinstance(version, str):
|
|
version = version.lower()
|
|
|
|
if version == 'tls':
|
|
return ssl.PROTOCOL_TLS
|
|
if version == 'tlsv1':
|
|
return ssl.PROTOCOL_TLSv1
|
|
if version == 'tlsv1.1':
|
|
return ssl.PROTOCOL_TLSv1_1
|
|
if version == 'tlsv1.2':
|
|
return ssl.PROTOCOL_TLSv1_2
|
|
|
|
assert 'Unrecognized TLS version: {}'.format(version)
|
|
|
|
def _mqtt_args(self, **kwargs):
|
|
return {
|
|
'host': kwargs.get('host', self.host),
|
|
'port': kwargs.get('port', self.port),
|
|
'timeout': kwargs.get('timeout', self.timeout),
|
|
'tls_certfile': kwargs.get('tls_certfile', self.tls_certfile),
|
|
'tls_keyfile': kwargs.get('tls_keyfile', self.tls_keyfile),
|
|
'tls_version': kwargs.get('tls_version', self.tls_version),
|
|
'tls_ciphers': kwargs.get('tls_ciphers', self.tls_ciphers),
|
|
'username': kwargs.get('username', self.username),
|
|
'password': kwargs.get('password', self.password),
|
|
}
|
|
|
|
@staticmethod
|
|
def _expandpath(path: Optional[str] = None) -> Optional[str]:
|
|
return os.path.abspath(os.path.expanduser(path)) if path else None
|
|
|
|
def _get_client(self, tls_cafile: Optional[str] = None, tls_certfile: Optional[str] = None,
|
|
tls_keyfile: Optional[str] = None, tls_version: Optional[str] = None,
|
|
tls_ciphers: Optional[str] = None, tls_insecure: Optional[bool] = None,
|
|
username: Optional[str] = None, password: Optional[str] = None):
|
|
from paho.mqtt.client import Client
|
|
|
|
tls_cafile = self._expandpath(tls_cafile or self.tls_cafile)
|
|
tls_certfile = self._expandpath(tls_certfile or self.tls_certfile)
|
|
tls_keyfile = self._expandpath(tls_keyfile or self.tls_keyfile)
|
|
tls_ciphers = tls_ciphers or self.tls_ciphers
|
|
username = username or self.username
|
|
password = password or self.password
|
|
|
|
tls_version = tls_version or self.tls_version
|
|
if tls_version:
|
|
tls_version = self.get_tls_version(tls_version)
|
|
if tls_insecure is None:
|
|
tls_insecure = self.tls_insecure
|
|
|
|
client = Client()
|
|
|
|
if username and password:
|
|
client.username_pw_set(username, password)
|
|
if tls_cafile:
|
|
client.tls_set(ca_certs=tls_cafile, certfile=tls_certfile, keyfile=tls_keyfile,
|
|
tls_version=tls_version, ciphers=tls_ciphers)
|
|
|
|
client.tls_insecure_set(tls_insecure)
|
|
|
|
return client
|
|
|
|
@action
|
|
def publish(self, topic: str, msg: Any, host: Optional[str] = None, port: Optional[int] = None,
|
|
reply_topic: Optional[str] = None, timeout: int = 60,
|
|
tls_cafile: Optional[str] = None, tls_certfile: Optional[str] = None,
|
|
tls_keyfile: Optional[str] = None, tls_version: Optional[str] = None,
|
|
tls_ciphers: Optional[str] = None, tls_insecure: Optional[bool] = None,
|
|
username: Optional[str] = None, password: Optional[str] = None):
|
|
"""
|
|
Sends a message to a topic.
|
|
|
|
:param topic: Topic/channel where the message will be delivered
|
|
:param msg: Message to be sent. It can be a list, a dict, or a Message object.
|
|
:param host: MQTT broker hostname/IP (default: default host configured on the plugin).
|
|
:param port: MQTT broker port (default: default port configured on the plugin).
|
|
:param reply_topic: If a ``reply_topic`` is specified, then the action will wait for a response on this topic.
|
|
:param timeout: If ``reply_topic`` is set, use this parameter to specify the maximum amount of time to
|
|
wait for a response (default: 60 seconds).
|
|
:param tls_cafile: If TLS/SSL is enabled on the MQTT server and the certificate requires a certificate authority
|
|
to authenticate it, `ssl_cafile` will point to the provided ca.crt file (default: None).
|
|
:param tls_certfile: If TLS/SSL is enabled on the MQTT server and a client certificate it required, specify it
|
|
here (default: None).
|
|
:param tls_keyfile: If TLS/SSL is enabled on the MQTT server and a client certificate key it required, specify
|
|
it here (default: None).
|
|
:param tls_version: If TLS/SSL is enabled on the MQTT server and it requires a certain TLS version, specify it
|
|
here (default: None). Supported versions: ``tls`` (automatic), ``tlsv1``, ``tlsv1.1``, ``tlsv1.2``.
|
|
:param tls_insecure: Set to True to ignore TLS insecure warnings (default: False).
|
|
:param tls_ciphers: If TLS/SSL is enabled on the MQTT server and an explicit list of supported ciphers is
|
|
required, specify it here (default: None).
|
|
:param username: Specify it if the MQTT server requires authentication (default: None).
|
|
:param password: Specify it if the MQTT server requires authentication (default: None).
|
|
"""
|
|
response_buffer = io.BytesIO()
|
|
client = None
|
|
|
|
try:
|
|
# Try to parse it as a platypush message or dump it to JSON from a dict/list
|
|
if isinstance(msg, (dict, list)):
|
|
msg = json.dumps(msg)
|
|
|
|
try:
|
|
msg = Message.build(json.loads(msg))
|
|
except Exception as e:
|
|
self.logger.debug(f'Not a valid JSON: {str(e)}')
|
|
|
|
host = host or self.host
|
|
port = port or self.port or 1883
|
|
assert host, 'No host specified'
|
|
|
|
client = self._get_client(tls_cafile=tls_cafile, tls_certfile=tls_certfile, tls_keyfile=tls_keyfile,
|
|
tls_version=tls_version, tls_ciphers=tls_ciphers, tls_insecure=tls_insecure,
|
|
username=username, password=password)
|
|
|
|
client.connect(host, port, keepalive=timeout)
|
|
response_received = threading.Event()
|
|
|
|
if reply_topic:
|
|
client.on_message = self._response_callback(reply_topic=reply_topic,
|
|
event=response_received,
|
|
buffer=response_buffer)
|
|
client.subscribe(reply_topic)
|
|
|
|
client.publish(topic, str(msg))
|
|
if not reply_topic:
|
|
return
|
|
|
|
client.loop_start()
|
|
ok = response_received.wait(timeout=timeout)
|
|
if not ok:
|
|
raise TimeoutError('Response timed out')
|
|
return response_buffer.getvalue()
|
|
finally:
|
|
response_buffer.close()
|
|
|
|
if client:
|
|
try:
|
|
client.loop_stop()
|
|
except Exception as e:
|
|
self.logger.warning(f'Could not stop client loop: {e}')
|
|
|
|
client.disconnect()
|
|
|
|
@staticmethod
|
|
def _response_callback(reply_topic: str, event: threading.Event, buffer: IO[bytes]):
|
|
def on_message(client, _, msg):
|
|
if msg.topic != reply_topic:
|
|
return
|
|
|
|
buffer.write(msg.payload)
|
|
client.loop_stop()
|
|
event.set()
|
|
return on_message
|
|
|
|
@action
|
|
def send_message(self, *args, **kwargs):
|
|
"""
|
|
Alias for :meth:`platypush.plugins.mqtt.MqttPlugin.publish`.
|
|
"""
|
|
return self.publish(*args, **kwargs)
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|