print('New message on topic {0}: {1}'.format(topic, message))

# Connect to WiFi
wifi.connect()

# Set up a MiniMQTT Client
mqtt_client = MQTT(socket,
                   broker = secrets['broker'],
                   username = secrets['user'],
                   password = secrets['pass'],
                   network_manager = wifi)

# Setup the callback methods above
mqtt_client.on_connect = connected
mqtt_client.on_disconnect = disconnected
mqtt_client.on_message = message

# Connect the client to the MQTT broker.
mqtt_client.connect()

photocell_val = 0
while True:
    # Poll the message queue
    mqtt_client.loop()

    # Send a new message
    print('Sending photocell value: %d'%photocell_val)
    mqtt_client.publish(default_topic, photocell_val)
    photocell_val += 1
    time.sleep(0.5)
예제 #2
0
wifi.connect()

# Set up a MiniMQTT Client
client = MQTT(socket,
              broker=secrets['broker'],
              username=secrets['user'],
              password=secrets['pass'],
              network_manager=wifi)

# Connect callback handlers to client
client.on_connect = connect
client.on_disconnect = disconnect
client.on_subscribe = subscribe
client.on_unsubscribe = unsubscribe
client.on_publish = publish

print('Attempting to connect to %s' % client.broker)
client.connect()

print('Subscribing to %s' % mqtt_topic)
client.subscribe(mqtt_topic)

print('Publishing to %s' % mqtt_topic)
client.publish(mqtt_topic, 'Hello Broker!')

print('Unsubscribing from %s' % mqtt_topic)
client.unsubscribe(mqtt_topic)

