├── .gitignore ├── README.md ├── mqtt-gpio-monitor.ini.example └── mqtt-gpio-monitor.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | mqtt-gpio-monitor.ini 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### THIS REPOSITORY IS NO LONGER SUPPORTED OR MAINTAINED ### 2 | 3 | 4 | mqtt-gpio-monitor 5 | ================= 6 | 7 | Python 3 script for sending/receiving commands to/from GPIO pins via MQTT messages. 8 | 9 | This was written for use on a RaspberryPi, with either the PiFace extension board, or just raw access to the GPIO pins. 10 | 11 | The example INI file contains the only configuration required. You must define the module to use, either GPIO or PFIO. Depending on which you will need to ensure the appropriate Python module is installed. 12 | 13 | If using the PiFace extension board you will need to follow the instructions [here](http://piface.github.io/pifacedigitalio/installation.html) to install the digital IO libraries; 14 | 15 | sudo apt-get install python3-pifacedigitalio 16 | 17 | If just using the raw GPIO pins then the RPi.GPIO module should be installed as part of Raspbian. 18 | 19 | You will also need an MQTT client, in this case [Paho](https://pypi.python.org/pypi/paho-mqtt/0.9). To install; 20 | 21 | sudo pip3 install paho-mqtt 22 | 23 | On Rapsbian Stretch, the kernel's SPI driver changed the default serial speed from 500Khz to 125Mhz. The pifacedigitalio SPI doesn't initialize the SPI speed or ioctl() option so we have to to edit the spi.py file manually. 24 | 25 | sudo nano /usr/lib/python3/dist-packages/pifacecommon/spi.py 26 | 27 | Change the spi transfer struct section to match the following. 28 | 29 | # create the spi transfer struct 30 | transfer = spi_ioc_transfer( 31 | tx_buf=ctypes.addressof(wbuffer), 32 | rx_buf=ctypes.addressof(rbuffer), 33 | len=ctypes.sizeof(wbuffer), 34 | speed_hz=ctypes.c_uint32(15000) 35 | ) 36 | 37 | You should now be ready to run the script. It will listen for incoming messages on {topic}/in/+ (where {topic} is specified in the INI file). The incoming messages need to arrive on {topic}/in/{pin} with a value of either 1 or 0. 38 | 39 | E.g. a message arriving on {topic}/in/3 with value 1 will set pin 3 to HIGH. 40 | 41 | Depending on what is set for MONITOR_PINS in the INI file, the script will also monitor these pins and if there are any changes publish a message on {topic}/out/{pin} with a value of either 1 or 0. 42 | 43 | E.g. if pin 7 changes from 1 to 0 a message would be published to {topic}/out/7 with a value of 1. 44 | 45 | So you are able to both monitor pins as well as set pins HIGH/LOW. Obviously you can't do both monitor and update for the same pin. 46 | 47 | Control the pull up resistors using MONITOR_PINS_PUD set to either UP, DOWN or nothing. 48 | 49 | Set the pin numbering to either BOARD or BCM (broadcom) using MONITOR_PIN_NUMBERING. 50 | 51 | Invert the output of pins using MONITOR_OUT_INVERT. i.e. high is read as 0 instead of 1 and vice-versa. 52 | 53 | The last option in the INI file is MONITOR_REFRESH, which is a special topic the script will subscribe to if specified. Any message arriving on that topic will trigger the script to re-send publishes with the current state of all monitored pins. This is useful for requesting the current state of all pins if the calling system is restarted for example. 54 | -------------------------------------------------------------------------------- /mqtt-gpio-monitor.ini.example: -------------------------------------------------------------------------------- 1 | [global] 2 | MODULE = 3 | DEBUG = True 4 | 5 | MQTT_HOST = localhost 6 | MQTT_PORT = 1883 7 | MQTT_CLIENT_ID = mqtt-gpio-monitor 8 | MQTT_QOS = 2 9 | MQTT_RETAIN = False 10 | MQTT_CLEAN_SESSION = True 11 | MQTT_TOPIC = mqtt-gpio-monitor 12 | MQTT_LWT = clients/mqtt-gpio-monitor 13 | 14 | # authentication (optional) 15 | MQTT_USERNAME = 16 | MQTT_PASSWORD = 17 | 18 | # tls protocol (optional) [one of tlsv1_2, tlsv1_1, tlsv1, sslv3] 19 | MQTT_TLS_PROTOCOL = 20 | # tls_insecure and cert_path mandatory if tls_protocol specified 21 | MQTT_TLS_INSECURE = False 22 | MQTT_CERT_PATH = 23 | 24 | MONITOR_PINS = 1, 2, 3, 4 25 | MONITOR_PINS_PUD = UP 26 | MONITOR_PIN_NUMBERING = BOARD 27 | MONITOR_OUT_INVERT = False 28 | MONITOR_POLL = 0.1 29 | MONITOR_REFRESH = mqtt-gpio-monitor/refresh 30 | -------------------------------------------------------------------------------- /mqtt-gpio-monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | __author__ = "Ben Jones" 4 | __copyright__ = "Copyright (C) Ben Jones" 5 | 6 | import logging 7 | import os 8 | import signal 9 | import socket 10 | import sys 11 | import time 12 | import ssl 13 | import configparser 14 | import paho.mqtt.client as mqtt 15 | 16 | PFIO_MODULE = False 17 | GPIO_MODULE = False 18 | GPIO_OUTPUT_PINS = [] 19 | 20 | # Script name (without extension) used for config/logfile names 21 | APPNAME = os.path.splitext(os.path.basename(__file__))[0] 22 | INIFILE = os.getenv('INIFILE', APPNAME + '.ini') 23 | LOGFILE = os.getenv('LOGFILE', APPNAME + '.log') 24 | 25 | # Read the config file 26 | config = configparser.ConfigParser() 27 | config.read(INIFILE) 28 | 29 | # Use ConfigParser to pick out the settings 30 | MODULE = config.get("global", "module") 31 | DEBUG = config.getboolean("global", "debug") 32 | 33 | MQTT_HOST = config.get("global", "mqtt_host") 34 | MQTT_PORT = config.getint("global", "mqtt_port") 35 | MQTT_CLIENT_ID = config.get("global", "mqtt_client_id") 36 | MQTT_QOS = config.getint("global", "mqtt_qos") 37 | MQTT_RETAIN = config.getboolean("global", "mqtt_retain") 38 | MQTT_CLEAN_SESSION = config.getboolean("global", "mqtt_clean_session") 39 | MQTT_TOPIC = config.get("global", "mqtt_topic") 40 | MQTT_LWT = config.get("global", "mqtt_lwt") 41 | 42 | MQTT_USERNAME = config.get("global", "mqtt_username", fallback=None) 43 | MQTT_PASSWORD = config.get("global", "mqtt_password", fallback=None) 44 | 45 | MQTT_TLS_PROTOCOL = config.get("global", "mqtt_tls_protocol", fallback=None) 46 | MQTT_TLS_INSECURE = config.get("global", "mqtt_tls_insecure", fallback=False) 47 | MQTT_CERT_PATH = config.get("global", "mqtt_cert_path", fallback=None) 48 | 49 | MONITOR_PINS = config.get("global", "monitor_pins", raw=True) 50 | MONITOR_PINS_PUD = config.get("global", "monitor_pins_pud") # UP, DOWN or unset 51 | MONITOR_PIN_NUMBERING = config.get("global", "monitor_pin_numbering") # BCM or BOARD 52 | MONITOR_OUT_INVERT = config.getboolean("global", "monitor_out_invert") 53 | MONITOR_POLL = config.getfloat("global", "monitor_poll") 54 | MONITOR_REFRESH = config.get("global", "monitor_refresh") 55 | 56 | # Initialise logging 57 | LOGFORMAT = '%(asctime)-15s %(levelname)-5s %(message)s' 58 | 59 | if DEBUG: 60 | logging.basicConfig(filename=LOGFILE, 61 | level=logging.DEBUG, 62 | format=LOGFORMAT) 63 | else: 64 | logging.basicConfig(filename=LOGFILE, 65 | level=logging.INFO, 66 | format=LOGFORMAT) 67 | 68 | logging.info("Starting " + APPNAME) 69 | logging.info("INFO MODE") 70 | logging.debug("DEBUG MODE") 71 | logging.debug("INIFILE = %s" % INIFILE) 72 | logging.debug("LOGFILE = %s" % LOGFILE) 73 | 74 | # Check we have the necessary module 75 | if MODULE.lower() == "pfio": 76 | try: 77 | import pifacedigitalio as PFIO 78 | logging.info("PiFace.PFIO module detected...") 79 | PFIO_MODULE = True 80 | except ImportError: 81 | logging.error("Module = %s in %s but PiFace.PFIO module was not found" % (MODULE, INIFILE)) 82 | sys.exit(2) 83 | 84 | if MODULE.lower() == "gpio": 85 | try: 86 | import RPi.GPIO as GPIO 87 | logging.info("RPi.GPIO module detected...") 88 | GPIO_MODULE = True 89 | except ImportError: 90 | logging.error("Module = %s in %s but RPi.GPIO module was not found" % (MODULE, INIFILE)) 91 | sys.exit(2) 92 | 93 | # Convert the list of strings to a list of ints. 94 | # Also strips any whitespace padding 95 | PINS = [] 96 | if MONITOR_PINS: 97 | PINS.extend(list(map(int, MONITOR_PINS.split(",")))) 98 | 99 | if len(PINS) == 0: 100 | logging.debug("Not monitoring any pins") 101 | else: 102 | logging.debug("Monitoring pins %s" % PINS) 103 | 104 | # Append a column to the list of PINS. This will be used to store state. 105 | for PIN in PINS: 106 | PINS[PINS.index(PIN)] = [PIN, -1] 107 | 108 | MQTT_TOPIC_IN = MQTT_TOPIC + "/in/+" 109 | MQTT_TOPIC_OUT = MQTT_TOPIC + "/out/%d" 110 | 111 | # Create the MQTT client 112 | if not MQTT_CLIENT_ID: 113 | MQTT_CLIENT_ID = APPNAME + "_%d" % os.getpid() 114 | MQTT_CLEAN_SESSION = True 115 | 116 | mqttc = mqtt.Client(MQTT_CLIENT_ID, clean_session=MQTT_CLEAN_SESSION) 117 | 118 | # MQTT callbacks 119 | def on_connect(mosq, obj, flags, result_code): 120 | """ 121 | Handle connections (or failures) to the broker. 122 | This is called after the client has received a CONNACK message 123 | from the broker in response to calling connect(). 124 | The parameter rc is an integer giving the return code: 125 | 126 | 0: Success 127 | 1: Refused . unacceptable protocol version 128 | 2: Refused . identifier rejected 129 | 3: Refused . server unavailable 130 | 4: Refused . bad user name or password (MQTT v3.1 broker only) 131 | 5: Refused . not authorised (MQTT v3.1 broker only) 132 | """ 133 | if result_code == 0: 134 | logging.info("Connected to %s:%s" % (MQTT_HOST, MQTT_PORT)) 135 | 136 | # Subscribe to our incoming topic 137 | mqttc.subscribe(MQTT_TOPIC_IN, qos=MQTT_QOS) 138 | 139 | # Subscribe to the monitor refesh topic if required 140 | if MONITOR_REFRESH: 141 | mqttc.subscribe(MONITOR_REFRESH, qos=0) 142 | 143 | # Publish retained LWT as per http://stackoverflow.com/questions/19057835/how-to-find-connected-mqtt-client-details/19071979#19071979 144 | # See also the will_set function in connect() below 145 | mqttc.publish(MQTT_LWT, "1", qos=0, retain=True) 146 | 147 | elif result_code == 1: 148 | logging.info("Connection refused - unacceptable protocol version") 149 | elif result_code == 2: 150 | logging.info("Connection refused - identifier rejected") 151 | elif result_code == 3: 152 | logging.info("Connection refused - server unavailable") 153 | elif result_code == 4: 154 | logging.info("Connection refused - bad user name or password") 155 | elif result_code == 5: 156 | logging.info("Connection refused - not authorised") 157 | else: 158 | logging.warning("Connection failed - result code %d" % (result_code)) 159 | 160 | def on_disconnect(mosq, obj, result_code): 161 | """ 162 | Handle disconnections from the broker 163 | """ 164 | if result_code == 0: 165 | logging.info("Clean disconnection from broker") 166 | else: 167 | logging.info("Broker connection lost. Retrying in 5s...") 168 | time.sleep(5) 169 | 170 | def on_message(mosq, obj, msg): 171 | """ 172 | Handle incoming messages 173 | """ 174 | if msg.topic == MONITOR_REFRESH: 175 | logging.debug("Refreshing the state of all monitored pins...") 176 | refresh() 177 | return 178 | 179 | topicparts = msg.topic.split("/") 180 | pin = int(topicparts[len(topicparts) - 1]) 181 | value = int(msg.payload) 182 | logging.debug("Incoming message for pin %d -> %d" % (pin, value)) 183 | 184 | if PFIO_MODULE: 185 | if value == 1: 186 | PFIO.digital_write(pin, 1) 187 | else: 188 | PFIO.digital_write(pin, 0) 189 | 190 | if GPIO_MODULE: 191 | if pin not in GPIO_OUTPUT_PINS: 192 | GPIO.setup(pin, GPIO.OUT, initial=GPIO.HIGH) 193 | GPIO_OUTPUT_PINS.append(pin) 194 | 195 | if value == 1: 196 | GPIO.output(pin, GPIO.LOW) 197 | else: 198 | GPIO.output(pin, GPIO.HIGH) 199 | 200 | # End of MQTT callbacks 201 | 202 | 203 | def cleanup(signum, frame): 204 | """ 205 | Signal handler to ensure we disconnect cleanly 206 | in the event of a SIGTERM or SIGINT. 207 | """ 208 | # Cleanup our interface modules 209 | if PFIO_MODULE: 210 | logging.debug("Clean up PiFace.PFIO module") 211 | PFIO.deinit() 212 | 213 | if GPIO_MODULE: 214 | logging.debug("Clean up RPi.GPIO module") 215 | for pin in GPIO_OUTPUT_PINS: 216 | GPIO.output(pin, GPIO.HIGH) 217 | GPIO.cleanup() 218 | 219 | # Publish our LWT and cleanup the MQTT connection 220 | logging.info("Disconnecting from broker...") 221 | mqttc.publish(MQTT_LWT, "0", qos=0, retain=True) 222 | mqttc.disconnect() 223 | mqttc.loop_stop() 224 | 225 | # Exit from our application 226 | logging.info("Exiting on signal %d" % (signum)) 227 | sys.exit(signum) 228 | 229 | 230 | def connect(): 231 | """ 232 | Connect to the broker, define the callbacks, and subscribe 233 | This will also set the Last Will and Testament (LWT) 234 | The LWT will be published in the event of an unclean or 235 | unexpected disconnection. 236 | """ 237 | # Add the callbacks 238 | mqttc.on_connect = on_connect 239 | mqttc.on_disconnect = on_disconnect 240 | mqttc.on_message = on_message 241 | 242 | # Set the login details 243 | if MQTT_USERNAME: 244 | mqttc.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) 245 | 246 | # Set TLS details 247 | if MQTT_TLS_PROTOCOL: 248 | if MQTT_TLS_PROTOCOL == 'tlsv1_2': 249 | mqttc.tls_set(MQTT_CERT_PATH, tls_version=ssl.PROTOCOL_TLSv1_2) 250 | mqttc.tls_insecure_set(MQTT_TLS_INSECURE) 251 | if MQTT_TLS_PROTOCOL == 'tlsv1_1': 252 | mqttc.tls_set(MQTT_CERT_PATH, tls_version=ssl.PROTOCOL_TLSv1_1) 253 | mqttc.tls_insecure_set(MQTT_TLS_INSECURE) 254 | if MQTT_TLS_PROTOCOL == 'tlsv1': 255 | mqttc.tls_set(MQTT_CERT_PATH, tls_version=ssl.PROTOCOL_TLSv1) 256 | mqttc.tls_insecure_set(MQTT_TLS_INSECURE) 257 | if MQTT_TLS_PROTOCOL == 'sslv3': 258 | mqttc.tls_set(MQTT_CERT_PATH, tls_version=ssl.PROTOCOL_SSLv3) 259 | mqttc.tls_insecure_set(MQTT_TLS_INSECURE) 260 | 261 | # Set the Last Will and Testament (LWT) *before* connecting 262 | mqttc.will_set(MQTT_LWT, payload="0", qos=0, retain=True) 263 | 264 | # Attempt to connect 265 | logging.debug("Connecting to %s:%d..." % (MQTT_HOST, MQTT_PORT)) 266 | try: 267 | mqttc.connect(MQTT_HOST, MQTT_PORT, 60) 268 | except Exception as e: 269 | logging.error("Error connecting to %s:%d: %s" % (MQTT_HOST, MQTT_PORT, str(e))) 270 | sys.exit(2) 271 | 272 | # Let the connection run forever 273 | mqttc.loop_start() 274 | 275 | 276 | def init_pfio(): 277 | """ 278 | Initialise the PFIO library 279 | """ 280 | PFIO.init() 281 | 282 | 283 | def init_gpio(): 284 | """ 285 | Initialise the GPIO library 286 | """ 287 | GPIO.setwarnings(False) 288 | if MONITOR_PIN_NUMBERING == "BCM": 289 | logging.debug("Initialising GPIO using BCM numbering") 290 | GPIO.setmode(GPIO.BCM) 291 | else: 292 | logging.debug("Initialising GPIO using Board numbering") 293 | GPIO.setmode(GPIO.BOARD) 294 | 295 | for PIN in PINS: 296 | index = [y[0] for y in PINS].index(PIN[0]) 297 | pin = PINS[index][0] 298 | 299 | logging.debug("Initialising GPIO input pin %d..." % (pin)) 300 | if MONITOR_PINS_PUD == "UP": 301 | GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 302 | elif MONITOR_PINS_PUD == "DOWN": 303 | GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) 304 | else: 305 | GPIO.setup(pin, GPIO.IN) 306 | 307 | def read_pin(pin): 308 | state = -1 309 | if PFIO_MODULE: 310 | state = PFIO.digital_read(pin) 311 | 312 | if GPIO_MODULE: 313 | state = GPIO.input(pin) 314 | 315 | if MONITOR_OUT_INVERT: 316 | if state == 0: 317 | state = 1 318 | elif state == 1: 319 | state = 0 320 | return(state) 321 | 322 | 323 | def refresh(): 324 | """ 325 | Refresh the state of all pins we are monitoring 326 | """ 327 | for PIN in PINS: 328 | index = [y[0] for y in PINS].index(PIN[0]) 329 | pin = PINS[index][0] 330 | state = read_pin(pin) 331 | 332 | logging.debug("Refreshing pin %d state -> %d" % (pin, state)) 333 | mqttc.publish(MQTT_TOPIC_OUT % pin, payload=state, qos=MQTT_QOS, retain=MQTT_RETAIN) 334 | 335 | 336 | def poll(): 337 | """ 338 | The main loop in which we monitor the state of the PINs 339 | and publish any changes. 340 | """ 341 | while True: 342 | for PIN in PINS: 343 | index = [y[0] for y in PINS].index(PIN[0]) 344 | pin = PINS[index][0] 345 | oldstate = PINS[index][1] 346 | newstate = read_pin(pin) 347 | 348 | if newstate != oldstate: 349 | logging.debug("Pin %d changed from %d to %d" % (pin, oldstate, newstate)) 350 | mqttc.publish(MQTT_TOPIC_OUT % pin, payload=newstate, qos=MQTT_QOS, retain=MQTT_RETAIN) 351 | PINS[index][1] = newstate 352 | 353 | time.sleep(MONITOR_POLL) 354 | 355 | # Use the signal module to handle signals 356 | for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT]: 357 | signal.signal(sig, cleanup) 358 | 359 | # Initialise our pins 360 | if PFIO_MODULE: 361 | init_pfio() 362 | 363 | if GPIO_MODULE: 364 | init_gpio() 365 | 366 | # Connect to broker and begin polling our GPIO pins 367 | connect() 368 | poll() 369 | --------------------------------------------------------------------------------