#!/usr/bin/env python from __future__ import division, print_function import paho.mqtt.client as mqtt import base64, sys, math from hashlib import md5 # The callback for when the client receives a CONNACK response from the server. def on_connect(client, userdata, flags, rc): if rc != 0: print("Connection Failed with result code {}".format(rc)) client.disconnect() else: print("Connected with result code {}".format(rc)) client.subscribe("{base_topic}{device_id}/$state".format(**userdata)) # v3 / v4 devices client.subscribe("{base_topic}{device_id}/$online".format(**userdata)) # v2 devices print("Waiting for device to come online...") # The callback for when a PUBLISH message is received from the server. def on_message(client, userdata, msg): # decode string for python2/3 compatiblity msg.payload = msg.payload.decode() if msg.topic.endswith('$implementation/ota/status'): status = int(msg.payload.split()[0]) if userdata.get("published"): if status == 206: # in progress # state in progress, print progress bar progress, total = [int(x) for x in msg.payload.split()[1].split('/')] bar_width = 30 bar = int(bar_width*(progress/total)) print("\r[", '+'*bar, ' '*(bar_width-bar), "] ", msg.payload.split()[1], end='', sep='') if (progress == total): print() sys.stdout.flush() elif status == 304: # not modified print("Device firmware already up to date with md5 checksum: {}".format(userdata.get('md5'))) client.disconnect() elif status == 403: # forbidden print("Device ota disabled, aborting...") client.disconnect() elif msg.topic.endswith('$fw/checksum'): checksum = msg.payload if userdata.get("published"): if checksum == userdata.get('md5'): print("Device back online. Update Successful!") else: print("Expecting checksum {}, got {}, update failed!".format(userdata.get('md5'), checksum)) client.disconnect() else: if checksum != userdata.get('md5'): # save old md5 for comparison with new firmware userdata.update({'old_md5': checksum}) else: print("Device firmware already up to date with md5 checksum: {}".format(checksum)) client.disconnect() elif msg.topic.endswith('ota/enabled'): if msg.payload == 'true': userdata.update({'ota_enabled': True}) else: print("Device ota disabled, aborting...") client.disconnect() elif msg.topic.endswith('$state') or msg.topic.endswith('$online'): if (msg.topic.endswith('$state') and msg.payload != 'ready') or (msg.topic.endswith('$online') and msg.payload == 'false'): return # calcluate firmware md5 firmware_md5 = md5(userdata['firmware']).hexdigest() userdata.update({'md5': firmware_md5}) # Subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. client.subscribe("{base_topic}{device_id}/$implementation/ota/status".format(**userdata)) client.subscribe("{base_topic}{device_id}/$implementation/ota/enabled".format(**userdata)) client.subscribe("{base_topic}{device_id}/$fw/#".format(**userdata)) # Wait for device info to come in and invoke the on_message callback where update will continue print("Waiting for device info...") if ( not userdata.get("published") ) and ( userdata.get('ota_enabled') ) and \ ( 'old_md5' in userdata.keys() ) and ( userdata.get('md5') != userdata.get('old_md5') ): # push the firmware binary userdata.update({"published": True}) topic = "{base_topic}{device_id}/$implementation/ota/firmware/{md5}".format(**userdata) print("Publishing new firmware with checksum {}".format(userdata.get('md5'))) client.publish(topic, userdata['firmware']) def main(broker_host, broker_port, broker_username, broker_password, broker_ca_cert, base_topic, device_id, firmware): # initialise mqtt client and register callbacks client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message # set username and password if given if broker_username and broker_password: client.username_pw_set(broker_username, broker_password) if broker_ca_cert is not None: client.tls_set( ca_certs=broker_ca_cert ) # save data to be used in the callbacks client.user_data_set({ "base_topic": base_topic, "device_id": device_id, "firmware": firmware }) # start connection print("Connecting to mqtt broker {} on port {}".format(broker_host, broker_port)) client.connect(broker_host, broker_port, 60) # Blocking call that processes network traffic, dispatches callbacks and handles reconnecting. client.loop_forever() if __name__ == '__main__': import argparse parser = argparse.ArgumentParser( description='ota firmware update scirpt for ESP8226 implemenation of the Homie mqtt IoT convention.') # ensure base topic always ends with a '/' def base_topic_arg(s): s = str(s) if not s.endswith('/'): s = s + '/' return s # specify arguments parser.add_argument('-l', '--broker-host', type=str, required=False, help='host name or ip address of the mqtt broker', default="127.0.0.1") parser.add_argument('-p', '--broker-port', type=int, required=False, help='port of the mqtt broker', default=1883) parser.add_argument('-u', '--broker-username', type=str, required=False, help='username used to authenticate with the mqtt broker') parser.add_argument('-d', '--broker-password', type=str, required=False, help='password used to authenticate with the mqtt broker') parser.add_argument('-t', '--base-topic', type=base_topic_arg, required=False, help='base topic of the homie devices on the broker', default="homie/") parser.add_argument('-i', '--device-id', type=str, required=True, help='homie device id') parser.add_argument('firmware', type=argparse.FileType('rb'), help='path to the firmware to be sent to the device') parser.add_argument("--broker-tls-cacert", default=None, required=False, help="CA certificate bundle used to validate TLS connections. If set, TLS will be enabled on the broker conncetion" ) # workaround for http://bugs.python.org/issue9694 parser._optionals.title = "arguments" # get and validate arguments args = parser.parse_args() # read the contents of firmware into buffer fw_buffer = args.firmware.read() args.firmware.close() firmware = bytearray() firmware.extend(fw_buffer) # Invoke the business logic main(args.broker_host, args.broker_port, args.broker_username, args.broker_password, args.broker_tls_cacert, args.base_topic, args.device_id, firmware)