print('Disconnecting from %s' % client.broker)
client.disconnect()
예제 #3
0
class IoTMQTT:
    """MQTT client for Azure IoT
    """
    def _gen_sas_token(self) -> str:
        token_expiry = int(time.time() + self._token_expires)
        uri = self._hostname + "%2Fdevices%2F" + self._device_id
        signed_hmac_sha256 = compute_derived_symmetric_key(
            self._key, uri + "\n" + str(token_expiry))
        signature = quote(signed_hmac_sha256, "~()*!.'")
        if signature.endswith(
                "\n"
        ):  # somewhere along the crypto chain a newline is inserted
            signature = signature[:-1]
        token = "SharedAccessSignature sr={}&sig={}&se={}".format(
            uri, signature, token_expiry)
        return token

    def _create_mqtt_client(self) -> None:
        minimqtt.set_socket(self._socket, self._iface)

        self._mqtts = MQTT(
            broker=self._hostname,
            username=self._username,
            password=self._passwd,
            port=8883,
            keep_alive=120,
            is_ssl=True,
            client_id=self._device_id,
            log=True,
        )

        self._mqtts.logger.setLevel(self._logger.getEffectiveLevel())

        # set actions to take throughout connection lifecycle
        self._mqtts.on_connect = self._on_connect
        self._mqtts.on_log = self._on_log
        self._mqtts.on_publish = self._on_publish
        self._mqtts.on_disconnect = self._on_disconnect

        # initiate the connection using the adafruit_minimqtt library
        self._mqtts.connect()

    # pylint: disable=C0103, W0613
    def _on_connect(self, client, userdata, _, rc) -> None:
        self._logger.info("- iot_mqtt :: _on_connect :: rc = " + str(rc) +
                          ", userdata = " + str(userdata))
        if rc == 0:
            self._mqtt_connected = True
        self._auth_response_received = True
        self._callback.connection_status_change(True)

    # pylint: disable=C0103, W0613
    def _on_log(self, client, userdata, level, buf) -> None:
        self._logger.info("mqtt-log : " + buf)
        if level <= 8:
            self._logger.error("mqtt-log : " + buf)

    def _on_disconnect(self, client, userdata, rc) -> None:
        self._logger.info("- iot_mqtt :: _on_disconnect :: rc = " + str(rc))
        self._auth_response_received = True

        if rc == 5:
            self._logger.error("on(disconnect) : Not authorized")
            self.disconnect()

        if rc == 1:
            self._mqtt_connected = False

        if rc != 5:
            self._callback.connection_status_change(False)

    def _on_publish(self, client, data, topic, msg_id) -> None:
        self._logger.info("- iot_mqtt :: _on_publish :: " + str(data) +
                          " on topic " + str(topic))

    # pylint: disable=W0703
    def _handle_device_twin_update(self, client, topic: str, msg: str) -> None:
        self._logger.debug("- iot_mqtt :: _echo_desired :: " + topic)
        twin = None
        desired = None

        try:
            twin = json.loads(msg)
        except json.JSONDecodeError as e:
            self._logger.error(
                "ERROR: JSON parse for Device Twin message object has failed. => "
                + msg + " => " + str(e))
            return

        if "reported" in twin:
            reported = twin["reported"]

            if "$version" in reported:
                reported_version = reported["$version"]
                reported.pop("$version")
            else:
                self._logger.error(
                    "ERROR: Unexpected payload for reported twin update => " +
                    msg)
                return

            for property_name, value in reported.items():
                self._callback.device_twin_reported_updated(
                    property_name, value, reported_version)

        is_patch = "desired" not in twin

        if is_patch:
            desired = twin
        else:
            desired = twin["desired"]

        if "$version" in desired:
            desired_version = desired["$version"]
            desired.pop("$version")
        else:
            self._logger.error(
                "ERROR: Unexpected payload for desired twin update => " + msg)
            return

        for property_name, value in desired.items():
            self._callback.device_twin_desired_updated(property_name, value,
                                                       desired_version)

    def _handle_direct_method(self, client, topic: str, msg: str) -> None:
        index = topic.find("$rid=")
        method_id = 1
        method_name = "None"
        if index == -1:
            self._logger.error("ERROR: C2D doesn't include topic id")
        else:
            method_id = topic[index + 5:]
            topic_template = "$iothub/methods/POST/"
            len_temp = len(topic_template)
            method_name = topic[len_temp:topic.find("/", len_temp + 1)]

        ret = self._callback.direct_method_invoked(method_name, msg)
        gc.collect()

        ret_code = 200
        ret_message = "{}"
        if ret.response_code is not None:
            ret_code = ret.response_code
        if ret.response_message is not None:
            ret_message = ret.response_message

            # ret message must be JSON
            if not ret_message.startswith("{") or not ret_message.endswith(
                    "}"):
                ret_json = {"Value": ret_message}
                ret_message = json.dumps(ret_json)

        next_topic = "$iothub/methods/res/{}/?$rid={}".format(
            ret_code, method_id)
        self._logger.info("C2D: => " + next_topic + " with data " +
                          ret_message + " and name => " + method_name)
        self._send_common(next_topic, ret_message)

    def _handle_cloud_to_device_message(self, client, topic: str,
                                        msg: str) -> None:
        parts = topic.split("&")[1:]

        properties = {}
        for part in parts:
            key_value = part.split("=")
            properties[key_value[0]] = key_value[1]

        self._callback.cloud_to_device_message_received(msg, properties)
        gc.collect()

    def _send_common(self, topic: str, data) -> None:
        # Convert data to a string
        if isinstance(data, dict):
            data = json.dumps(data)

        if not isinstance(data, str):
            raise IoTError("Data must be a string or a dictionary")

        self._logger.debug("Sending message on topic: " + topic)
        self._logger.debug("Sending message: " + str(data))

        retry = 0

        while True:
            gc.collect()
            try:
                self._logger.debug("Trying to send...")
                self._mqtts.publish(topic, data)
                self._logger.debug("Data sent")
                break
            except RuntimeError as runtime_error:
                self._logger.info(
                    "Could not send data, retrying after 0.5 seconds: " +
                    str(runtime_error))
                retry = retry + 1

                if retry >= 10:
                    self._logger.error("Failed to send data")
                    raise

                time.sleep(0.5)
                continue

        gc.collect()

    def _get_device_settings(self) -> None:
        self._logger.info("- iot_mqtt :: _get_device_settings :: ")
        self.loop()
        self._send_common("$iothub/twin/GET/?$rid=0", " ")

    # pylint: disable=R0913
    def __init__(
        self,
        callback: IoTMQTTCallback,
        socket,
        iface,
        hostname: str,
        device_id: str,
        key: str,
        token_expires: int = 21600,
        logger: logging = None,
    ):
        """Create the Azure IoT MQTT client
        :param IoTMQTTCallback callback: A callback class
        :param socket: The socket to communicate over
        :param iface: The network interface to communicate over
        :param str hostname: The hostname of the MQTT broker to connect to, get this by registering the device
        :param str device_id: The device ID of the device to register
        :param str key: The primary or secondary key of the device to register
        :param int token_expires: The number of seconds till the token expires, defaults to 6 hours
        :param adafruit_logging logger: The logger
        """
        self._callback = callback
        self._socket = socket
        self._iface = iface
        self._mqtt_connected = False
        self._auth_response_received = False
        self._mqtts = None
        self._device_id = device_id
        self._hostname = hostname
        self._key = key
        self._token_expires = token_expires
        self._username = "******".format(
            self._hostname, device_id, constants.IOTC_API_VERSION)
        self._passwd = self._gen_sas_token()
        self._logger = logger if logger is not None else logging.getLogger(
            "log")
        self._is_subscribed_to_twins = False

    def _subscribe_to_core_topics(self):
        device_bound_topic = "devices/{}/messages/devicebound/#".format(
            self._device_id)
        self._mqtts.add_topic_callback(device_bound_topic,
                                       self._handle_cloud_to_device_message)
        self._mqtts.subscribe(device_bound_topic)

        self._mqtts.add_topic_callback("$iothub/methods/#",
                                       self._handle_direct_method)
        self._mqtts.subscribe("$iothub/methods/#")

    def _subscribe_to_twin_topics(self):
        self._mqtts.add_topic_callback(
            "$iothub/twin/PATCH/properties/desired/#",
            self._handle_device_twin_update)
        self._mqtts.subscribe("$iothub/twin/PATCH/properties/desired/#"
                              )  # twin desired property changes

        self._mqtts.add_topic_callback("$iothub/twin/res/200/#",
                                       self._handle_device_twin_update)
        self._mqtts.subscribe(
            "$iothub/twin/res/200/#")  # twin properties response

    def connect(self) -> bool:
        """Connects to the MQTT broker
        :returns: True if the connection is successful, otherwise False
        :rtype: bool
        """
        self._logger.info("- iot_mqtt :: connect :: " + self._hostname)

        self._create_mqtt_client()

        self._logger.info(
            " - iot_mqtt :: connect :: created mqtt client. connecting..")
        while self._auth_response_received is None:
            self.loop()

        self._logger.info(
            " - iot_mqtt :: connect :: on_connect must be fired. Connected ? "
            + str(self.is_connected()))
        if not self.is_connected():
            return False

        self._mqtt_connected = True
        self._auth_response_received = True

        self._subscribe_to_core_topics()

        return True

    def subscribe_to_twins(self) -> None:
        """Subscribes to digital twin updates
        Only call this if your tier of IoT Hub supports this
        """
        if self._is_subscribed_to_twins:
            return

        # do this separately as this is not supported in B1 hubs
        self._subscribe_to_twin_topics()

        self._get_device_settings()

        self._is_subscribed_to_twins = True

    def disconnect(self) -> None:
        """Disconnects from the MQTT broker
        """
        if not self.is_connected():
            return

        self._logger.info("- iot_mqtt :: disconnect :: ")
        self._mqtt_connected = False
        self._mqtts.disconnect()

    def reconnect(self) -> None:
        """Reconnects to the MQTT broker
        """
        self._logger.info("- iot_mqtt :: reconnect :: ")

        self._mqtts.reconnect()

    def is_connected(self) -> bool:
        """Gets if there is an open connection to the MQTT broker
        :returns: True if there is an open connection, False if not
        :rtype: bool
        """
        return self._mqtt_connected

    def loop(self) -> None:
        """Listens for MQTT messages
        """
        if not self.is_connected():
            return

        self._mqtts.loop()
        gc.collect()

    def send_device_to_cloud_message(self,
                                     message,
                                     system_properties: dict = None) -> None:
        """Send a device to cloud message from this device to Azure IoT Hub
        :param message: The message data as a JSON string or a dictionary
        :param system_properties: System properties to send with the message
        :raises: ValueError if the message is not a string or dictionary
        :raises RuntimeError: if the internet connection is not responding or is unable to connect
        """
        self._logger.info("- iot_mqtt :: send_device_to_cloud_message :: " +
                          message)
        topic = "devices/{}/messages/events/".format(self._device_id)

        if system_properties is not None:
            firstProp = True
            for prop in system_properties:
                if not firstProp:
                    topic += "&"
                else:
                    firstProp = False
                topic += prop + "=" + str(system_properties[prop])

        # Convert message to a string
        if isinstance(message, dict):
            message = json.dumps(message)

        if not isinstance(message, str):
            raise ValueError("message must be a string or a dictionary")

        self._send_common(topic, message)
        self._callback.message_sent(message)

    def send_twin_patch(self, patch) -> None:
        """Send a patch for the reported properties of the device twin
        :param patch: The patch as a JSON string or a dictionary
        :raises: IoTError if the data is not a string or dictionary
        :raises RuntimeError: if the internet connection is not responding or is unable to connect
        """
        self._logger.info("- iot_mqtt :: sendProperty :: " + str(patch))
        topic = "$iothub/twin/PATCH/properties/reported/?$rid={}".format(
            int(time.time()))
        self._send_common(topic, patch)
