218 lines
6.7 KiB
Python
218 lines
6.7 KiB
Python
"""Small MQTT compatibility layer for CPython and MicroPython.
|
|
|
|
The wrapper keeps the broker host explicit so embedded clients can point to a
|
|
real IP address instead of localhost.
|
|
"""
|
|
|
|
try:
|
|
import paho.mqtt.client as _mqtt
|
|
BACKEND_NAME = "paho"
|
|
IS_MICROPYTHON = False
|
|
except ImportError:
|
|
try:
|
|
from umqtt.simple import MQTTClient as _MQTTClient
|
|
BACKEND_NAME = "umqtt.simple"
|
|
IS_MICROPYTHON = True
|
|
except ImportError:
|
|
try:
|
|
from umqtt.robust import MQTTClient as _MQTTClient
|
|
BACKEND_NAME = "umqtt.robust"
|
|
IS_MICROPYTHON = True
|
|
except ImportError as exc:
|
|
raise ImportError("No MQTT client found. Expected paho.mqtt or umqtt.") from exc
|
|
|
|
|
|
DEFAULT_PORT = 8884
|
|
DEFAULT_CA_FILE = "orchestrateur/mqtt/certs/ca.crt"
|
|
|
|
|
|
def _resolve_host(host):
|
|
if host:
|
|
return host
|
|
|
|
try:
|
|
import os
|
|
|
|
getenv = getattr(os, "getenv", None)
|
|
if getenv is not None:
|
|
host = getenv("MQTT_BROKER_HOST")
|
|
except Exception:
|
|
host = None
|
|
|
|
if not host:
|
|
raise ValueError("MQTT broker host is required. Pass the broker IP address instead of localhost.")
|
|
|
|
return host
|
|
|
|
|
|
def _ensure_bytes(payload):
|
|
if payload is None:
|
|
return b""
|
|
if isinstance(payload, bytes):
|
|
return payload
|
|
if isinstance(payload, bytearray):
|
|
return bytes(payload)
|
|
return str(payload).encode()
|
|
|
|
|
|
def _read_file_bytes(path):
|
|
with open(path, "rb") as handle:
|
|
return handle.read()
|
|
|
|
|
|
class BrokerClient:
|
|
"""Small MQTT client with a normalised API across runtimes."""
|
|
|
|
def __init__(self, host=None, port=DEFAULT_PORT, client_id=None, use_tls=True, cafile=None, certfile=None, keyfile=None, ssl_params=None, tls_insecure=False, username=None, password=None, keepalive=60):
|
|
self.host = _resolve_host(host)
|
|
self.port = port
|
|
self.client_id = client_id
|
|
self.use_tls = use_tls
|
|
self.cafile = cafile or DEFAULT_CA_FILE
|
|
self.certfile = certfile
|
|
self.keyfile = keyfile
|
|
self.ssl_params = ssl_params
|
|
self.tls_insecure = tls_insecure
|
|
self.username = username
|
|
self.password = password
|
|
self.keepalive = keepalive
|
|
self._client = None
|
|
self._callback = None
|
|
self._messages = []
|
|
|
|
def set_callback(self, callback):
|
|
self._callback = callback
|
|
if self._client is not None and not IS_MICROPYTHON:
|
|
self._client.on_message = self._on_message
|
|
|
|
def _store_message(self, topic, payload, qos=None, retain=False):
|
|
message = {
|
|
"topic": topic,
|
|
"payload": payload,
|
|
"qos": qos,
|
|
"retain": retain,
|
|
}
|
|
self._messages.append(message)
|
|
if self._callback is not None:
|
|
self._callback(message)
|
|
|
|
def _on_message(self, client, userdata, msg):
|
|
self._store_message(msg.topic, msg.payload, getattr(msg, "qos", None), getattr(msg, "retain", False))
|
|
|
|
def open(self):
|
|
if self._client is not None:
|
|
return self._client
|
|
|
|
if IS_MICROPYTHON:
|
|
ssl_params = self.ssl_params
|
|
if self.use_tls and ssl_params is None and self.cafile is not None:
|
|
ssl_params = {"cadata": _read_file_bytes(self.cafile)}
|
|
client = _MQTTClient(
|
|
self.client_id or "smartWave-client",
|
|
self.host,
|
|
port=self.port,
|
|
user=self.username,
|
|
password=self.password,
|
|
keepalive=self.keepalive,
|
|
ssl=self.use_tls or ssl_params is not None,
|
|
ssl_params=ssl_params,
|
|
)
|
|
self._client = client
|
|
return self._client
|
|
|
|
client = _mqtt.Client(client_id=self.client_id or "", clean_session=True, protocol=4, transport="tcp")
|
|
if self.username is not None or self.password is not None:
|
|
client.username_pw_set(self.username, self.password)
|
|
if self.use_tls:
|
|
tls_kwargs = {}
|
|
if self.cafile is not None:
|
|
tls_kwargs["ca_certs"] = self.cafile
|
|
if self.certfile is not None:
|
|
tls_kwargs["certfile"] = self.certfile
|
|
if self.keyfile is not None:
|
|
tls_kwargs["keyfile"] = self.keyfile
|
|
if tls_kwargs:
|
|
client.tls_set(**tls_kwargs)
|
|
else:
|
|
client.tls_set()
|
|
if self.tls_insecure:
|
|
client.tls_insecure_set(True)
|
|
client.on_message = self._on_message
|
|
self._client = client
|
|
return self._client
|
|
|
|
def connect(self):
|
|
client = self.open()
|
|
if IS_MICROPYTHON:
|
|
client.connect()
|
|
return client
|
|
|
|
client.connect(self.host, self.port, self.keepalive)
|
|
return client
|
|
|
|
def publish(self, topic, payload, qos=2, retain=False):
|
|
client = self.open()
|
|
payload_bytes = _ensure_bytes(payload)
|
|
if IS_MICROPYTHON:
|
|
return client.publish(topic, payload_bytes, retain=retain, qos=qos)
|
|
return client.publish(topic, payload_bytes, qos=qos, retain=retain)
|
|
|
|
def subscribe(self, topic, qos=2):
|
|
client = self.open()
|
|
if IS_MICROPYTHON:
|
|
client.set_callback(self._on_micropython_message)
|
|
return client.subscribe(topic, qos=qos)
|
|
return client.subscribe(topic, qos=qos)
|
|
|
|
def _on_micropython_message(self, topic, payload):
|
|
self._store_message(topic, payload, None, False)
|
|
|
|
def poll(self, timeout=0.1):
|
|
if self._client is None:
|
|
return None
|
|
if IS_MICROPYTHON:
|
|
return self._client.check_msg()
|
|
return self._client.loop(timeout=timeout)
|
|
|
|
def wait(self):
|
|
if self._client is None:
|
|
return None
|
|
if IS_MICROPYTHON:
|
|
return self._client.wait_msg()
|
|
return self._client.loop_forever()
|
|
|
|
def get_message(self):
|
|
if not self._messages:
|
|
return None
|
|
return self._messages.pop(0)
|
|
|
|
def close(self):
|
|
if self._client is None:
|
|
return
|
|
try:
|
|
self._client.disconnect()
|
|
except Exception:
|
|
pass
|
|
self._client = None
|
|
|
|
def __enter__(self):
|
|
self.connect()
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, traceback):
|
|
self.close()
|
|
|
|
|
|
def connect(host=None, **client_kwargs):
|
|
return BrokerClient(host=host, **client_kwargs)
|
|
|
|
|
|
def publish(host, topic, payload, **client_kwargs):
|
|
qos = client_kwargs.pop("qos", 2)
|
|
retain = client_kwargs.pop("retain", False)
|
|
client = connect(host=host, **client_kwargs)
|
|
client.connect()
|
|
try:
|
|
return client.publish(topic, payload, qos=qos, retain=retain)
|
|
finally:
|
|
client.close() |