#!/usr/bin/env python3 # -*- coding: utf-8 -*- # pylint: disable=line-too-long VER = '2.4.0041' """ mqtt2mysql.py - Copy MQTT topic payloads to MySQL/SQLite database Copyright (C) 2020 Norbert Richter <nr@prsolution.eu> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. Requirements: Python 3, Paho MQTT, MySQLdb: sudo apt-get install python3 python3-pip python3-mysqldb python3-configargparse python3-paho-mqtt Usage: See README.md for more details how to install and use https://github.com/curzon01/mqtt2sql#mqtt2sql """ class ExitCode: """ Program return codes """ OK = 0 MISSING_MODULE = 1 MQTT_CONNECTION_ERROR = 2 SQL_CONNECTION_ERROR = 3 def module_import_error(module): """ Print suggestion for import failures @param module: import module name """ err_str = str(module) print("{}. Try 'python -m pip install {}' to install".format(err_str, err_str.split(' ')[len(err_str.split(' '))-1])) sys.exit(9) # pylint: disable=wrong-import-position import os import sys try: import paho.mqtt.client as mqtt import time import datetime import signal import logging import configargparse import re from threading import get_ident, Thread, BoundedSemaphore from random import random except ImportError as err: module_import_error(err) try: import ssl MODULE_SSL_AVAIL = True except ImportError: MODULE_SSL_AVAIL = False try: import MySQLdb MODULE_MYSQLDB_AVAIL = True except ImportError: MODULE_MYSQLDB_AVAIL = False try: import sqlite3 MODULE_SQLITE3_AVAIL = True except ImportError: MODULE_SQLITE3_AVAIL = False # pylint: enable=wrong-import-position SCRIPTNAME = os.path.basename(sys.argv[0]) SCRIPTPID = os.getpid() EXIT_CODE = ExitCode.OK SQLTYPES = {'mysql':'MySQl', 'sqlite':'SQLite'} ARGS = {} DEFAULTS = { 'configfile': None, 'logfile': None, 'debug': None, 'verbose': None, 'mqtt-host': 'localhost', 'mqtt-port': 1883, 'mqtt-username': None, 'mqtt-password': None, 'mqtt-topic': '#', 'mqtt-cafile': None, 'mqtt-certfile': None, 'mqtt-keyfile': None, 'mqtt-insecure': False, 'mqtt-keepalive': 60, 'sql-type': 'mysql' if MODULE_MYSQLDB_AVAIL else ('sqlite' if MODULE_SQLITE3_AVAIL else None), 'sql-host': 'localhost', 'sql-port': 3306, 'sql-username': None, 'sql-password': None, 'sql-db': None, 'sql-table': 'mqtt', 'sql-max-connection': 50, 'sql-connection-retry': 10, 'sql-connection-retry-start-delay': 1, 'sql-transaction-retry': 10 } def parseargs(): """ Program argument parser @return: configargparse.parse_args() result """ # pylint: disable=global-statement global VER global DEFAULTS global MODULE_SSL_AVAIL global MODULE_MYSQLDB_AVAIL global MODULE_SQLITE3_AVAIL # pylint: enable=global-statement parser = configargparse.ArgumentParser(\ description='MQTT to MySQL bridge. Subscribes to MQTT broker topic(s) and write values into a database.', epilog="There are no required arguments, defaults are displayed using --help." ) parser.add_argument( '-c', '--configfile', metavar="<filename>", dest='configfile', is_config_file=True, default=DEFAULTS['configfile'], help="Config file, can be used instead of command parameter (default {})".format(DEFAULTS['configfile']) ) mqtt_group = parser.add_argument_group('MQTT Options') mqtt_group.add_argument( '--mqtt-host', metavar='<host>', dest='mqtt_host', default=DEFAULTS['mqtt-host'], help="host to connect to (default '{}')".format(DEFAULTS['mqtt-host']) ) mqtt_group.add_argument('--mqtthost', '--host', dest='mqtt_host', help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-port', metavar='<port>', dest='mqtt_port', type=int, default=DEFAULTS['mqtt-port'], help="port to connect to (default {})".format(DEFAULTS['mqtt-port']) ) mqtt_group.add_argument('--mqttport', '--port', dest='mqtt_port', type=int, help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-username', metavar='<username>', dest='mqtt_username', default=DEFAULTS['mqtt-username'], help="username (default {})".format(DEFAULTS['mqtt-username']) ) mqtt_group.add_argument('--mqttusername', '--username', dest='mqtt_username', help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-password', metavar='<password>', dest='mqtt_password', default=DEFAULTS['mqtt-password'], help="password (default {})".format(DEFAULTS['mqtt-password'])) mqtt_group.add_argument('--mqttpassword', '--password', dest='mqtt_password', help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-topic', metavar='<topic>', dest='mqtt_topic', nargs='*', default=DEFAULTS['mqtt-topic'], help="topic to use (default {})".format(DEFAULTS['mqtt-topic'])) mqtt_group.add_argument('--topic', dest='mqtt_topic', nargs='*', help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-cafile', metavar='<cafile>', dest='mqtt_cafile', default=DEFAULTS['mqtt-cafile'], help="cafile (default {})".format(DEFAULTS['mqtt-cafile']) if MODULE_SSL_AVAIL else configargparse.SUPPRESS) mqtt_group.add_argument('--cafile', dest='mqtt_cafile', help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-certfile', metavar='<certfile>', dest='mqtt_certfile', default=DEFAULTS['mqtt-certfile'], help="certfile (default {})".format(DEFAULTS['mqtt-certfile']) if MODULE_SSL_AVAIL else configargparse.SUPPRESS) mqtt_group.add_argument('--certfile', dest='mqtt_certfile', help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-keyfile', metavar='<keyfile>', dest='mqtt_keyfile', default=DEFAULTS['mqtt-keyfile'], help="keyfile (default {})".format(DEFAULTS['mqtt-keyfile']) if MODULE_SSL_AVAIL else configargparse.SUPPRESS) mqtt_group.add_argument('--keyfile', dest='mqtt_keyfile', help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-insecure', dest='mqtt_insecure', action='store_true', default=DEFAULTS['mqtt-insecure'], help="suppress TLS verification (default {})".format(DEFAULTS['mqtt-insecure']) if MODULE_SSL_AVAIL else configargparse.SUPPRESS) mqtt_group.add_argument('--insecure', dest='mqtt_insecure', help=configargparse.SUPPRESS) mqtt_group.add_argument( '--mqtt-keepalive', metavar='<sec>', dest='mqtt_keepalive', type=int, default=DEFAULTS['mqtt-keepalive'], help="keepalive timeout for the client (default {})".format(DEFAULTS['mqtt-keepalive'])) mqtt_group.add_argument('--keepalive', dest='mqtt_keepalive', type=int, help=configargparse.SUPPRESS) sql_group = parser.add_argument_group('SQL Options') sql_choices = [] if MODULE_MYSQLDB_AVAIL: sql_choices.append('mysql') if MODULE_SQLITE3_AVAIL: sql_choices.append('sqlite') if len(sql_choices) == 0: SignalHandler.exitus(ExitCode.MISSING_MODULE, 'Either module MySQLdb or sqlite must be installed') sql_group.add_argument( '--sql-type', metavar='<type>', dest='sql_type', choices=sql_choices, default=DEFAULTS['sql-type'], help="server type {} (default '{}')".format(sql_choices, DEFAULTS['sql-type'])) sql_group.add_argument('--sqltype', dest='sql_type', choices=sql_choices, help=configargparse.SUPPRESS) sql_group.add_argument( '--sql-host', metavar='<hostname>|<socket>', dest='sql_host', default=DEFAULTS['sql-host'], help="hostname or unix socket to connect (default '{}')".format(DEFAULTS['sql-host'])) sql_group.add_argument('--sqlhost', dest='sql_host', help=configargparse.SUPPRESS) sql_group.add_argument( '--sql-port', metavar='<port>', dest='sql_port', type=int, default=DEFAULTS['sql-port'], help="port to connect (default {}, ignored when unix socket is defined)".format(DEFAULTS['sql-port'])) sql_group.add_argument('--sqlport', dest='sql_port', type=int, help=configargparse.SUPPRESS) sql_group.add_argument( '--sql-username', metavar='<username>', dest='sql_username', default=DEFAULTS['sql-username'], help="username (default {})".format(DEFAULTS['sql-username'])) sql_group.add_argument('--sqlusername', dest='sql_username', help=configargparse.SUPPRESS) sql_group.add_argument( '--sql-password', metavar='<password>', dest='sql_password', default=DEFAULTS['sql-password'], help="password (default {})".format(DEFAULTS['sql-password'])) sql_group.add_argument('--sqlpassword', dest='sql_password', help=configargparse.SUPPRESS) sql_group.add_argument( '--sql-db', metavar='<db>', dest='sql_db', default=DEFAULTS['sql-db'], help="database to use (default '{}')".format(DEFAULTS['sql-db'])) sql_group.add_argument('--sqldb', dest='sql_db', help=configargparse.SUPPRESS) sql_group.add_argument( '--sql-table', metavar='<table>', dest='sql_table', default=DEFAULTS['sql-table'], help="table to use (default '{}')".format(DEFAULTS['sql-table'])) sql_group.add_argument('--sqltable', dest='sql_table', help=configargparse.SUPPRESS) sql_group.add_argument( '--sql-max-connection', metavar='<num>', dest='sql_max_connection', type=int, default=DEFAULTS['sql-max-connection'], help="maximum number of simultaneous connections (default {})".format(DEFAULTS['sql-max-connection'])) sql_group.add_argument('--sqlmaxconnection', dest='sql_max_connection', type=int, help=configargparse.SUPPRESS) sql_group.add_argument( '--sql-connection-retry', metavar='<num>', dest='sql_connection_retry', type=int, default=DEFAULTS['sql-connection-retry'], help="maximum number of SQL connection retries on error (default {})".format(DEFAULTS['sql-connection-retry'])) sql_group.add_argument( '--sql-connection-retry-start-delay', metavar='<sec>', dest='sql_connection_retry_start_delay', type=float, default=DEFAULTS['sql-connection-retry-start-delay'], help="start delay between SQL reconnect retry (default {}), is doubled after each occurrence".format(DEFAULTS['sql-connection-retry-start-delay'])) sql_group.add_argument( '--sql-transaction-retry', metavar='<num>', dest='sql_transaction_retry', type=int, default=DEFAULTS['sql-transaction-retry'], help="maximum number of SQL transaction retry on error (default {})".format(DEFAULTS['sql-transaction-retry'])) logging_group = parser.add_argument_group('Informational') logging_group.add_argument( '-l', '--logfile', metavar='<filename>', dest='logfile', default=DEFAULTS['logfile'], help="optional logfile (default {}".format(DEFAULTS['logfile'])) logging_group.add_argument( '-d', '--debug', dest='debug', action='count', help='debug output') logging_group.add_argument( '-v', '--verbose', dest='verbose', action='count', help='verbose output') logging_group.add_argument( '-V', '--version', action='version', version='%(prog)s v'+VER) return parser.parse_args() def verbose_print(_args): """ Verbose args @param _args: result from configargparse.parse_args() """ log(1, ' MQTT server: {}:{} {}{} keepalive {}'.format(_args.mqtt_host, _args.mqtt_port, 'SSL' if (_args.mqtt_cafile is not None) else '', ' (suppress TLS verification)' if _args.mqtt_insecure else '', _args.mqtt_keepalive)) log(1, ' user: {}'.format(_args.mqtt_username)) log(1, ' topics: {}'.format(_args.mqtt_topic)) log(1, ' SQL type: {}'.format(SQLTYPES[_args.sql_type])) if issocket(_args.sql_host): log(1, ' server: {} [max {} connections]'.format(_args.sql_host, _args.sql_max_connection)) else: log(1, ' server: {}:{} [max {} connections]'.format(_args.sql_host, _args.sql_port, _args.sql_max_connection)) log(1, ' db: {}'.format(_args.sql_db)) log(1, ' table: {}'.format(_args.sql_table)) log(1, ' user: {}'.format(_args.sql_username)) if _args.logfile is not None: log(1, ' Log file: {}'.format(_args.logfile)) if debug_level() > 0: log(1, ' Debug level: {}'.format(debug_level())) log(1, ' Verbose level: {}'.format(verbose_level())) def verbose_level(): """ return verbose level (0 if disabled) """ global ARGS # pylint: disable=global-statement if ARGS.verbose is not None and ARGS.verbose > 0: return ARGS.verbose return 0 def debug_level(): """ return debug level (0 if disabled) """ global ARGS # pylint: disable=global-statement if ARGS.debug is not None and ARGS.debug > 0: return ARGS.debug return 0 def log(loglevel, msg): """ Writes a message to stdout and optional logfile if given loglevel is >= verbose_level() @param msg: message to output """ if verbose_level() >= loglevel: global ARGS # pylint: disable=global-statement strtime = str(time.strftime("%Y-%m-%d %H:%M:%S")) # print strtime+': '+msg if ARGS.logfile is not None: filename = str(time.strftime(ARGS.logfile, time.localtime())) logfile = open(filename, "a") logfile.write(strtime+': '+msg+'\n') logfile.close() print(strtime+': '+msg) def debuglog(dbglevel, msg): """ Writes a message to stdout and optional logfile if given dbglevel is >= debug_level() @param dbglevel: debug level used e.g. if -d is given one time and dbglevel is 1, then msg is output if -d is given one time and dbglevel is 2, then msg will not output if -d is given two times and dbglevel is 2, then msg will output @param msg: message to output """ if debug_level() > dbglevel: log(0, msg) def issocket(name): """ Simple check if name is a socket name @param name: name to check return True if name is a socket name """ nametype = re.search(r'^(\/\S*)*', name) return nametype is not None and nametype.group(0) == name class Mqtt2Sql: """ MQTT to SQL handling class """ def __init__(self, args_): self._args = args_ self.write2sql_thread = None self.pool_sqlconnections = BoundedSemaphore(value=self._args.sql_max_connection) self.userdata = { 'haveresponse' : False, 'starttime' : time.time() } self.mqttc, ret = self.mqtt_connect( host=self._args.mqtt_host, port=self._args.mqtt_port, username=self._args.mqtt_username, password=self._args.mqtt_password, keepalive=self._args.mqtt_keepalive, cafile=self._args.mqtt_cafile, certfile=self._args.mqtt_certfile, keyfile=self._args.mqtt_keyfile, insecure=self._args.mqtt_insecure, userdata=self.userdata ) if ret != ExitCode.OK: SignalHandler.exitus(ret, '{}:{} failed - [{}] {}'.format(self._args.mqtt_host, self._args.mqtt_port, ret, mqtt.error_string(ret))) def write2sql(self, message): """ Called as thread from mqtt backlog handler when a message has been received on a topic that the client subscribes to. @param message: an instance of MQTTMessage. This is a class with members topic, payload, qos, retain. """ def sql_execute_exception(retry_condition, error_str): """ handling local SQL exceptions @param retry_condition: condition for retry transaction if True delay process and return if False rollback transaction and return @param error_str: error string to output """ global SQLTYPES # pylint: disable=global-statement nonlocal db_connection, sql, transaction_retry typestr = SQLTYPES[self._args.sql_type] transaction_delay = random() transaction_delay *= 2 debuglog(1, "[{}]: {} transaction ERROR: {}, retry={}, delay={}".format(get_ident(), typestr, error_str, transaction_retry, transaction_delay)) if retry_condition: transaction_retry -= 1 log(1, "SQL transaction NOTE: {} - try retry".format(err)) time.sleep(transaction_delay) else: log(0, "{} transaction ERROR {}".format(typestr, error_str)) log(1, "{} give up: {}".format(typestr, sql)) # Rollback in case there is any error db_connection.rollback() transaction_retry = 0 # pylint: disable=global-statement global EXIT_CODE global SQLTYPES # pylint: enable=global-statement if EXIT_CODE != ExitCode.OK: sys.exit(0) timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") connection_retry = self._args.sql_connection_retry connection_delay = self._args.sql_connection_retry_start_delay connection_delay_base = self._args.sql_connection_retry_start_delay debuglog(3, "SQL type is '{}'".format(SQLTYPES[self._args.sql_type])) db_connection = None while connection_retry > 0: if EXIT_CODE != ExitCode.OK: sys.exit(0) try: if self._args.sql_type == 'mysql': connection = {'db': self._args.sql_db} if issocket(self._args.sql_host): connection['unix_socket'] = self._args.sql_host else: connection['host'] = self._args.sql_host if self._args.sql_port is not None: connection['port'] = self._args.sql_port if self._args.sql_username is not None: connection['user'] = self._args.sql_username if self._args.sql_password is not None: connection['passwd'] = self._args.sql_password db_connection = MySQLdb.connect(**connection) elif self._args.sql_type == 'sqlite': db_connection = sqlite3.connect(self._args.sql_db) connection_retry = 0 except Exception as err: # pylint: disable=broad-except connection_retry -= 1 try: error_code = err.args[0] except: # pylint: disable=bare-except error_code = 0 if error_code == 2005: connection_retry = 0 debuglog(1, "[{}]: SQL connection ERROR: {}, retry={}, delay={}".format(get_ident(), SQLTYPES[self._args.sql_type], connection_retry, connection_delay)) if connection_retry > 0: log(1, "SQL connection NOTE: {} - try retry".format(err)) time.sleep(connection_delay) connection_delay += connection_delay_base else: log(1, "SQL connection ERROR: {} - give up".format(err)) os.kill(os.getpid(), signal.SIGTERM) SignalHandler.exitus(ExitCode.SQL_CONNECTION_ERROR, "SQL connection ERROR: {} - give up".format(err)) transaction_retry = self._args.sql_transaction_retry while transaction_retry > 0: if EXIT_CODE != ExitCode.OK: sys.exit(0) cursor = db_connection.cursor() try: # INSERT/UPDATE record payload = message.payload if not isinstance(payload, str): payload = str(payload, 'utf-8', errors='ignore') if self._args.sql_type == 'mysql': sql = "INSERT INTO `{0}` \ SET `ts`='{1}',`topic`='{2}',`value`='{3}',`qos`='{4}',`retain`='{5}' \ ON DUPLICATE KEY UPDATE `ts`='{1}',`value`='{3}',`qos`='{4}',`retain`='{5}'"\ .format( self._args.sql_table, timestamp, message.topic, payload, message.qos, message.retain ) debuglog(4, "SQL exec: '{}'".format(sql)) cursor.execute(sql) elif self._args.sql_type == 'sqlite': # strtime=str(time.strftime("%Y-%m-%d %H:%M:%S")) sql1 = "INSERT OR IGNORE INTO `{0}` \ (ts,topic,value,qos,retain) \ VALUES('{1}','{2}','{3}','{4}','{5}')"\ .format( self._args.sql_table, timestamp, message.topic, payload, message.qos, message.retain ) sql2 = "UPDATE `{0}` \ SET ts='{1}', \ value='{3}', \ qos='{4}', \ retain='{5}' \ WHERE topic='{2}'"\ .format( self._args.sql_table, timestamp, message.topic, payload, message.qos, message.retain ) sql = sql1 + '\n' + sql2 debuglog(4, "SQL exec: '{}'".format(sql)) try: cursor.execute(sql1) cursor.execute(sql2) except sqlite3.OperationalError as err: EXIT_CODE = ExitCode.SQL_CONNECTION_ERROR sys.exit(EXIT_CODE) db_connection.commit() debuglog(1, "[{}]: SQL success: table='{}', topic='{}', value='{}', qos='{}', retain='{}'".format(get_ident(), self._args.sql_table, message.topic, payload, message.qos, message.retain)) transaction_retry = 0 except MySQLdb.Error as err: # pylint: disable=no-member sql_execute_exception( err.args[0] in [1040, 1205, 1213], "[{}]: {}".format(err.args[0], err.args[1]) ) except sqlite3.Error as err: sql_execute_exception( "database is locked" in str(err).lower(), err ) finally: cursor.close() db_connection.close() self.pool_sqlconnections.release() def on_connect(self, client, userdata, message, return_code): """ Called when the broker responds to our connection request. @param client: the client instance for this callback @param userdata: the private user data as set in Client() or userdata_set() @param message: response message sent by the broker @param return_code: the connection result """ debuglog(1, "MQTT on_connect({},{},{},{}): {}".format(client, userdata, message, return_code, mqtt.error_string(return_code))) for topic in self._args.mqtt_topic: debuglog(1, "subscribe to topic {}".format(topic)) client.subscribe(topic, 0) def on_message(self, client, userdata, message): """ Called when a message has been received on a topic that the client subscribes to. This callback will be called for every message received. @param client: the client instance for this callback @param userdata: the private user data as set in Client() or userdata_set() @param message: an instance of MQTTMessage. This is a class with members topic, payload, qos, retain. """ global EXIT_CODE # pylint: disable=global-statement if EXIT_CODE != ExitCode.OK: sys.exit(EXIT_CODE) log(2, '{} {} [QOS {} Retain {}]'.format(message.topic, message.payload, message.qos, message.retain)) debuglog(2, "on_message({},{},{})".format(client, userdata, message)) if EXIT_CODE == ExitCode.OK: self.pool_sqlconnections.acquire() self.write2sql_thread = Thread(target=self.write2sql, args=(message,)) self.write2sql_thread.start() def on_publish(self, client, userdata, mid): """ Called when a message that was to be sent using the publish() call has completed transmission to the broker. For messages with QoS levels 1 and 2, this means that the appropriate handshakes have completed. For QoS 0, this simply means that the message has left the client. The mid variable matches the mid variable returned from the corresponding publish() call, to allow outgoing messages to be tracked. @param client: the client instance for this callback @param userdata: the private user data as set in Client() or userdata_set() @param mid: matches the mid variable returned from the corresponding publish() call, to allow outgoing messages to be tracked. """ debuglog(2, "on_publish({},{},{})".format(client, userdata, mid)) def on_subscribe(self, client, userdata, mid, granted_qos): """ Called when the broker responds to a subscribe request. @param client: the client instance for this callback @param userdata: the private user data as set in Client() or userdata_set() @param mid: Matches the mid variable returned from the corresponding subscribe() call. @param granted_qos: a list of integers that give the QoS level the broker has granted for each of the different subscription requests. """ debuglog(2, "on_subscribe({},{},{},{})".format(client, userdata, mid, granted_qos)) def on_log(self, client, userdata, level, string): """ Called when the client has log information. @param client: the client instance for this callback @param userdata: the private user data as set in Client() or userdata_set() @param level: gives the severity of the message and will be one of MQTT_LOG_INFO, MQTT_LOG_NOTICE, MQTT_LOG_WARNING, MQTT_LOG_ERR, and MQTT_LOG_DEBUG. The message itself is in string. @param string: The message itself """ debuglog(2, "on_log({},{},{},{})".format(client, userdata, level, string)) def mqtt_connect(self, host, port, username=None, password=None, keepalive=60, cafile=None, certfile=None, keyfile=None, insecure=True, userdata=None): """ Create MQTT client and set callback handler @param host: the hostname or IP address of the remote broker @param port: the network port of the server host to connect to @param username: username for broker authentication @param password: password for broker authentication @param keepalive: maximum period in seconds allowed between communications with the broker @param cafile: a string path to the Certificate Authority certificate files that are to be treated as trusted by this client @param certfile: strings pointing to the PEM encoded client certificate @param keyfile: strings pointing to the PEM encoded private keys @param insecure: verification of the server hostname in the server certificate if True @param userdata: private user data that will be passed to callbacks @returns mqttc: MQTT client handle (or None on error) ret: Error code """ mqttc = mqtt.Client('{}-{:d}'.format(SCRIPTNAME, os.getpid()), clean_session=True, userdata=userdata, protocol=mqtt.MQTTv31) if debug_level() > 0: if debug_level() <= 2: logging.basicConfig(level=logging.WARNING) elif debug_level() <= 3: logging.basicConfig(level=logging.INFO) elif debug_level() >= 4: logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) mqttc.enable_logger(logger) mqttc.on_connect = self.on_connect mqttc.on_message = self.on_message mqttc.on_publish = self.on_publish mqttc.on_subscribe = self.on_subscribe if debug_level() > 0: mqttc.on_log = self.on_log # cafile controls TLS usage if cafile is not None: if certfile is not None and keyfile is not None: mqttc.tls_set(cafile, certfile=certfile, keyfile=keyfile, cert_reqs=ssl.CERT_REQUIRED) else: mqttc.tls_set(cafile, cert_reqs=ssl.CERT_REQUIRED) mqttc.tls_insecure_set(insecure) # username & password may be None if username is not None: mqttc.username_pw_set(username, password) debuglog(1, "mqttc.connect({}, {}, {})".format(host, port, keepalive)) try: res = mqttc.connect(host, port, keepalive) debuglog(1, "mqttc.connect() returns {}".format(res)) except Exception as err: # pylint: disable=broad-except,unused-variable return None, ExitCode.MQTT_CONNECTION_ERROR return mqttc, ExitCode.OK def loop_forever(self): """ Main MQTT to SQL loop does not return until an error occurs """ global EXIT_CODE # pylint: disable=global-statement while True: # Main loop as long as no error occurs ret = mqtt.MQTT_ERR_SUCCESS while not self.userdata['haveresponse'] and ret == mqtt.MQTT_ERR_SUCCESS: try: ret = self.mqttc.loop() except Exception as err: # pylint: disable=broad-except log(0, 'ERROR: loop() - {}'.format(err)) time.sleep(0.1) if EXIT_CODE != ExitCode.OK: sys.exit(EXIT_CODE) if ret not in ( mqtt.MQTT_ERR_AGAIN, mqtt.MQTT_ERR_PROTOCOL, mqtt.MQTT_ERR_INVAL, mqtt.MQTT_ERR_NO_CONN, mqtt.MQTT_ERR_CONN_REFUSED, mqtt.MQTT_ERR_NOT_FOUND, mqtt.MQTT_ERR_TLS, mqtt.MQTT_ERR_PAYLOAD_SIZE, mqtt.MQTT_ERR_NOT_SUPPORTED, mqtt.MQTT_ERR_AUTH, mqtt.MQTT_ERR_ERRNO): # disconnect from server log(0, 'Remote disconnected from MQTT - [{}] {})'.format(ret, mqtt.error_string(ret))) try: ret = self.mqttc.reconnect() log(0, 'MQTT reconnected - [{}] {})'.format(ret, mqtt.error_string(ret))) except Exception as err: # pylint: disable=broad-except SignalHandler.exitus(ExitCode.MQTT_CONNECTION_ERROR, '{}:{} failed - [{}] {}'.format(self._args.mqtt_host, self._args.mqtt_port, ret, mqtt.error_string(err))) else: SignalHandler.exitus(ExitCode.MQTT_CONNECTION_ERROR, '{}:{} failed: - [{}] {}'.format(self._args.mqtt_host, self._args.mqtt_port, ret, mqtt.error_string(ret))) class SignalHandler: """ Signal Handler Class """ def __init__(self): signal.signal(signal.SIGINT, self._exitus) signal.signal(signal.SIGTERM, self._exitus) def _signalname(self, signal_): signals_ = dict((k, v) for v, k in reversed(sorted(signal.__dict__.items())) if v.startswith('SIG') and not v.startswith('SIG_')) return signals_.get(signal_, 'UNKNOWN') def _exitus(self, signal_, frame_): # pylint: disable=unused-argument """ local exit with logging """ self.exitus(signal_, 'signal {}#{}'.format(self._signalname(signal_), signal_)) def exitus(self, status=0, message="end"): """ Called when the program should be exit @param status: the exit status program returns to callert @param message: the message logged before exit """ # pylint: disable=global-statement global SCRIPTNAME global SCRIPTPID global VER # pylint: enable=global-statement if message is not None: log(1, message) log(0, '{}[{}] v{} end'.format(SCRIPTNAME, SCRIPTPID, VER)) if status in (signal.SIGINT, signal.SIGTERM): status = 0 sys.exit(status) if __name__ == "__main__": # set signal handler SIG = SignalHandler() # Parse command line arguments ARGS = parseargs() # Log program start log(0, '{}[{}] v{} start'.format(SCRIPTNAME, SCRIPTPID, VER)) # print possible verbose info verbose_print(ARGS) # Create class MQTT2SQL = Mqtt2Sql(ARGS) # run loop MQTT2SQL.loop_forever()