예제 #4
0
class IoTMQTT:
    """MQTT client for Azure IoT
    """

    _iotc_api_version = constants["iotcAPIVersion"]

    def _gen_sas_token(self):
        token_expiry = int(time.time() + self._token_expires)
        uri = self._hostname + "%2Fdevices%2F" + self._device_id
        signed_hmac_sha256 = DeviceRegistration.compute_derived_symmetric_key(
            self._key, uri + "\n" + str(token_expiry))
        signature = parse.quote(signed_hmac_sha256, "~()*!.'")
        if signature.endswith(
                "\n"
        ):  # somewhere along the crypto chain a newline is inserted
            signature = signature[:-1]
        token = "SharedAccessSignature sr={}&sig={}&se={}".format(
            uri, signature, token_expiry)
        return token

    # Workaround for https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT/issues/25
    def _try_create_mqtt_client(self, hostname):
        minimqtt.set_socket(socket, self._wifi_manager.esp)

        self._mqtts = MQTT(
            broker=hostname,
            username=self._username,
            password=self._passwd,
            port=8883,
            keep_alive=120,
            is_ssl=True,
            client_id=self._device_id,
            log=True,
        )

        self._mqtts.logger.setLevel(logging.INFO)

        # set actions to take throughout connection lifecycle
        self._mqtts.on_connect = self._on_connect
        self._mqtts.on_message = self._on_message
        self._mqtts.on_log = self._on_log
        self._mqtts.on_publish = self._on_publish
        self._mqtts.on_disconnect = self._on_disconnect

        # initiate the connection using the adafruit_minimqtt library
        self._mqtts.last_will()
        self._mqtts.connect()

    def _create_mqtt_client(self):
        try:
            self._try_create_mqtt_client(self._hostname)
        except ValueError:
            # Workaround for https://github.com/adafruit/Adafruit_CircuitPython_MiniMQTT/issues/25
            self._try_create_mqtt_client("https://" + self._hostname)

    # pylint: disable=C0103, W0613
    def _on_connect(self, client, userdata, _, rc):
        self._logger.info("- iot_mqtt :: _on_connect :: rc = " + str(rc) +
                          ", userdata = " + str(userdata))
        if rc == 0:
            self._mqtt_connected = True
        self._auth_response_received = True
        self._callback.connection_status_change(True)

    # pylint: disable=C0103, W0613
    def _on_log(self, client, userdata, level, buf):
        self._logger.info("mqtt-log : " + buf)
        if level <= 8:
            self._logger.error("mqtt-log : " + buf)

    def _on_disconnect(self, client, userdata, rc):
        self._logger.info("- iot_mqtt :: _on_disconnect :: rc = " + str(rc))
        self._auth_response_received = True

        if rc == 5:
            self._logger.error("on(disconnect) : Not authorized")
            self.disconnect()

        if rc == 1:
            self._mqtt_connected = False

        if rc != 5:
            self._callback.connection_status_change(False)

    def _on_publish(self, client, data, topic, msg_id):
        self._logger.info("- iot_mqtt :: _on_publish :: " + str(data) +
                          " on topic " + str(topic))

    # pylint: disable=W0703
    def _handle_device_twin_update(self, msg: str, topic: str):
        self._logger.debug("- iot_mqtt :: _echo_desired :: " + topic)
        twin = None
        desired = None

        print(msg)

        try:
            twin = json.loads(msg)
        except Exception as e:
            self._logger.error(
                "ERROR: JSON parse for Device Twin message object has failed. => "
                + msg + " => " + str(e))
            return

        if "reported" in twin:
            reported = twin["reported"]

            if "$version" in reported:
                reported_version = reported["$version"]
                reported.pop("$version")
            else:
                self._logger.error(
                    "ERROR: Unexpected payload for reported twin update => " +
                    msg)
                return

            for property_name, value in reported.items():
                self._callback.device_twin_reported_updated(
                    property_name, value, reported_version)

        is_patch = "desired" not in twin

        if is_patch:
            desired = twin
        else:
            desired = twin["desired"]

        if "$version" in desired:
            desired_version = desired["$version"]
            desired.pop("$version")
        else:
            self._logger.error(
                "ERROR: Unexpected payload for desired twin update => " + msg)
            return

        for property_name, value in desired.items():
            self._callback.device_twin_desired_updated(property_name, value,
                                                       desired_version)

    def _handle_direct_method(self, msg: str, topic: str):
        index = topic.find("$rid=")
        method_id = 1
        method_name = "None"
        if index == -1:
            self._logger.error("ERROR: C2D doesn't include topic id")
        else:
            method_id = topic[index + 5:]
            topic_template = "$iothub/methods/POST/"
            len_temp = len(topic_template)
            method_name = topic[len_temp:topic.find("/", len_temp + 1)]

        ret = self._callback.direct_method_called(method_name, msg)

        ret_code = 200
        ret_message = "{}"
        if ret.get_response_code() is not None:
            ret_code = ret.get_response_code()
        if ret.get_response_message() is not None:
            ret_message = ret.get_response_message()

            # ret message must be JSON
            if not ret_message.startswith("{") or not ret_message.endswith(
                    "}"):
                ret_json = {"Value": ret_message}
                ret_message = json.dumps(ret_json)

        next_topic = "$iothub/methods/res/{}/?$rid={}".format(
            ret_code, method_id)
        self._logger.info("C2D: => " + next_topic + " with data " +
                          ret_message + " and name => " + method_name)
        self._send_common(next_topic, ret_message)

    def _handle_cloud_to_device_message(self, msg: str, topic: str):
        parts = topic.split("&")[1:]

        properties = {}
        for part in parts:
            key_value = part.split("=")
            properties[key_value[0]] = key_value[1]

        self._callback.cloud_to_device_message_received(msg, properties)

    # pylint: disable=W0702, R0912
    def _on_message(self, client, msg_topic, payload):
        topic = ""
        msg = None

        print("Topic: ", str(msg_topic))
        self._logger.info("- iot_mqtt :: _on_message :: payload(" +
                          str(payload) + ")")

        if payload is not None:
            try:
                msg = payload.decode("utf-8")
            except:
                msg = str(payload)

        if msg_topic is not None:
            try:
                topic = msg_topic.decode("utf-8")
            except:
                topic = str(msg_topic)

        if topic.startswith("$iothub/"):
            if topic.startswith("$iothub/twin/PATCH/properties/desired/"
                                ) or topic.startswith(
                                    "$iothub/twin/res/200/?$rid="):
                self._handle_device_twin_update(str(msg), topic)
            elif topic.startswith("$iothub/methods"):
                self._handle_direct_method(str(msg), topic)
            else:
                if not topic.startswith(
                        "$iothub/twin/res/"):  # not twin response
                    self._logger.error("ERROR: unknown twin! - {}".format(msg))
        elif topic.startswith("devices/{}/messages/devicebound".format(
                self._device_id)):
            self._handle_cloud_to_device_message(str(msg), topic)
        else:
            self._logger.error("ERROR: (unknown message) - {}".format(msg))

    def _send_common(self, topic, data) -> None:
        self._logger.debug("Sending message on topic: " + topic)
        self._logger.debug("Sending message: " + str(data))

        retry = 0

        while True:
            gc.collect()
            try:
                self._logger.debug("Trying to send...")
                self._mqtts.publish(topic, data)
                self._logger.debug("Data sent")
                break
            except RuntimeError as runtime_error:
                self._logger.info(
                    "Could not send data, retrying after 0.5 seconds: " +
                    str(runtime_error))
                retry = retry + 1

                if retry >= 10:
                    self._logger.error("Failed to send data")
                    raise

                time.sleep(0.5)
                continue

        print("finished _send_common")
        gc.collect()

    def _get_device_settings(self) -> None:
        self._logger.info("- iot_mqtt :: _get_device_settings :: ")
        self.loop()
        self._send_common("$iothub/twin/GET/?$rid=0", " ")

    # pylint: disable=R0913
    def __init__(self,
                 callback: IoTMQTTCallback,
                 wifi_manager: ESPSPI_WiFiManager,
                 hostname: str,
                 device_id: str,
                 key: str,
                 token_expires: int = 21600,
                 logger: logging = None):
        """Create the Azure IoT MQTT client
        :param wifi_manager: The WiFi manager
        :param IoTMQTTCallback callback: A callback class
        :param str hostname: The hostname of the MQTT broker to connect to, get this by registering the device
        :param str device_id: The device ID of the device to register
        :param str key: The primary or secondary key of the device to register
        :param int token_expires: The number of seconds till the token expires, defaults to 6 hours
        :param adafruit_logging logger: The logger
        """
        self._wifi_manager = wifi_manager
        self._callback = callback
        self._mqtt_connected = False
        self._auth_response_received = False
        self._mqtts = None
        self._device_id = device_id
        self._hostname = hostname
        self._key = key
        self._token_expires = token_expires
        self._username = "******".format(self._hostname,
                                                       device_id,
                                                       self._iotc_api_version)
        self._passwd = self._gen_sas_token()
        self._logger = logger if logger is not None else logging.getLogger(
            "log")

    def connect(self):
        """Connects to the MQTT broker
        """
        self._logger.info("- iot_mqtt :: connect :: " + self._hostname)

        self._create_mqtt_client()

        self._logger.info(
            " - iot_mqtt :: connect :: created mqtt client. connecting..")
        while self._auth_response_received is None:
            self.loop()

        self._logger.info(
            " - iot_mqtt :: connect :: on_connect must be fired. Connected ? "
            + str(self.is_connected()))
        if not self.is_connected():
            return 1

        self._mqtt_connected = True
        self._auth_response_received = True

        self._mqtts.subscribe("devices/{}/messages/events/#".format(
            self._device_id))
        self._mqtts.subscribe("devices/{}/messages/devicebound/#".format(
            self._device_id))
        self._mqtts.subscribe("$iothub/twin/PATCH/properties/desired/#"
                              )  # twin desired property changes
        self._mqtts.subscribe("$iothub/twin/res/#")  # twin properties response
        self._mqtts.subscribe("$iothub/methods/#")

        if self._get_device_settings() == 0:
            self._callback.settings_updated()
        else:
            return 1

        return 0

    def disconnect(self):
        """Disconnects from the MQTT broker
        """
        if not self.is_connected():
            return

        self._logger.info("- iot_mqtt :: disconnect :: ")
        self._mqtt_connected = False
        self._mqtts.disconnect()

    def is_connected(self):
        """Gets if there is an open connection to the MQTT broker
        """
        return self._mqtt_connected

    def loop(self):
        """Listens for MQTT messages
        """
        if not self.is_connected():
            return

        self._mqtts.loop()

    def _send_common(self, topic, data):
        self._mqtts.publish(topic, data)

    def send_device_to_cloud_message(self,
                                     data,
                                     system_properties=None) -> None:
        """Send a device to cloud message from this device to Azure IoT Hub
        """
        self._logger.info("- iot_mqtt :: send_device_to_cloud_message :: " +
                          data)
        topic = "devices/{}/messages/events/".format(self._device_id)

        if system_properties is not None:
            firstProp = True
            for prop in system_properties:
                if not firstProp:
                    topic += "&"
                else:
                    firstProp = False
                topic += prop + "=" + str(system_properties[prop])

        self._send_common(topic, data)
        self._callback.message_sent(data)

    def send_twin_patch(self, data):
        """Send a patch for the reported properties of the device twin
        """
        self._logger.info("- iot_mqtt :: sendProperty :: " + data)
        topic = "$iothub/twin/PATCH/properties/reported/?$rid={}".format(
            int(time.time()))
        return self._send_common(topic, data)