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(loop=self._loop) # 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.warning( "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.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 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.warning( "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. :raises: :class:`asyncio.TimeoutError` if timeout occurs before a message is delivered """ 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 deliver_task in done: self.client_tasks.pop() return deliver_task.result() else: #timeout occured before message received deliver_task.cancel() raise asyncio.TimeoutError @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.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) @asyncio.coroutine def handle_connection_close(self): def cancel_tasks(): 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) 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 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
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
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
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