#!/usr/bin/python3 # \brief sane2MQTT # \author Pascal Gollor # \copyright cc 2020 by-sa import optparse, logging, signal, time, re, argparse, json import paho.mqtt.client as mqtt import sane DEFAULT_MQTT_SERVER = "127.0.0.1" DEFAULT_MQTT_PORT = 1883 DEFAULT_TOPIC = "sane" # without leading / ## detecting kill signal # source: http://stackoverflow.com/questions/18499497/how-to-process-sigterm-signal-gracefully class GracefulKiller: def __init__(self): self.kill_now = False signal.signal(signal.SIGINT, self.exit_gracefully) signal.signal(signal.SIGTERM, self.exit_gracefully) # end __init__ def exit_gracefully(self, signum, frame): self.kill_now = True # end exit_gracefully # end class GracefulKiller # class as interface between sane and the mqtt protocol class saneMQTT(mqtt.Client): logger = None outTopic = "" inTopic = "" stateTopic = "" devices = list() device = None options = {'mode': 'Lineart', 'resolution': 300, 'source': 'Automatic Document Feeder'} def setTopics(self, inT, outT): self.outTopic = str(outT) self.inTopic = str(inT) self.stateTopic = self.outTopic + "/state" def setDevices(self, devices): self.devices = devices def on_connect(self, client, userdata, flags, rc): self.logger.debug("Connected with result code: %i", rc) self.subscribe(self.inTopic + "/#", qos=1) self.logger.debug("subscribe to: %s", self.inTopic + "/#") # commands self.message_callback_add(self.inTopic + "/set_device", self.on_setDevice) self.message_callback_add(self.inTopic + "/list_devices", self.on_listDevices) self.message_callback_add(self.inTopic + "/scan", self.on_scan) self.message_callback_add(self.inTopic + "/set_option", self.on_setOption) self.publish(self.stateTopic, payload="online", qos=1, retain=True) # last will message self.will_set(self.stateTopic, payload="offline", qos=1, retain=True) def on_disconnect(self, client, userdata, rc): msg = "Disconnected with result code: %i" if (rc): self.logger.error(msg, rc) else: self.logger.debug(msg, rc) def on_message(self, client, userdata, msg): self.logger.info("Topic: %s - Message: %s", msg.topic, msg.payload.decode()) def publishDevices(self): if (len(self.devices) < 1): return msg = json.dumps(self.devices) self.publish(self.outTopic + "/devices", payload=msg) for i in range(len(self.devices)): device = self.devices[i] msg = json.dumps({'id': i, "port": device[0], "vendor": device[1], "pid": device[2], "type": device[3]}) self.logger.debug(msg) self.publish(self.outTopic + "/device", payload=msg) def on_setDevice(self, client, userdata, msg): devID = -1 try: try: devID = int(msg.payload.decode()) except ValueError: raise RuntimeError("Unknown device ID") if (len(self.devices) == 0): raise RuntimeError("No devices available.") if (len(self.devices) < (devID + 1)): raise RuntimeError("Invalid device ID.") except RuntimeError as e: self.logger.error(e.args) self.error(e.args) return self.device = sane.open(self.devices[devID][0]) self.logger.info("using device: %s", self.device) # set optinos try: self.device.mode = self.options['mode'] self.device.resolution = self.options['resolution'] self.device.source = self.options['source'] except: self.logger.error('Cannot set device options') self.error('Cannot set device options') def on_listDevices(self, client, userdata, msg): self.publishDevices() def on_scan(self, client, userdata, msg): if (self.device == None): self.error("No scan device selected") return filepath = '/tmp/a.png' try: self.device.start() im = self.device.snap() im.save(filepath) except Exception as e: self.error(e.args) self.logger.error(e.args) return self.publish(self.outTopic + "/scan_ready", payload=filepath) def on_setOption(self, client, userdata, msg): if (self.device == None): self.error("No scan device selected") return options = json.loads(msg.payload.decode()) self.logger.debug(options) try: if ('mode' in options): self.device.mode = options['mode'].strip if ('resolution' in options): self.device.resolution = int(options['resolution']) if ('source' in options): source = str(options['source']).strip() if (source.lower == "adf"): source = 'Automatic Document Feeder' self.device.source = source except Exception as e: print(e.args) self.logger.error('Cannot set options') self.error('Cannot set options') def error(self, message): self.publish(self.outTopic + "/error", payload=str(message)) def stop(self): if (self.device != None): self.device.close() # end class saneMQTT def main(): parser = optparse.OptionParser( usage = "%prog [options]", description = "sane2MQTT controls scanner with sane via MQTT", version="%prog 0.1a" ) group = optparse.OptionGroup(parser, "MQTT settings") group.add_option("-s", "--server", dest = "server", help = "mqtt server, default %default", default = DEFAULT_MQTT_SERVER ) group.add_option("--port", dest = "port", action = "store", type = 'int', help = "mqtt server port, default %default", default = DEFAULT_MQTT_PORT ) group.add_option("-k", "--keepalive", dest = "keepalive", action = "store", type = 'int', help = "keepalive option for mqtt server, default %default", default = 60 ) group.add_option("-t", "--topic", dest = "topic", help = "topic to publish to without leading /, default %default", default = DEFAULT_TOPIC ) group.add_option("-u", "--username", dest = "username", help = "connection username", default = "" ) group.add_option("-p", "--password", dest = "password", help = "connection password", default = "" ) parser.add_option_group(group) group = optparse.OptionGroup(parser, "Basic settings") group.add_option("-l", "--loglevel", dest = "loglevel", action = "store", type = 'int', help = str(logging.CRITICAL) + ": critical " + str(logging.ERROR) + ": error " + str(logging.WARNING) + ": warning " + str(logging.INFO) + ":info " + str(logging.DEBUG) + ":debug", default = logging.ERROR ) group.add_option("-v", "--verbose", dest = "verbose", action = "store_true", help = "show debug messages (overrites loglevel to debug)", default = False ) parser.add_option_group(group) # parse options (options, _) = parser.parse_args() # mqtt topics mqttTopic = str(options.topic) while (mqttTopic.endswith("/")): mqttTopic = mqttTopic[:-1] # add infos to userdata userdata = dict() inTopic = mqttTopic + "/in" outTopic = mqttTopic # init logging loglevel = int(options.loglevel) if (options.verbose): loglevel = logging.DEBUG logger = logging.getLogger("miflora2mqtt") logger.setLevel(loglevel) ch = logging.StreamHandler() ch.setLevel(loglevel) formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') formatter.datefmt = '%Y-%m-%d %H:%M:%S' ch.setFormatter(formatter) logger.addHandler(ch) # add killer killer = GracefulKiller() # add MQTT client client = saneMQTT() # set user data client.logger = logger client.setTopics(inTopic, outTopic) # check username and password if (len(options.username) > 0): if (len(options.password) == 0): raise ValueError("please do not use username without password") client.username_pw_set(options.username, options.password) # end if # sane init ver = sane.init() logger.debug("SANE version: %s", ver) # scanner devices devices = sane.get_devices() client.setDevices(devices) logger.debug("scanner: %s", str(devices)) print(len(devices)) if (len(devices) < 1): logger.error("No devices available. Please trigger device search at runtime.") else: for i in range(len(devices)): logger.info("scanner %i: %s %s", i, devices[0][1], devices[0][2]) # end if # mqtt parameters mqttServer = str(options.server).strip() mqttPort = int(options.port) mqttKeepalive = int(options.keepalive) # debug output logger.debug("MQTT server: %s", mqttServer) logger.debug("MQTT port: %i", mqttPort) logger.debug("MQTT keepalive: %i", mqttKeepalive) logger.info("MQTT input topic: %s", inTopic) # connect to mqttclient logger.debug("connect to mqtt client") client.connect(mqttServer, mqttPort, mqttKeepalive) if (len(devices) > 0): client.publishDevices() # start client loop client.loop_start() # forever loop try: logger.debug("start program loop") while (1): time.sleep(0.1) if (killer.kill_now): raise KeyboardInterrupt # end if # end while except KeyboardInterrupt: logger.debug("exit program loop") # end try # disconnecting client.stop() client.publish(client.stateTopic, payload="offline", qos=1, retain=True) logger.debug("disconnecting from MQTT server") client.loop_stop() client.disconnect() # end main if __name__ == "__main__": try: main() except Exception as e: logging.error(str(e))