Ejemplo n.º 1
0
class MQTTClient:
    """
    MQTT client implementation.

    MQTTClient instances provides API for connecting to a broker and send/receive messages using the MQTT protocol.

    :param client_id: MQTT client ID to use when connecting to the broker. If none, it will generated randomly by :func:`hbmqtt.utils.gen_client_id`
    :param config: Client configuration
    :param loop: asynio loop to use
    :return: class instance
    """
    def __init__(self, client_id=None, config=None, loop=None):
        self.logger = logging.getLogger(__name__)
        self.config = copy.deepcopy(_defaults)
        if config is not None:
            self.config.update(config)
        if client_id is not None:
            self.client_id = client_id
        else:
            from hbmqtt.utils import gen_client_id

            self.client_id = gen_client_id()
            self.logger.debug("Using generated client ID : %s" %
                              self.client_id)

        if loop is not None:
            self._loop = loop
        else:
            self._loop = asyncio.get_event_loop()
        self.session = None
        self._handler = None
        self._disconnect_task = None
        self._connected_state = asyncio.Event(loop=self._loop)
        self._no_more_connections = asyncio.Event(loop=self._loop)
        self.extra_headers = {}

        # Init plugins manager
        context = ClientContext()
        context.config = self.config
        self.plugins_manager = PluginManager("hbmqtt.client.plugins",
                                             context,
                                             loop=self._loop)
        self.client_tasks = deque()

    async def connect(
        self,
        uri=None,
        cleansession=None,
        cafile=None,
        capath=None,
        cadata=None,
        extra_headers={},
    ):
        """
        Connect to a remote broker.

        At first, a network connection is established with the server using the given protocol (``mqtt``, ``mqtts``, ``ws`` or ``wss``). Once the socket is connected, a `CONNECT <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028>`_ message is sent with the requested informations.

        This method is a *coroutine*.

        :param uri: Broker URI connection, conforming to `MQTT URI scheme <https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>`_. Uses ``uri`` config attribute by default.
        :param cleansession: MQTT CONNECT clean session flag
        :param cafile: server certificate authority file (optional, used for secured connection)
        :param capath: server certificate authority path (optional, used for secured connection)
        :param cadata: server certificate authority data (optional, used for secured connection)
        :param extra_headers: a dictionary with additional http headers that should be sent on the initial connection (optional, used only with websocket connections)
        :return: `CONNACK <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718033>`_ return code
        :raise: :class:`hbmqtt.client.ConnectException` if connection fails
        """

        self.session = self._initsession(uri, cleansession, cafile, capath,
                                         cadata)
        self.extra_headers = extra_headers
        self.logger.debug("Connect to: %s" % uri)

        try:
            return await self._do_connect()
        except asyncio.CancelledError:
            raise
        except BaseException as be:
            self.logger.warning("Connection failed: %r" % be)
            auto_reconnect = self.config.get("auto_reconnect", False)
            if not auto_reconnect:
                raise
            else:
                return await self.reconnect()

    async def disconnect(self):
        """
        Disconnect from the connected broker.

        This method sends a `DISCONNECT <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718090>`_ message and closes the network socket.

        This method is a *coroutine*.
        """
        await self.cancel_tasks()
        if self.session.transitions.is_connected():
            if not self._disconnect_task.done():
                self._disconnect_task.cancel()
            await self._handler.mqtt_disconnect()
            self._connected_state.clear()
            await self._handler.stop()
            self.session.transitions.disconnect()
        else:
            self.logger.warning(
                "Client session is not currently connected, ignoring call")

    async def cancel_tasks(self):
        """
        Before disconnection need to cancel all pending tasks
        :return:
        """
        try:
            while self.client_tasks:
                task = self.client_tasks.pop()
                task.cancel()
        except IndexError as err:
            pass

    async def reconnect(self, cleansession=None):
        """
        Reconnect a previously connected broker.

        Reconnection tries to establish a network connection and send a `CONNECT <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028>`_ message.
        Retries interval and attempts can be controlled with the ``reconnect_max_interval`` and ``reconnect_retries`` configuration parameters.

        This method is a *coroutine*.

        :param cleansession: clean session flag used in MQTT CONNECT messages sent for reconnections.
        :return: `CONNACK <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718033>`_ return code
        :raise: :class:`hbmqtt.client.ConnectException` if re-connection fails after max retries.
        """

        if self.session.transitions.is_connected():
            self.logger.warning("Client already connected")
            return CONNECTION_ACCEPTED

        if cleansession:
            self.session.clean_session = cleansession
        self.logger.debug("Reconnecting with session parameters: %s" %
                          self.session)
        reconnect_max_interval = self.config.get("reconnect_max_interval", 10)
        reconnect_retries = self.config.get("reconnect_retries", 5)
        nb_attempt = 1
        await asyncio.sleep(1, loop=self._loop)
        while True:
            try:
                self.logger.debug("Reconnect attempt %d ..." % nb_attempt)
                return await self._do_connect()
            except asyncio.CancelledError:
                raise
            except BaseException as e:
                self.logger.warning("Reconnection attempt failed: %r" % e)
                if reconnect_retries >= 0 and nb_attempt > reconnect_retries:
                    self.logger.error(
                        "Maximum number of connection attempts reached. Reconnection aborted"
                    )
                    raise ConnectException(
                        "Too many connection attempts failed")
                exp = 2**nb_attempt
                delay = exp if exp < reconnect_max_interval else reconnect_max_interval
                self.logger.debug("Waiting %d second before next attempt" %
                                  delay)
                await asyncio.sleep(delay, loop=self._loop)
                nb_attempt += 1

    async def _do_connect(self):
        return_code = await self._connect_coro()
        self._disconnect_task = asyncio.ensure_future(
            self.handle_connection_close(), loop=self._loop)
        return return_code

    @mqtt_connected
    async def ping(self):
        """
        Ping the broker.

        Send a MQTT `PINGREQ <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718081>`_ message for response.

        This method is a *coroutine*.
        """

        if self.session.transitions.is_connected():
            await self._handler.mqtt_ping()
        else:
            self.logger.warning(
                "MQTT PING request incompatible with current session state '%s'"
                % self.session.transitions.state)

    @mqtt_connected
    async def publish(self,
                      topic,
                      message,
                      qos=None,
                      retain=None,
                      ack_timeout=None):
        """
        Publish a message to the broker.

        Send a MQTT `PUBLISH <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718037>`_ message and wait for acknowledgment depending on Quality Of Service

        This method is a *coroutine*.

        :param topic: topic name to which message data is published
        :param message: payload message (as bytes) to send.
        :param qos: requested publish quality of service : QOS_0, QOS_1 or QOS_2. Defaults to ``default_qos`` config parameter or QOS_0.
        :param retain: retain flag. Defaults to ``default_retain`` config parameter or False.
        """
        def get_retain_and_qos():
            if qos:
                assert qos in (QOS_0, QOS_1, QOS_2)
                _qos = qos
            else:
                _qos = self.config["default_qos"]
                try:
                    _qos = self.config["topics"][topic]["qos"]
                except KeyError:
                    pass
            if retain:
                _retain = retain
            else:
                _retain = self.config["default_retain"]
                try:
                    _retain = self.config["topics"][topic]["retain"]
                except KeyError:
                    pass
            return _qos, _retain

        (app_qos, app_retain) = get_retain_and_qos()
        return await self._handler.mqtt_publish(topic, message, app_qos,
                                                app_retain, ack_timeout)

    @mqtt_connected
    async def subscribe(self, topics):
        """
        Subscribe to some topics.

        Send a MQTT `SUBSCRIBE <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718063>`_ message and wait for broker acknowledgment.

        This method is a *coroutine*.

        :param topics: array of topics pattern to subscribe with associated QoS.
        :return: `SUBACK <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718068>`_ message return code.

        Example of ``topics`` argument expected structure:
        ::

            [
                ('$SYS/broker/uptime', QOS_1),
                ('$SYS/broker/load/#', QOS_2),
            ]
        """
        return await self._handler.mqtt_subscribe(topics,
                                                  self.session.next_packet_id)

    @mqtt_connected
    async def unsubscribe(self, topics):
        """
        Unsubscribe from some topics.

        Send a MQTT `UNSUBSCRIBE <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718072>`_ message and wait for broker `UNSUBACK <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718077>`_ message.

        This method is a *coroutine*.

        :param topics: array of topics to unsubscribe from.

        Example of ``topics`` argument expected structure:
        ::

            ['$SYS/broker/uptime', '$SYS/broker/load/#']
        """
        await self._handler.mqtt_unsubscribe(topics,
                                             self.session.next_packet_id)

    async def deliver_message(self, timeout=None):
        """
        Deliver next received message.

        Deliver next message received from the broker. If no message is available, this methods waits until next message arrives or ``timeout`` occurs.

        This method is a *coroutine*.

        :param timeout: maximum number of seconds to wait before returning. If timeout is not specified or None, there is no limit to the wait time until next message arrives.
        :return: instance of :class:`hbmqtt.session.ApplicationMessage` containing received message information flow.
        :raises: :class:`asyncio.TimeoutError` if timeout occurs before a message is delivered
        """
        deliver_task = asyncio.ensure_future(
            self._handler.mqtt_deliver_next_message(), loop=self._loop)
        self.client_tasks.append(deliver_task)
        self.logger.debug("Waiting message delivery")
        done, pending = await asyncio.wait(
            [deliver_task],
            loop=self._loop,
            return_when=asyncio.FIRST_EXCEPTION,
            timeout=timeout,
        )
        if self.client_tasks:
            self.client_tasks.pop()
        if deliver_task in done:
            if deliver_task.exception() is not None:
                # deliver_task raised an exception, pass it on to our caller
                raise deliver_task.exception()
            return deliver_task.result()
        else:
            # timeout occured before message received
            deliver_task.cancel()
            raise asyncio.TimeoutError

    async def _connect_coro(self):
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        secure = True if scheme in ("mqtts", "wss") else False
        self.session.username = (self.session.username if self.session.username
                                 else uri_attributes.username)
        self.session.password = (self.session.password if self.session.password
                                 else uri_attributes.password)
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ("mqtt", "mqtts") and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == "mqtts" else 1883
        if scheme in ("ws", "wss") and not self.session.remote_port:
            self.session.remote_port = 443 if scheme == "wss" else 80
        if scheme in ("ws", "wss"):
            # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3
            uri = (
                scheme,
                self.session.remote_address + ":" +
                str(self.session.remote_port),
                uri_attributes[2],
                uri_attributes[3],
                uri_attributes[4],
                uri_attributes[5],
            )
            self.session.broker_uri = urlunparse(uri)
        # Init protocol handler
        # if not self._handler:
        self._handler = ClientProtocolHandler(self.plugins_manager,
                                              loop=self._loop)

        if secure:
            sc = ssl.create_default_context(
                ssl.Purpose.SERVER_AUTH,
                cafile=self.session.cafile,
                capath=self.session.capath,
                cadata=self.session.cadata,
            )
            if "certfile" in self.config and "keyfile" in self.config:
                sc.load_cert_chain(self.config["certfile"],
                                   self.config["keyfile"])
            if "check_hostname" in self.config and isinstance(
                    self.config["check_hostname"], bool):
                sc.check_hostname = self.config["check_hostname"]
            kwargs["ssl"] = sc

        try:
            reader = None
            writer = None
            self._connected_state.clear()
            # Open connection
            if scheme in ("mqtt", "mqtts"):
                conn_reader, conn_writer = await asyncio.open_connection(
                    self.session.remote_address,
                    self.session.remote_port,
                    loop=self._loop,
                    **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ("ws", "wss"):
                websocket = await websockets.connect(
                    self.session.broker_uri,
                    subprotocols=["mqtt"],
                    loop=self._loop,
                    extra_headers=self.extra_headers,
                    **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
            # Start MQTT protocol
            self._handler.attach(self.session, reader, writer)
            return_code = await self._handler.mqtt_connect()
            if return_code is not CONNECTION_ACCEPTED:
                self.session.transitions.disconnect()
                self.logger.warning("Connection rejected with code '%s'" %
                                    return_code)
                exc = ConnectException("Connection rejected by broker")
                exc.return_code = return_code
                raise exc
            else:
                # Handle MQTT protocol
                await self._handler.start()
                self.session.transitions.connect()
                self._connected_state.set()
                self.logger.debug(
                    "connected to %s:%s" %
                    (self.session.remote_address, self.session.remote_port))
            return return_code
        except InvalidURI as iuri:
            self.logger.warning("connection failed: invalid URI '%s'" %
                                self.session.broker_uri)
            self.session.transitions.disconnect()
            raise ConnectException(
                "connection failed: invalid URI '%s'" %
                self.session.broker_uri, iuri)
        except InvalidHandshake as ihs:
            self.logger.warning(
                "connection failed: invalid websocket handshake")
            self.session.transitions.disconnect()
            raise ConnectException(
                "connection failed: invalid websocket handshake", ihs)
        except (ProtocolHandlerException, ConnectionError, OSError) as e:
            self.logger.warning("MQTT connection failed: %r" % e)
            self.session.transitions.disconnect()
            raise ConnectException(e)

    async def handle_connection_close(self):
        def cancel_tasks():
            self._no_more_connections.set()
            while self.client_tasks:
                task = self.client_tasks.popleft()
                if not task.done():
                    task.set_exception(ClientException("Connection lost"))

        self.logger.debug("Watch broker disconnection")
        # Wait for disconnection from broker (like connection lost)
        await self._handler.wait_disconnect()
        self.logger.warning("Disconnected from broker")

        # Block client API
        self._connected_state.clear()

        # stop an clean handler
        # await self._handler.stop()
        self._handler.detach()
        self.session.transitions.disconnect()

        if self.config.get("auto_reconnect", False):
            # Try reconnection
            self.logger.debug("Auto-reconnecting")
            try:
                await self.reconnect()
            except ConnectException:
                # Cancel client pending tasks
                cancel_tasks()
        else:
            # Cancel client pending tasks
            cancel_tasks()

    def _initsession(self,
                     uri=None,
                     cleansession=None,
                     cafile=None,
                     capath=None,
                     cadata=None) -> Session:
        # Load config
        broker_conf = self.config.get("broker", dict()).copy()
        if uri:
            broker_conf["uri"] = uri
        if cafile:
            broker_conf["cafile"] = cafile
        elif "cafile" not in broker_conf:
            broker_conf["cafile"] = None
        if capath:
            broker_conf["capath"] = capath
        elif "capath" not in broker_conf:
            broker_conf["capath"] = None
        if cadata:
            broker_conf["cadata"] = cadata
        elif "cadata" not in broker_conf:
            broker_conf["cadata"] = None

        if cleansession is not None:
            broker_conf["cleansession"] = cleansession

        for key in ["uri"]:
            if not_in_dict_or_none(broker_conf, key):
                raise ClientException("Missing connection parameter '%s'" %
                                      key)

        s = Session()
        s.broker_uri = uri
        s.client_id = self.client_id
        s.cafile = broker_conf["cafile"]
        s.capath = broker_conf["capath"]
        s.cadata = broker_conf["cadata"]
        if cleansession is not None:
            s.clean_session = cleansession
        else:
            s.clean_session = self.config.get("cleansession", True)
        s.keep_alive = self.config["keep_alive"] - self.config["ping_delay"]
        if "will" in self.config:
            s.will_flag = True
            s.will_retain = self.config["will"]["retain"]
            s.will_topic = self.config["will"]["topic"]
            s.will_message = self.config["will"]["message"]
            s.will_qos = self.config["will"]["qos"]
        else:
            s.will_flag = False
            s.will_retain = False
            s.will_topic = None
            s.will_message = None
        return s
Ejemplo n.º 2
0
    def _connect_coro(self):
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        secure = True if scheme in ('mqtts', 'wss') else False
        self.session.username = uri_attributes.username
        self.session.password = uri_attributes.password
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == 'mqtts' else 1883
        if scheme in ('ws', 'wss') and not self.session.remote_port:
            self.session.remote_port = 443 if scheme == 'wss' else 80
        if scheme in ('ws', 'wss'):
            # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3
            uri = (scheme, self.session.remote_address + ":" +
                   str(self.session.remote_port), uri_attributes[2],
                   uri_attributes[3], uri_attributes[4], uri_attributes[5])
            self.session.broker_uri = urlunparse(uri)
        # Init protocol handler
        #if not self._handler:
        self._handler = ClientProtocolHandler(self.plugins_manager,
                                              loop=self._loop)

        if secure:
            if self.session.cafile is None or self.session.cafile == '':
                self.logger.warning(
                    "TLS connection can't be estabilshed, no certificate file (.cert) given"
                )
                raise ClientException(
                    "TLS connection can't be estabilshed, no certificate file (.cert) given"
                )
            sc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
                                            cafile=self.session.cafile,
                                            capath=self.session.capath,
                                            cadata=self.session.cadata)
            if 'certfile' in self.config and 'keyfile' in self.config:
                sc.load_cert_chain(self.config['certfile'],
                                   self.config['keyfile'])
            if 'check_hostname' in self.config and isinstance(
                    self.config['check_hostname'], bool):
                sc.check_hostname = self.config['check_hostname']
            kwargs['ssl'] = sc

        try:
            reader = None
            writer = None
            self._connected_state.clear()
            # Open connection
            if scheme in ('mqtt', 'mqtts'):
                conn_reader, conn_writer = \
                    yield from asyncio.open_connection(
                        self.session.remote_address,
                        self.session.remote_port, loop=self._loop, **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ('ws', 'wss'):
                websocket = yield from websockets.connect(
                    self.session.broker_uri,
                    subprotocols=['mqtt'],
                    loop=self._loop,
                    **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
            # Start MQTT protocol
            self._handler.attach(self.session, reader, writer)
            return_code = yield from self._handler.mqtt_connect()
            if return_code is not CONNECTION_ACCEPTED:
                self.session.transitions.disconnect()
                self.logger.warning("Connection rejected with code '%s'" %
                                    return_code)
                exc = ConnectException("Connection rejected by broker")
                exc.return_code = return_code
                raise exc
            else:
                # Handle MQTT protocol
                yield from self._handler.start()
                self.session.transitions.connect()
                self._connected_state.set()
                self.logger.debug(
                    "connected to %s:%s" %
                    (self.session.remote_address, self.session.remote_port))
            return return_code
        except InvalidURI as iuri:
            self.logger.warning("connection failed: invalid URI '%s'" %
                                self.session.broker_uri)
            self.session.transitions.disconnect()
            raise ConnectException(
                "connection failed: invalid URI '%s'" %
                self.session.broker_uri, iuri)
        except InvalidHandshake as ihs:
            self.logger.warning(
                "connection failed: invalid websocket handshake")
            self.session.transitions.disconnect()
            raise ConnectException(
                "connection failed: invalid websocket handshake", ihs)
        except (ProtocolHandlerException, ConnectionError, OSError) as e:
            self.logger.warning("MQTT connection failed: %r" % e)
            self.session.transitions.disconnect()
            raise ConnectException(e)
Ejemplo n.º 3
0
    async def _connect_coro(self):
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        secure = True if scheme in ("mqtts", "wss") else False
        self.session.username = (self.session.username if self.session.username
                                 else uri_attributes.username)
        self.session.password = (self.session.password if self.session.password
                                 else uri_attributes.password)
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ("mqtt", "mqtts") and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == "mqtts" else 1883
        if scheme in ("ws", "wss") and not self.session.remote_port:
            self.session.remote_port = 443 if scheme == "wss" else 80
        if scheme in ("ws", "wss"):
            # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3
            uri = (
                scheme,
                self.session.remote_address + ":" +
                str(self.session.remote_port),
                uri_attributes[2],
                uri_attributes[3],
                uri_attributes[4],
                uri_attributes[5],
            )
            self.session.broker_uri = urlunparse(uri)
        # Init protocol handler
        # if not self._handler:
        self._handler = ClientProtocolHandler(self.plugins_manager,
                                              loop=self._loop)

        if secure:
            sc = ssl.create_default_context(
                ssl.Purpose.SERVER_AUTH,
                cafile=self.session.cafile,
                capath=self.session.capath,
                cadata=self.session.cadata,
            )
            if "certfile" in self.config and "keyfile" in self.config:
                sc.load_cert_chain(self.config["certfile"],
                                   self.config["keyfile"])
            if "check_hostname" in self.config and isinstance(
                    self.config["check_hostname"], bool):
                sc.check_hostname = self.config["check_hostname"]
            kwargs["ssl"] = sc

        try:
            reader = None
            writer = None
            self._connected_state.clear()
            # Open connection
            if scheme in ("mqtt", "mqtts"):
                conn_reader, conn_writer = await asyncio.open_connection(
                    self.session.remote_address,
                    self.session.remote_port,
                    loop=self._loop,
                    **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ("ws", "wss"):
                websocket = await websockets.connect(
                    self.session.broker_uri,
                    subprotocols=["mqtt"],
                    loop=self._loop,
                    extra_headers=self.extra_headers,
                    **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
            # Start MQTT protocol
            self._handler.attach(self.session, reader, writer)
            return_code = await self._handler.mqtt_connect()
            if return_code is not CONNECTION_ACCEPTED:
                self.session.transitions.disconnect()
                self.logger.warning("Connection rejected with code '%s'" %
                                    return_code)
                exc = ConnectException("Connection rejected by broker")
                exc.return_code = return_code
                raise exc
            else:
                # Handle MQTT protocol
                await self._handler.start()
                self.session.transitions.connect()
                self._connected_state.set()
                self.logger.debug(
                    "connected to %s:%s" %
                    (self.session.remote_address, self.session.remote_port))
            return return_code
        except InvalidURI as iuri:
            self.logger.warning("connection failed: invalid URI '%s'" %
                                self.session.broker_uri)
            self.session.transitions.disconnect()
            raise ConnectException(
                "connection failed: invalid URI '%s'" %
                self.session.broker_uri, iuri)
        except InvalidHandshake as ihs:
            self.logger.warning(
                "connection failed: invalid websocket handshake")
            self.session.transitions.disconnect()
            raise ConnectException(
                "connection failed: invalid websocket handshake", ihs)
        except (ProtocolHandlerException, ConnectionError, OSError) as e:
            self.logger.warning("MQTT connection failed: %r" % e)
            self.session.transitions.disconnect()
            raise ConnectException(e)
Ejemplo n.º 4
0
    def _connect_coro(self):
        sc = None
        reader = None
        writer = None
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        self.session.username = uri_attributes.username
        self.session.password = uri_attributes.password
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == 'mqtts' else 1883

        if scheme in ('mqtts', 'wss'):
            if self.session.cafile is None or self.session.cafile == '':
                self.logger.warn(
                    "TLS connection can't be estabilshed, no certificate file (.cert) given"
                )
                raise ClientException(
                    "TLS connection can't be estabilshed, no certificate file (.cert) given"
                )
            sc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
                                            cafile=self.session.cafile,
                                            capath=self.session.capath,
                                            cadata=self.session.cadata)
            if 'certfile' in self.config and 'keyfile' in self.config:
                sc.load_cert_chain(self.config['certfile'],
                                   self.config['keyfile'])
            kwargs['ssl'] = sc

        # Open connection
        try:
            if scheme in ('mqtt', 'mqtts'):
                conn_reader, conn_writer = \
                    yield from asyncio.open_connection(self.session.remote_address, self.session.remote_port, **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ('ws', 'wss'):
                websocket = yield from websockets.connect(
                    self.session.broker_uri, subprotocols=['mqtt'], **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
        except Exception as e:
            self.logger.warn("connection failed: %s" % e)
            self.session.transitions.disconnect()
            raise ConnectException("connection Failed: %s" % e)

        return_code = None
        try:
            connect_packet = self.build_connect_packet()
            yield from connect_packet.to_stream(writer)
            self.logger.debug(" -out-> " + repr(connect_packet))

            connack = yield from ConnackPacket.from_stream(reader)
            self.logger.debug(" <-in-- " + repr(connack))
            return_code = connack.variable_header.return_code
        except Exception as e:
            self.logger.warn("connection failed: %s" % e)
            self.session.transitions.disconnect()
            raise ClientException("connection Failed: %s" % e)

        if return_code is not CONNECTION_ACCEPTED:
            yield from self._handler.stop()
            self.session.transitions.disconnect()
            self.logger.warn("Connection rejected with code '%s'" %
                             return_code)
            exc = ConnectException("Connection rejected by broker")
            exc.return_code = return_code
            raise exc
        else:
            # Handle MQTT protocol
            self._handler = ClientProtocolHandler(reader,
                                                  writer,
                                                  loop=self._loop)
            self._handler.attach_to_session(self.session)
            yield from self._handler.start()
            self.session.transitions.connect()
            self.logger.debug(
                "connected to %s:%s" %
                (self.session.remote_address, self.session.remote_port))
Ejemplo n.º 5
0
class MQTTClient:
    def __init__(self, client_id=None, config=None, loop=None):
        """

        :param config: Example yaml config
            broker:
                uri: mqtt:username@password//localhost:1883/
                cafile: somefile.cert  #Server authority file
                capath: /some/path # certficate file path
                cadata: certificate as string data
            keep_alive: 60
            cleansession: true
            will:
                retain: false
                topic: some/topic
                message: Will message
                qos: 0
            default_qos: 0
            default_retain: false
            topics:
                a/b:
                    qos: 2
                    retain: true
        :param loop:
        :return:
        """
        self.logger = logging.getLogger(__name__)
        self.config = _defaults
        if config is not None:
            self.config.update(config)
        if client_id is not None:
            self.client_id = client_id
        else:
            from hbmqtt.utils import gen_client_id
            self.client_id = gen_client_id()
            self.logger.debug("Using generated client ID : %s" %
                              self.client_id)

        if loop is not None:
            self._loop = loop
        else:
            self._loop = asyncio.get_event_loop()
        self.session = None
        self._handler = None
        self._disconnect_task = None
        self._connection_closed_future = None

    @asyncio.coroutine
    def connect(self,
                uri=None,
                cleansession=None,
                cafile=None,
                capath=None,
                cadata=None):
        """
        Connect to a remote broker
        :param uri: Broker URI connection, conforming to `MQTT URI scheme <https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>`_.
        :param cleansession: MQTT CONNECT clean session flaf
        :param cafile: server certificate authority file
        :return:
        """
        self.session = self._initsession(uri, cleansession, cafile, capath,
                                         cadata)
        self.logger.debug("Connect to: %s" % uri)

        return_code = yield from self._connect_coro()
        self._connection_closed_future = asyncio.Future(loop=self._loop)
        self._disconnect_task = asyncio.Task(self.handle_connection_close())
        return self._connection_closed_future

    @asyncio.coroutine
    def disconnect(self):
        if self.session.transitions.is_connected():
            if not self._disconnect_task.done():
                self._disconnect_task.cancel()
            yield from self._handler.mqtt_disconnect()
            yield from self._handler.stop()
            self._handler.detach_from_session()
            self.session.transitions.disconnect()
            self._connection_closed_future.set_result(None)
        else:
            self.logger.warn(
                "Client session is not currently connected, ignoring call")

    @asyncio.coroutine
    def reconnect(self, cleansession=False):
        if self.session.transitions.is_connected():
            self.logger.warn("Client already connected")
            return CONNECTION_ACCEPTED

        self.session.clean_session = cleansession
        self.logger.debug("Reconnecting with session parameters: %s" %
                          self.session)

        return_code = yield from self._connect_coro()
        self._connection_closed_future = asyncio.Future(loop=self._loop)
        self._disconnect_task = asyncio.Task(self.handle_connection_close())
        return self._connection_closed_future

    @asyncio.coroutine
    def ping(self):
        """
        Send a MQTT ping request and wait for response
        :return: None
        """
        if self.session.transitions.is_connected():
            yield from self._handler.mqtt_ping()
        else:
            self.logger.warn(
                "MQTT PING request incompatible with current session state '%s'"
                % self.session.transitions.state)

    @asyncio.coroutine
    def publish(self, topic, message, qos=None, retain=None):
        def get_retain_and_qos():
            if qos:
                _qos = qos
            else:
                _qos = self.config['default_qos']
                try:
                    _qos = self.config['topics'][topic]['qos']
                except KeyError:
                    pass
            if retain:
                _retain = retain
            else:
                _retain = self.config['default_retain']
                try:
                    _retain = self.config['topics'][topic]['retain']
                except KeyError:
                    pass
            return _qos, _retain

        if not self.session.transitions.is_connected():
            self.logger.warn(
                "publish MQTT message while not connected to broker, message may be lost"
            )
        (app_qos, app_retain) = get_retain_and_qos()
        if app_qos == 0:
            yield from self._handler.mqtt_publish(topic, message, 0x00,
                                                  app_retain)
        if app_qos == 1:
            yield from self._handler.mqtt_publish(topic, message, 0x01,
                                                  app_retain)
        if app_qos == 2:
            yield from self._handler.mqtt_publish(topic, message, 0x02,
                                                  app_retain)

    @asyncio.coroutine
    def subscribe(self, topics):
        if not self.session.transitions.is_connected():
            self.logger.warn(
                "subscribe while not connected to broker, message may be lost")
        return (yield from
                self._handler.mqtt_subscribe(topics,
                                             self.session.next_packet_id))

    @asyncio.coroutine
    def unsubscribe(self, topics):
        if not self.session.transitions.is_connected():
            self.logger.warn(
                "unsubscribe while not connected to broker, message may be lost"
            )
        yield from self._handler.mqtt_unsubscribe(topics,
                                                  self.session.next_packet_id)

    @asyncio.coroutine
    def deliver_message(self):
        return (yield from self._handler.mqtt_deliver_next_message())

    @asyncio.coroutine
    def acknowledge_delivery(self, packet_id):
        yield from self._handler.mqtt_acknowledge_delivery(packet_id)

    @asyncio.coroutine
    def _connect_coro(self):
        sc = None
        reader = None
        writer = None
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        self.session.username = uri_attributes.username
        self.session.password = uri_attributes.password
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == 'mqtts' else 1883

        if scheme in ('mqtts', 'wss'):
            if self.session.cafile is None or self.session.cafile == '':
                self.logger.warn(
                    "TLS connection can't be estabilshed, no certificate file (.cert) given"
                )
                raise ClientException(
                    "TLS connection can't be estabilshed, no certificate file (.cert) given"
                )
            sc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
                                            cafile=self.session.cafile,
                                            capath=self.session.capath,
                                            cadata=self.session.cadata)
            if 'certfile' in self.config and 'keyfile' in self.config:
                sc.load_cert_chain(self.config['certfile'],
                                   self.config['keyfile'])
            kwargs['ssl'] = sc

        # Open connection
        try:
            if scheme in ('mqtt', 'mqtts'):
                conn_reader, conn_writer = \
                    yield from asyncio.open_connection(self.session.remote_address, self.session.remote_port, **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ('ws', 'wss'):
                websocket = yield from websockets.connect(
                    self.session.broker_uri, subprotocols=['mqtt'], **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
        except Exception as e:
            self.logger.warn("connection failed: %s" % e)
            self.session.transitions.disconnect()
            raise ConnectException("connection Failed: %s" % e)

        return_code = None
        try:
            connect_packet = self.build_connect_packet()
            yield from connect_packet.to_stream(writer)
            self.logger.debug(" -out-> " + repr(connect_packet))

            connack = yield from ConnackPacket.from_stream(reader)
            self.logger.debug(" <-in-- " + repr(connack))
            return_code = connack.variable_header.return_code
        except Exception as e:
            self.logger.warn("connection failed: %s" % e)
            self.session.transitions.disconnect()
            raise ClientException("connection Failed: %s" % e)

        if return_code is not CONNECTION_ACCEPTED:
            yield from self._handler.stop()
            self.session.transitions.disconnect()
            self.logger.warn("Connection rejected with code '%s'" %
                             return_code)
            exc = ConnectException("Connection rejected by broker")
            exc.return_code = return_code
            raise exc
        else:
            # Handle MQTT protocol
            self._handler = ClientProtocolHandler(reader,
                                                  writer,
                                                  loop=self._loop)
            self._handler.attach_to_session(self.session)
            yield from self._handler.start()
            self.session.transitions.connect()
            self.logger.debug(
                "connected to %s:%s" %
                (self.session.remote_address, self.session.remote_port))

    def build_connect_packet(self):
        vh = ConnectVariableHeader()
        payload = ConnectPayload()

        vh.keep_alive = self.session.keep_alive
        vh.clean_session_flag = self.session.clean_session
        vh.will_retain_flag = self.session.will_retain
        payload.client_id = self.session.client_id

        if self.session.username:
            vh.username_flag = True
            payload.username = self.session.username
        else:
            vh.username_flag = False

        if self.session.password:
            vh.password_flag = True
            payload.password = self.session.password
        else:
            vh.password_flag = False
        if self.session.will_flag:
            vh.will_flag = True
            vh.will_qos = self.session.will_qos
            payload.will_message = self.session.will_message
            payload.will_topic = self.session.will_topic
        else:
            vh.will_flag = False

        header = MQTTFixedHeader(CONNECT, 0x00)
        packet = ConnectPacket(header, vh, payload)
        return packet

    @asyncio.coroutine
    def handle_connection_close(self):
        self.logger.debug("Watch broker disconnection")
        yield from self._handler.wait_disconnect()
        self.logger.debug("Handle broker disconnection")
        yield from self._handler.stop()
        self.session.transitions.disconnect()
        self._connection_closed_future.set_result(None)

    def _initsession(self,
                     uri=None,
                     cleansession=None,
                     cafile=None,
                     capath=None,
                     cadata=None) -> Session:
        # Load config
        broker_conf = self.config.get('broker', dict()).copy()
        if uri:
            broker_conf['uri'] = uri
        if cafile:
            broker_conf['cafile'] = cafile
        elif 'cafile' not in broker_conf:
            broker_conf['cafile'] = None
        if capath:
            broker_conf['capath'] = capath
        elif 'capath' not in broker_conf:
            broker_conf['capath'] = None
        if cadata:
            broker_conf['cadata'] = cadata
        elif 'cadata' not in broker_conf:
            broker_conf['cadata'] = None

        if cleansession is not None:
            broker_conf['cleansession'] = cleansession

        for key in ['uri']:
            if not_in_dict_or_none(broker_conf, key):
                raise ClientException("Missing connection parameter '%s'" %
                                      key)

        s = Session()
        s.broker_uri = uri
        s.client_id = self.client_id
        s.cafile = broker_conf['cafile']
        s.capath = broker_conf['capath']
        s.cadata = broker_conf['cadata']
        if cleansession is not None:
            s.clean_session = cleansession
        else:
            s.clean_session = self.config.get('cleansession', True)
        s.keep_alive = self.config['keep_alive'] - self.config['ping_delay']
        if 'will' in self.config:
            s.will_flag = True
            s.will_retain = self.config['will']['retain']
            s.will_topic = self.config['will']['topic']
            s.will_message = self.config['will']['message']
            s.will_qos = self.config['will']['qos']
        else:
            s.will_flag = False
            s.will_retain = False
            s.will_topic = None
            s.will_message = None
        return s
Ejemplo n.º 6
0
class MQTTClient:
    """
        MQTT client implementation.

        MQTTClient instances provides API for connecting to a broker and send/receive messages using the MQTT protocol.

        :param client_id: MQTT client ID to use when connecting to the broker. If none, it will generated randomly by :func:`hbmqtt.utils.gen_client_id`
        :param config: Client configuration
        :param loop: asynio loop to use
        :return: class instance
    """

    def __init__(self, client_id=None, config=None, loop=None):
        self.logger = logging.getLogger(__name__)
        self.config = _defaults
        if config is not None:
            self.config.update(config)
        if client_id is not None:
            self.client_id = client_id
        else:
            from hbmqtt.utils import gen_client_id
            self.client_id = gen_client_id()
            self.logger.debug("Using generated client ID : %s" % self.client_id)

        if loop is not None:
            self._loop = loop
        else:
            self._loop = asyncio.get_event_loop()
        self.session = None
        self._handler = None
        self._disconnect_task = None
        self._connected_state = asyncio.Event()

        # Init plugins manager
        context = ClientContext()
        context.config = self.config
        self.plugins_manager = PluginManager('hbmqtt.client.plugins', context)
        self.client_tasks = deque()


    @asyncio.coroutine
    def connect(self,
                uri=None,
                cleansession=None,
                cafile=None,
                capath=None,
                cadata=None):
        """
            Connect to a remote broker.

            At first, a network connection is established with the server using the given protocol (``mqtt``, ``mqtts``, ``ws`` or ``wss``). Once the socket is connected, a `CONNECT <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028>`_ message is sent with the requested informations.

            This method is a *coroutine*.

            :param uri: Broker URI connection, conforming to `MQTT URI scheme <https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>`_. Uses ``uri`` config attribute by default.
            :param cleansession: MQTT CONNECT clean session flag
            :param cafile: server certificate authority file (optional, used for secured connection)
            :param capath: server certificate authority path (optional, used for secured connection)
            :param cadata: server certificate authority data (optional, used for secured connection)
            :return: `CONNACK <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718033>`_ return code
            :raise: :class:`hbmqtt.client.ConnectException` if connection fails
        """

        self.session = self._initsession(uri, cleansession, cafile, capath, cadata)
        self.logger.debug("Connect to: %s" % uri)

        try:
            return (yield from self._do_connect())
        except BaseException as be:
            self.logger.warning("Connection failed: %r" % be)
            auto_reconnect = self.config.get('auto_reconnect', False)
            if not auto_reconnect:
                raise
            else:
                return (yield from self.reconnect())

    @asyncio.coroutine
    def disconnect(self):
        """
            Disconnect from the connected broker.

            This method sends a `DISCONNECT <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718090>`_ message and closes the network socket.

            This method is a *coroutine*.
        """

        if self.session.transitions.is_connected():
            if not self._disconnect_task.done():
                self._disconnect_task.cancel()
            yield from self._handler.mqtt_disconnect()
            self._connected_state.clear()
            yield from self._handler.stop()
            self.session.transitions.disconnect()
        else:
            self.logger.warn("Client session is not currently connected, ignoring call")

    @asyncio.coroutine
    def reconnect(self, cleansession=None):
        """
            Reconnect a previously connected broker.

            Reconnection tries to establish a network connection and send a `CONNECT <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028>`_ message.
            Retries interval and attempts can be controled with the ``reconnect_max_interval`` and ``reconnect_retries`` configuration parameters.

            This method is a *coroutine*.

            :param cleansession: clean session flag used in MQTT CONNECT messages sent for reconnections.
            :return: `CONNACK <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718033>`_ return code
            :raise: :class:`hbmqtt.client.ConnectException` if re-connection fails after max retries.
        """

        if self.session.transitions.is_connected():
            self.logger.warn("Client already connected")
            return CONNECTION_ACCEPTED

        if cleansession:
            self.session.clean_session = cleansession
        self.logger.debug("Reconnecting with session parameters: %s" % self.session)
        reconnect_max_interval = self.config.get('reconnect_max_interval', 10)
        reconnect_retries = self.config.get('reconnect_retries', 5)
        nb_attempt = 1
        yield from asyncio.sleep(1, loop=self._loop)
        while True:
            try:
                self.logger.debug("Reconnect attempt %d ..." % nb_attempt)
                return (yield from self._do_connect())
            except BaseException as e:
                self.logger.warning("Reconnection attempt failed: %r" % e)
                if nb_attempt > reconnect_retries:
                    self.logger.error("Maximum number of connection attempts reached. Reconnection aborted")
                    raise ConnectException("Too many connection attempts failed")
                exp = 2 ** nb_attempt
                delay = exp if exp < reconnect_max_interval else reconnect_max_interval
                self.logger.debug("Waiting %d second before next attempt" % delay)
                yield from asyncio.sleep(delay, loop=self._loop)
                nb_attempt += 1


    @asyncio.coroutine
    def _do_connect(self):
        return_code = yield from self._connect_coro()
        self._disconnect_task = ensure_future(self.handle_connection_close(), loop=self._loop)
        return return_code

    @mqtt_connected
    @asyncio.coroutine
    def ping(self):
        """
            Ping the broker.

            Send a MQTT `PINGREQ <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718081>`_ message for response.

            This method is a *coroutine*.
        """

        if self.session.transitions.is_connected():
            yield from self._handler.mqtt_ping()
        else:
            self.logger.warn("MQTT PING request incompatible with current session state '%s'" %
                             self.session.transitions.state)

    @mqtt_connected
    @asyncio.coroutine
    def publish(self, topic, message, qos=None, retain=None):
        """
            Publish a message to the broker.

            Send a MQTT `PUBLISH <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718037>`_ message and wait for acknowledgment depending on Quality Of Service

            This method is a *coroutine*.

            :param topic: topic name to which message data is published
            :param message: payload message (as bytes) to send.
            :param qos: requested publish quality of service : QOS_0, QOS_1 or QOS_2. Defaults to ``default_qos`` config parameter or QOS_0.
            :param retain: retain flag. Defaults to ``default_retain`` config parameter or False.
        """

        def get_retain_and_qos():
            if qos:
                assert qos in (QOS_0, QOS_1, QOS_2)
                _qos = qos
            else:
                _qos = self.config['default_qos']
                try:
                    _qos = self.config['topics'][topic]['qos']
                except KeyError:
                    pass
            if retain:
                _retain = retain
            else:
                _retain = self.config['default_retain']
                try:
                    _retain = self.config['topics'][topic]['retain']
                except KeyError:
                    pass
            return _qos, _retain
        (app_qos, app_retain) = get_retain_and_qos()
        return (yield from self._handler.mqtt_publish(topic, message, app_qos, app_retain))

    @mqtt_connected
    @asyncio.coroutine
    def subscribe(self, topics):
        """
            Subscribe to some topics.

            Send a MQTT `SUBSCRIBE <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718063>`_ message and wait for broker acknowledgment.

            This method is a *coroutine*.

            :param topics: array of topics pattern to subscribe with associated QoS.
            :return: `SUBACK <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718068>`_ message return code.

            Example of ``topics`` argument expected structure:
            ::

                [
                    ('$SYS/broker/uptime', QOS_1),
                    ('$SYS/broker/load/#', QOS_2),
                ]
        """
        return (yield from self._handler.mqtt_subscribe(topics, self.session.next_packet_id))

    @mqtt_connected
    @asyncio.coroutine
    def unsubscribe(self, topics):
        """
            Unsubscribe from some topics.

            Send a MQTT `UNSUBSCRIBE <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718072>`_ message and wait for broker `UNSUBACK <http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718077>`_ message.

            This method is a *coroutine*.

            :param topics: array of topics to unsubscribe from.

            Example of ``topics`` argument expected structure:
            ::

                ['$SYS/broker/uptime', QOS_1), '$SYS/broker/load/#', QOS_2]
        """
        yield from self._handler.mqtt_unsubscribe(topics, self.session.next_packet_id)

    @asyncio.coroutine
    def deliver_message(self, timeout=None):
        """
            Deliver next received message.

            Deliver next message received from the broker. If no message is available, this methods waits until next message arrives or ``timeout`` occurs.

            This method is a *coroutine*.

            :param timeout: maximum number of seconds to wait before returning. If timeout is not specified or None, there is no limit to the wait time until next message arrives.
            :return: instance of :class:`hbmqtt.session.ApplicationMessage` containing received message information flow.
        """
        deliver_task = ensure_future(self._handler.mqtt_deliver_next_message(), loop=self._loop)
        self.client_tasks.append(deliver_task)
        self.logger.debug("Waiting message delivery")
        done, pending = yield from asyncio.wait([deliver_task], loop=self._loop, return_when=asyncio.FIRST_EXCEPTION, timeout=timeout)
        if pending:
            #timeout occured before message received
            deliver_task.cancel()
        if deliver_task.exception():
            raise deliver_task.exception()
        self.client_tasks.pop()
        return deliver_task.result()

    @asyncio.coroutine
    def _connect_coro(self):
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        secure = True if scheme in ('mqtts', 'wss') else False        
        self.session.username = uri_attributes.username
        self.session.password = uri_attributes.password
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == 'mqtts' else 1883
        if scheme in ('ws', 'wss') and not self.session.remote_port:
            self.session.remote_port = 443 if scheme == 'wss' else 80
        if scheme in ('ws', 'wss'):
            # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3
            uri = (scheme, self.session.remote_address + ":" + str(self.session.remote_port), uri_attributes[2],
                   uri_attributes[3], uri_attributes[4], uri_attributes[5])
            self.session.broker_uri = urlunparse(uri)
        # Init protocol handler
        #if not self._handler:
        self._handler = ClientProtocolHandler(self.plugins_manager, loop=self._loop)

        if secure:
            if self.session.cafile is None or self.session.cafile == '':
                self.logger.warn("TLS connection can't be estabilshed, no certificate file (.cert) given")
                raise ClientException("TLS connection can't be estabilshed, no certificate file (.cert) given")
            sc = ssl.create_default_context(
                ssl.Purpose.SERVER_AUTH,
                cafile=self.session.cafile,
                capath=self.session.capath,
                cadata=self.session.cadata)
            if 'certfile' in self.config and 'keyfile' in self.config:
                sc.load_cert_chain(self.config['certfile'], self.config['keyfile'])
            kwargs['ssl'] = sc

        try:
            reader = None
            writer = None
            self._connected_state.clear()
            # Open connection
            if scheme in ('mqtt', 'mqtts'):
                conn_reader, conn_writer = \
                    yield from asyncio.open_connection(
                        self.session.remote_address,
                        self.session.remote_port, loop=self._loop, **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ('ws', 'wss'):
                websocket = yield from websockets.connect(
                    self.session.broker_uri,
                    subprotocols=['mqtt'],
                    loop=self._loop,
                    **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
            # Start MQTT protocol
            self._handler.attach(self.session, reader, writer)
            return_code = yield from self._handler.mqtt_connect()
            if return_code is not CONNECTION_ACCEPTED:
                self.session.transitions.disconnect()
                self.logger.warning("Connection rejected with code '%s'" % return_code)
                exc = ConnectException("Connection rejected by broker")
                exc.return_code = return_code
                raise exc
            else:
                # Handle MQTT protocol
                yield from self._handler.start()
                self.session.transitions.connect()
                self._connected_state.set()
                self.logger.debug("connected to %s:%s" % (self.session.remote_address, self.session.remote_port))
            return return_code
        except InvalidURI as iuri:
            self.logger.warn("connection failed: invalid URI '%s'" % self.session.broker_uri)
            self.session.transitions.disconnect()
            raise ConnectException("connection failed: invalid URI '%s'" % self.session.broker_uri, iuri)
        except InvalidHandshake as ihs:
            self.logger.warn("connection failed: invalid websocket handshake")
            self.session.transitions.disconnect()
            raise ConnectException("connection failed: invalid websocket handshake", ihs)
        except (ProtocolHandlerException, ConnectionError, OSError) as e:
            self.logger.warn("MQTT connection failed: %r" % e)
            self.session.transitions.disconnect()
            raise ConnectException(e)

    @asyncio.coroutine
    def handle_connection_close(self):
        self.logger.debug("Watch broker disconnection")
        # Wait for disconnection from broker (like connection lost)
        yield from self._handler.wait_disconnect()
        self.logger.warning("Disconnected from broker")

        # Block client API
        self._connected_state.clear()

        # stop an clean handler
        #yield from self._handler.stop()
        self._handler.detach()
        self.session.transitions.disconnect()

        if self.config.get('auto_reconnect', False):
            # Try reconnection
            self.logger.debug("Auto-reconnecting")
            try:
                yield from self.reconnect()
            except ConnectException:
                # Cancel client pending tasks
                while self.client_tasks:
                    self.client_tasks.popleft().set_exception(ClientException("Connection lost"))
        else:
            # Cancel client pending tasks
            while self.client_tasks:
                self.client_tasks.popleft().set_exception(ClientException("Connection lost"))

    def _initsession(
            self,
            uri=None,
            cleansession=None,
            cafile=None,
            capath=None,
            cadata=None) -> Session:
        # Load config
        broker_conf = self.config.get('broker', dict()).copy()
        if uri:
            broker_conf['uri'] = uri
        if cafile:
            broker_conf['cafile'] = cafile
        elif 'cafile' not in broker_conf:
            broker_conf['cafile'] = None
        if capath:
            broker_conf['capath'] = capath
        elif 'capath' not in broker_conf:
            broker_conf['capath'] = None
        if cadata:
            broker_conf['cadata'] = cadata
        elif 'cadata' not in broker_conf:
            broker_conf['cadata'] = None

        if cleansession is not None:
            broker_conf['cleansession'] = cleansession

        for key in ['uri']:
            if not_in_dict_or_none(broker_conf, key):
                raise ClientException("Missing connection parameter '%s'" % key)

        s = Session()
        s.broker_uri = uri
        s.client_id = self.client_id
        s.cafile = broker_conf['cafile']
        s.capath = broker_conf['capath']
        s.cadata = broker_conf['cadata']
        if cleansession is not None:
            s.clean_session = cleansession
        else:
            s.clean_session = self.config.get('cleansession', True)
        s.keep_alive = self.config['keep_alive'] - self.config['ping_delay']
        if 'will' in self.config:
            s.will_flag = True
            s.will_retain = self.config['will']['retain']
            s.will_topic = self.config['will']['topic']
            s.will_message = self.config['will']['message']
            s.will_qos = self.config['will']['qos']
        else:
            s.will_flag = False
            s.will_retain = False
            s.will_topic = None
            s.will_message = None
        return s
Ejemplo n.º 7
0
    def _connect_coro(self):
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        secure = True if scheme in ('mqtts', 'wss') else False        
        self.session.username = uri_attributes.username
        self.session.password = uri_attributes.password
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == 'mqtts' else 1883
        if scheme in ('ws', 'wss') and not self.session.remote_port:
            self.session.remote_port = 443 if scheme == 'wss' else 80
        if scheme in ('ws', 'wss'):
            # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3
            uri = (scheme, self.session.remote_address + ":" + str(self.session.remote_port), uri_attributes[2],
                   uri_attributes[3], uri_attributes[4], uri_attributes[5])
            self.session.broker_uri = urlunparse(uri)
        # Init protocol handler
        #if not self._handler:
        self._handler = ClientProtocolHandler(self.plugins_manager, loop=self._loop)

        if secure:
            if self.session.cafile is None or self.session.cafile == '':
                self.logger.warn("TLS connection can't be estabilshed, no certificate file (.cert) given")
                raise ClientException("TLS connection can't be estabilshed, no certificate file (.cert) given")
            sc = ssl.create_default_context(
                ssl.Purpose.SERVER_AUTH,
                cafile=self.session.cafile,
                capath=self.session.capath,
                cadata=self.session.cadata)
            if 'certfile' in self.config and 'keyfile' in self.config:
                sc.load_cert_chain(self.config['certfile'], self.config['keyfile'])
            kwargs['ssl'] = sc

        try:
            reader = None
            writer = None
            self._connected_state.clear()
            # Open connection
            if scheme in ('mqtt', 'mqtts'):
                conn_reader, conn_writer = \
                    yield from asyncio.open_connection(
                        self.session.remote_address,
                        self.session.remote_port, loop=self._loop, **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ('ws', 'wss'):
                websocket = yield from websockets.connect(
                    self.session.broker_uri,
                    subprotocols=['mqtt'],
                    loop=self._loop,
                    **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
            # Start MQTT protocol
            self._handler.attach(self.session, reader, writer)
            return_code = yield from self._handler.mqtt_connect()
            if return_code is not CONNECTION_ACCEPTED:
                self.session.transitions.disconnect()
                self.logger.warning("Connection rejected with code '%s'" % return_code)
                exc = ConnectException("Connection rejected by broker")
                exc.return_code = return_code
                raise exc
            else:
                # Handle MQTT protocol
                yield from self._handler.start()
                self.session.transitions.connect()
                self._connected_state.set()
                self.logger.debug("connected to %s:%s" % (self.session.remote_address, self.session.remote_port))
            return return_code
        except InvalidURI as iuri:
            self.logger.warn("connection failed: invalid URI '%s'" % self.session.broker_uri)
            self.session.transitions.disconnect()
            raise ConnectException("connection failed: invalid URI '%s'" % self.session.broker_uri, iuri)
        except InvalidHandshake as ihs:
            self.logger.warn("connection failed: invalid websocket handshake")
            self.session.transitions.disconnect()
            raise ConnectException("connection failed: invalid websocket handshake", ihs)
        except (ProtocolHandlerException, ConnectionError, OSError) as e:
            self.logger.warn("MQTT connection failed: %r" % e)
            self.session.transitions.disconnect()
            raise ConnectException(e)
Ejemplo n.º 8
0
class MQTTClient:
    def __init__(self, client_id=None, config=None, loop=None):
        """

        :param config: Example yaml config
            broker:
                uri: mqtt:username@password//localhost:1883/
                cafile: somefile.cert  #Server authority file
                capath: /some/path # certficate file path
                cadata: certificate as string data
            keep_alive: 60
            cleansession: true
            will:
                retain: false
                topic: some/topic
                message: Will message
                qos: 0
            default_qos: 0
            default_retain: false
            topics:
                a/b:
                    qos: 2
                    retain: true
        :param loop:
        :return:
        """
        self.logger = logging.getLogger(__name__)
        self.config = _defaults
        if config is not None:
            self.config.update(config)
        if client_id is not None:
            self.client_id = client_id
        else:
            from hbmqtt.utils import gen_client_id
            self.client_id = gen_client_id()
            self.logger.debug("Using generated client ID : %s" % self.client_id)

        if loop is not None:
            self._loop = loop
        else:
            self._loop = asyncio.get_event_loop()
        self.session = None
        self._handler = None
        self._disconnect_task = None
        self._connection_closed_future = None


    @asyncio.coroutine
    def connect(self,
                uri=None,
                cleansession=None,
                cafile=None,
                capath=None,
                cadata=None):
        """
        Connect to a remote broker
        :param uri: Broker URI connection, conforming to `MQTT URI scheme <https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme>`_.
        :param cleansession: MQTT CONNECT clean session flaf
        :param cafile: server certificate authority file
        :return:
        """
        self.session = self._initsession(uri, cleansession, cafile, capath, cadata)
        self.logger.debug("Connect to: %s" % uri)

        return_code = yield from self._connect_coro()
        self._connection_closed_future = asyncio.Future(loop=self._loop)
        self._disconnect_task = asyncio.Task(self.handle_connection_close())
        return self._connection_closed_future

    @asyncio.coroutine
    def disconnect(self):
        if self.session.transitions.is_connected():
            if not self._disconnect_task.done():
                self._disconnect_task.cancel()
            yield from self._handler.mqtt_disconnect()
            yield from self._handler.stop()
            self._handler.detach_from_session()
            self.session.transitions.disconnect()
            self._connection_closed_future.set_result(None)
        else:
            self.logger.warn("Client session is not currently connected, ignoring call")

    @asyncio.coroutine
    def reconnect(self, cleansession=False):
        if self.session.transitions.is_connected():
            self.logger.warn("Client already connected")
            return CONNECTION_ACCEPTED

        self.session.clean_session = cleansession
        self.logger.debug("Reconnecting with session parameters: %s" % self.session)

        return_code = yield from self._connect_coro()
        self._connection_closed_future = asyncio.Future(loop=self._loop)
        self._disconnect_task = asyncio.Task(self.handle_connection_close())
        return self._connection_closed_future

    @asyncio.coroutine
    def ping(self):
        """
        Send a MQTT ping request and wait for response
        :return: None
        """
        if self.session.transitions.is_connected():
            yield from self._handler.mqtt_ping()
        else:
            self.logger.warn("MQTT PING request incompatible with current session state '%s'" %
                             self.session.transitions.state)

    @asyncio.coroutine
    def publish(self, topic, message, qos=None, retain=None):
        def get_retain_and_qos():
            if qos:
                _qos = qos
            else:
                _qos = self.config['default_qos']
                try:
                    _qos = self.config['topics'][topic]['qos']
                except KeyError:
                    pass
            if retain:
                _retain = retain
            else:
                _retain = self.config['default_retain']
                try:
                    _retain = self.config['topics'][topic]['retain']
                except KeyError:
                    pass
            return _qos, _retain
        if not self.session.transitions.is_connected():
            self.logger.warn("publish MQTT message while not connected to broker, message may be lost")
        (app_qos, app_retain) = get_retain_and_qos()
        if app_qos == 0:
            yield from self._handler.mqtt_publish(topic, message, 0x00, app_retain)
        if app_qos == 1:
            yield from self._handler.mqtt_publish(topic, message, 0x01, app_retain)
        if app_qos == 2:
            yield from self._handler.mqtt_publish(topic, message, 0x02, app_retain)

    @asyncio.coroutine
    def subscribe(self, topics):
        if not self.session.transitions.is_connected():
            self.logger.warn("subscribe while not connected to broker, message may be lost")
        return (yield from self._handler.mqtt_subscribe(topics, self.session.next_packet_id))

    @asyncio.coroutine
    def unsubscribe(self, topics):
        if not self.session.transitions.is_connected():
            self.logger.warn("unsubscribe while not connected to broker, message may be lost")
        yield from self._handler.mqtt_unsubscribe(topics, self.session.next_packet_id)

    @asyncio.coroutine
    def deliver_message(self):
        return (yield from self._handler.mqtt_deliver_next_message())

    @asyncio.coroutine
    def acknowledge_delivery(self, packet_id):
        yield from self._handler.mqtt_acknowledge_delivery(packet_id)

    @asyncio.coroutine
    def _connect_coro(self):
        sc = None
        reader = None
        writer = None
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        self.session.username = uri_attributes.username
        self.session.password = uri_attributes.password
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == 'mqtts' else 1883

        if scheme in ('mqtts', 'wss'):
            if self.session.cafile is None or self.session.cafile == '':
                self.logger.warn("TLS connection can't be estabilshed, no certificate file (.cert) given")
                raise ClientException("TLS connection can't be estabilshed, no certificate file (.cert) given")
            sc = ssl.create_default_context(
                ssl.Purpose.SERVER_AUTH,
                cafile=self.session.cafile,
                capath=self.session.capath,
                cadata=self.session.cadata)
            if 'certfile' in self.config and 'keyfile' in self.config:
                sc.load_cert_chain(self.config['certfile'], self.config['keyfile'])
            kwargs['ssl'] = sc

        # Open connection
        try:
            if scheme in ('mqtt', 'mqtts'):
                conn_reader, conn_writer = \
                    yield from asyncio.open_connection(self.session.remote_address, self.session.remote_port, **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ('ws', 'wss'):
                websocket = yield from websockets.connect(self.session.broker_uri, subprotocols=['mqtt'], **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
        except Exception as e:
            self.logger.warn("connection failed: %s" % e)
            self.session.transitions.disconnect()
            raise ConnectException("connection Failed: %s" % e)

        return_code = None
        try :
            connect_packet = self.build_connect_packet()
            yield from connect_packet.to_stream(writer)
            self.logger.debug(" -out-> " + repr(connect_packet))

            connack = yield from ConnackPacket.from_stream(reader)
            self.logger.debug(" <-in-- " + repr(connack))
            return_code = connack.variable_header.return_code
        except Exception as e:
            self.logger.warn("connection failed: %s" % e)
            self.session.transitions.disconnect()
            raise ClientException("connection Failed: %s" % e)

        if return_code is not CONNECTION_ACCEPTED:
            yield from self._handler.stop()
            self.session.transitions.disconnect()
            self.logger.warn("Connection rejected with code '%s'" % return_code)
            exc = ConnectException("Connection rejected by broker")
            exc.return_code = return_code
            raise exc
        else:
            # Handle MQTT protocol
            self._handler = ClientProtocolHandler(reader, writer, loop=self._loop)
            self._handler.attach_to_session(self.session)
            yield from self._handler.start()
            self.session.transitions.connect()
            self.logger.debug("connected to %s:%s" % (self.session.remote_address, self.session.remote_port))

    def build_connect_packet(self):
        vh = ConnectVariableHeader()
        payload = ConnectPayload()

        vh.keep_alive = self.session.keep_alive
        vh.clean_session_flag = self.session.clean_session
        vh.will_retain_flag = self.session.will_retain
        payload.client_id = self.session.client_id

        if self.session.username:
            vh.username_flag = True
            payload.username = self.session.username
        else:
            vh.username_flag = False

        if self.session.password:
            vh.password_flag = True
            payload.password = self.session.password
        else:
            vh.password_flag = False
        if self.session.will_flag:
            vh.will_flag = True
            vh.will_qos = self.session.will_qos
            payload.will_message = self.session.will_message
            payload.will_topic = self.session.will_topic
        else:
            vh.will_flag = False

        header = MQTTFixedHeader(CONNECT, 0x00)
        packet = ConnectPacket(header, vh, payload)
        return packet

    @asyncio.coroutine
    def handle_connection_close(self):
        self.logger.debug("Watch broker disconnection")
        yield from self._handler.wait_disconnect()
        self.logger.debug("Handle broker disconnection")
        yield from self._handler.stop()
        self.session.transitions.disconnect()
        self._connection_closed_future.set_result(None)

    def _initsession(
            self,
            uri=None,
            cleansession=None,
            cafile=None,
            capath=None,
            cadata=None) -> Session:
        # Load config
        broker_conf = self.config.get('broker', dict()).copy()
        if uri:
            broker_conf['uri'] = uri
        if cafile:
            broker_conf['cafile'] = cafile
        elif 'cafile' not in broker_conf:
            broker_conf['cafile'] = None
        if capath:
            broker_conf['capath'] = capath
        elif 'capath' not in broker_conf:
            broker_conf['capath'] = None
        if cadata:
            broker_conf['cadata'] = cadata
        elif 'cadata' not in broker_conf:
            broker_conf['cadata'] = None

        if cleansession is not None:
            broker_conf['cleansession'] = cleansession

        for key in ['uri']:
            if not_in_dict_or_none(broker_conf, key):
                raise ClientException("Missing connection parameter '%s'" % key)

        s = Session()
        s.broker_uri = uri
        s.client_id = self.client_id
        s.cafile = broker_conf['cafile']
        s.capath = broker_conf['capath']
        s.cadata = broker_conf['cadata']
        if cleansession is not None:
            s.clean_session = cleansession
        else:
            s.clean_session = self.config.get('cleansession', True)
        s.keep_alive = self.config['keep_alive'] - self.config['ping_delay']
        if 'will' in self.config:
            s.will_flag = True
            s.will_retain = self.config['will']['retain']
            s.will_topic = self.config['will']['topic']
            s.will_message = self.config['will']['message']
            s.will_qos = self.config['will']['qos']
        else:
            s.will_flag = False
            s.will_retain = False
            s.will_topic = None
            s.will_message = None
        return s
Ejemplo n.º 9
0
    def _connect_coro(self):
        sc = None
        reader = None
        writer = None
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        self.session.username = uri_attributes.username
        self.session.password = uri_attributes.password
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == 'mqtts' else 1883

        if scheme in ('mqtts', 'wss'):
            if self.session.cafile is None or self.session.cafile == '':
                self.logger.warn("TLS connection can't be estabilshed, no certificate file (.cert) given")
                raise ClientException("TLS connection can't be estabilshed, no certificate file (.cert) given")
            sc = ssl.create_default_context(
                ssl.Purpose.SERVER_AUTH,
                cafile=self.session.cafile,
                capath=self.session.capath,
                cadata=self.session.cadata)
            if 'certfile' in self.config and 'keyfile' in self.config:
                sc.load_cert_chain(self.config['certfile'], self.config['keyfile'])
            kwargs['ssl'] = sc

        # Open connection
        try:
            if scheme in ('mqtt', 'mqtts'):
                conn_reader, conn_writer = \
                    yield from asyncio.open_connection(self.session.remote_address, self.session.remote_port, **kwargs)
                reader = StreamReaderAdapter(conn_reader)
                writer = StreamWriterAdapter(conn_writer)
            elif scheme in ('ws', 'wss'):
                websocket = yield from websockets.connect(self.session.broker_uri, subprotocols=['mqtt'], **kwargs)
                reader = WebSocketsReader(websocket)
                writer = WebSocketsWriter(websocket)
        except Exception as e:
            self.logger.warn("connection failed: %s" % e)
            self.session.transitions.disconnect()
            raise ConnectException("connection Failed: %s" % e)

        return_code = None
        try :
            connect_packet = self.build_connect_packet()
            yield from connect_packet.to_stream(writer)
            self.logger.debug(" -out-> " + repr(connect_packet))

            connack = yield from ConnackPacket.from_stream(reader)
            self.logger.debug(" <-in-- " + repr(connack))
            return_code = connack.variable_header.return_code
        except Exception as e:
            self.logger.warn("connection failed: %s" % e)
            self.session.transitions.disconnect()
            raise ClientException("connection Failed: %s" % e)

        if return_code is not CONNECTION_ACCEPTED:
            yield from self._handler.stop()
            self.session.transitions.disconnect()
            self.logger.warn("Connection rejected with code '%s'" % return_code)
            exc = ConnectException("Connection rejected by broker")
            exc.return_code = return_code
            raise exc
        else:
            # Handle MQTT protocol
            self._handler = ClientProtocolHandler(reader, writer, loop=self._loop)
            self._handler.attach_to_session(self.session)
            yield from self._handler.start()
            self.session.transitions.connect()
            self.logger.debug("connected to %s:%s" % (self.session.remote_address, self.session.remote_port))
Ejemplo n.º 10
0
    async def _connect_coro(self):
        kwargs = dict()

        # Decode URI attributes
        uri_attributes = urlparse(self.session.broker_uri)
        scheme = uri_attributes.scheme
        secure = True if scheme in ('mqtts', 'wss') else False
        self.session.username = self.session.username if self.session.username else uri_attributes.username
        self.session.password = self.session.password if self.session.password else uri_attributes.password
        self.session.remote_address = uri_attributes.hostname
        self.session.remote_port = uri_attributes.port
        if scheme in ('mqtt', 'mqtts') and not self.session.remote_port:
            self.session.remote_port = 8883 if scheme == 'mqtts' else 1883
        if scheme in ('ws', 'wss') and not self.session.remote_port:
            self.session.remote_port = 443 if scheme == 'wss' else 80
        if scheme in ('ws', 'wss'):
            # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3
            uri = (scheme, self.session.remote_address + ":" +
                   str(self.session.remote_port), uri_attributes[2],
                   uri_attributes[3], uri_attributes[4], uri_attributes[5])
            self.session.broker_uri = urlunparse(uri)
        # Init protocol handler
        #if not self._handler:
        self._handler = ClientProtocolHandler(self.plugins_manager)

        if secure:
            sc = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,
                                            cafile=self.session.cafile,
                                            capath=self.session.capath,
                                            cadata=self.session.cadata)
            if 'certfile' in self.config and 'keyfile' in self.config:
                sc.load_cert_chain(self.config['certfile'],
                                   self.config['keyfile'])
            if 'check_hostname' in self.config and isinstance(
                    self.config['check_hostname'], bool):
                sc.check_hostname = self.config['check_hostname']
            kwargs['ssl_context'] = sc
            kwargs['autostart_tls'] = True

        try:
            adapter = None
            self._connected_state.clear()
            # Open connection
            if scheme in ('mqtt', 'mqtts'):
                conn = \
                    await anyio.connect_tcp(
                        self.session.remote_address,
                        self.session.remote_port, **kwargs)
                if secure:
                    await conn.start_tls()
                adapter = StreamAdapter(conn)
            elif scheme in ('ws', 'wss'):
                if kwargs.pop('autostart_tls', False):
                    kwargs['ssl'] = kwargs.pop('ssl_context')
                websocket = await create_websocket(self.session.broker_uri,
                                                   subprotocols=['mqtt'],
                                                   headers=self.extra_headers,
                                                   **kwargs)
                adapter = WebSocketsAdapter(websocket)
            # Start MQTT protocol
            await self._handler.attach(self.session, adapter)
            try:
                return_code = await self._handler.mqtt_connect()
            except NoDataException:
                self.logger.warning("Connection broken by broker")
                exc = ConnectException("Connection broken by broker")
                raise exc
            if return_code is not CONNECTION_ACCEPTED:
                self.session.transitions.disconnect()
                self.logger.warning("Connection rejected with code '%s'",
                                    return_code)
                exc = ConnectException("Connection rejected by broker")
                exc.return_code = return_code
                raise exc
            else:
                # Handle MQTT protocol
                await self._handler.start()
                self.session.transitions.connect()
                await self._connected_state.set()
                self.logger.debug("connected to %s:%s",
                                  self.session.remote_address,
                                  self.session.remote_port)
            return return_code
        except ProtocolError as exc:
            self.logger.warning(
                "connection failed: invalid websocket handshake")
            self.session.transitions.disconnect()
            raise ConnectException(
                "connection failed: invalid websocket handshake") from exc
        except (ProtocolHandlerException, ConnectionError, OSError) as exc:
            self.logger.warning("MQTT connection failed")
            self.session.transitions.disconnect()
            raise ConnectException from exc