class MQTTClient(Entity): """ A helper class for MQTT. Handles all the connection details. Returned to the library or module that calls self._MQTTYombo.new(). .. code-block:: python self.my_mqtt = self._MQTT.new(on_message_callback=self.mqtt_incoming, client_id="my_client_name") self.my_mqtt.subscribe("yombo/devices/+/get") # subscribe to a topic. + is a wildcard for a single section. """ def __init__(self, parent, hostname: Optional[str] = None, port: Optional[int] = None, username: Optional[str] = None, password: Optional[str] = None, use_ssl: Optional[bool] = None, version: Optional[str] = None, keepalive: Optional[int] = None, session_expiry: Optional[int] = None, receive_maximum: Optional[int] = None, user_property: Optional[Union[tuple, List[tuple]]] = None, last_will: Optional = None, maximum_packet_size: Optional[int] = None, on_message_callback: Callable = None, subscribe_callback: Callable = None, unsubscribe_callback: Callable = None, connected_callback: Optional[Callable] = None, disconnected_callback: Optional[Callable] = None, error_callback: Optional[Callable] = None, client_id: Optional[str] = None, password2: Optional[str] = None): """ Creates a new client connection to an MQTT broker. :param parent: A reference to the MQTT library. :param hostname: IP address or hostname to connect to. :param port: Port number to connect to. :param username: Username to connect as. Use "" to not use a username & password. :param password: Password to to connect with. Use "" to not use a password. :param use_ssl: Use SSL when attempting to connect to server, default is True. :param version: MQTT version to use, default: MQTTv50. Other: MQTTv311 :param keepalive: How often the connection should be checked that it's still alive. :param session_expiry: How many seconds the session should be valid. Defaults to 0. :param receive_maximum: The Client uses this value to limit the number of QoS 1 and QoS 2 publications that it is willing to process concurrently. :param user_property: Connection user_property. A tuple or list of tuples. :param last_will: Last will message generated by 'will()'. :param maximum_packet_size: The maximum size the mqtt payload should be, in size. :param on_message_callback: (required) method - Method to send messages to. :param connected_callback: method - If you want a function called when connected to server. :param disconnected_callback: method - If you want a function called when disconnected from server. :param subscribe_callback: method - This method will be called when successfully subscribed to topic. :param unsubscribe_callback: method - This method will be called when successfully unsubscribed from topic. :param error_callback: method - A function to call if something goes wrong. :param client_id: (default - random) - A client id to use for logging. :param password2: A second password to try. Used by MQTTYombo. :return: """ self._Entity_type: str = "MQTTClient" self._Entity_label_attribute: str = "client_id" super().__init__(parent) self.connected = False self.incoming_duplicates = deque([], 150) self.send_queue = deque() self.subscriptions = {} self.unsubscriptions = {} self.topics = {} # Store topics to resubscribe to self.hostname = hostname self.port = port self.username = username self.password = password self.password2 = password2 self.use_ssl = use_ssl self.version = version self.keepalive = keepalive self.session_expiry = session_expiry self.receive_maximum = receive_maximum self.user_property = user_property self.last_will = last_will self.maximum_packet_size = maximum_packet_size self.on_message_callback = on_message_callback self.subscribe_callback = subscribe_callback self.unsubscribe_callback = unsubscribe_callback self.connected_callback = connected_callback self.disconnected_callback = disconnected_callback self.error_callback = error_callback self.client_id = client_id client_options = { "receive_maximum": receive_maximum, "session_expiry_interval": session_expiry, "maximum_packet_size": maximum_packet_size, "user_property": user_property, } self.client = QClient( client_id, **{k: v for k, v in client_options.items() if v is not None}) self.client.set_auth_credentials(username, password.encode()) self.client.on_connect = self.on_connect self.client.on_message = self.on_message_callback self.client.on_disconnect = self.on_disconnect self.client.on_subscribe = self.on_subscribe @inlineCallbacks def connect(self): """Connects to the mqtt broker.""" d = self.as_deferred(self.do_connect()) yield d def as_deferred(self, f): return Deferred.fromFuture(asyncio.ensure_future(f)) async def do_connect(self): """Connects to the mqtt broker.""" await asyncio.create_task( self.client.connect(host=self.hostname, port=self.port)) def on_connect(self, client, flags, rc, properties): """Received a message.""" self.connected = True # Do subscribes for topic, kwargs in self.subscriptions.items(): self.client.subscribe(topic, **kwargs) for topic, kwargs in self.unsubscriptions.items(): self.client.unsubscribe(topic, **kwargs) # Do messages for message in self.send_queue: self.client.publish(message["topic"], **message["kwargs"]) if callable(self.connected_callback): self.connected_callback(properties=properties) def on_disconnect(self, client, packet, exc=None): """Disconnected notification.""" self.connected = False if callable(self.disconnected_callback): self.disconnected_callback(client=client, packet=packet) def on_message(self, client, topic, body, qos, properties): """Received a message.""" if callable(self.on_message_callback): self.on_message_callback(client=client, topic=topic, body=body, qos=qos, properties=properties) def on_subscribe(self, client, mid, qos, properties): """Received subscribe confirmation.""" if callable(self.subscribe_callback): self.subscribe_callback(client=client, mid=mid, qos=qos, properties=properties) def on_unsubscribe(self, client, mid, qos): """Received unsubscribe confirmation.""" if callable(self.unsubscribe_callback): self.unsubscribe_callback(client=client, mid=mid, qos=qos) def subscribe(self, topic: str, **kwargs): """ Subscribe to a topic. :param topic: :param kwargs: :return: """ if "qos" not in kwargs: kwargs["qos"] = 1 if self.session_expiry == 0: self.subscriptions[topic] = kwargs if self.connected is True: self.client.subscribe(topic, **kwargs) def unsubscribe(self, topic: str, **kwargs): """ Unsubscribe from topic. :param topic: Topic to unsubscribe from. :param kwargs: :return: """ if "qos" not in kwargs: kwargs["qos"] = 1 if self.connected is True: self.client.unsubscribe(topic, **kwargs) if self.session_expiry == 0: self.unsubscriptions[topic] = kwargs def publish(self, topic: str, message: Optional[str] = None, qos: Optional[int] = None, **kwargs): """ Publish a message to the MQTT broker. If not connected yet, will hold in a queue for later. :param topic: Topic to publish too. :param message: Message to send. :param qos: quality of service. :param kwargs: Any additional items to send to the qmqtt publish command. :return: """ if qos is None: qos = 1 if self.connected is True: self.client.publish(topic, payload=message, qos=qos, **kwargs) else: kwargs["message"] = message kwargs["qos"] = qos self.send_queue.append({"topic": topic, "kwargs": kwargs})
class GMQTT_Client(MQTT_Base): def __init__(self, mqtt_settings): MQTT_Base.__init__(self, mqtt_settings) self.mqtt_client = None def connect(self): MQTT_Base.connect(self) self.mqtt_client = MQTTClient( 'gmqtt' #self.mqtt_settings["MQTT_CLIENT_ID"] ) self.mqtt_client.on_connect = self._on_connect self.mqtt_client.on_message = self._on_message self.mqtt_client.on_disconnect = self._on_disconnect if self.mqtt_settings["MQTT_USERNAME"]: self.mqtt_client.set_auth_credentials( self.mqtt_settings["MQTT_USERNAME"], self.mqtt_settings["MQTT_PASSWORD"], ) def start(): try: logger.warning('Connecting to MQTT') asyncio.set_event_loop(self.event_loop) # self.event_loop.run_until_complete( # self.mqtt_client.connect(self.mqtt_settings["MQTT_BROKER"], self.mqtt_settings["MQTT_PORT"],keepalive=self.mqtt_settings["MQTT_KEEPALIVE"], version=MQTTv311) # ) logger.warning('Looping forever') self.event_loop.run_forever() logger.warning('Event loop stopped') #self.session.close() except Exception as e: logger.error('Error in event loop {}'.format(e)) self.event_loop = asyncio.new_event_loop() logger.warning("Starting MQTT thread") self._ws_thread = threading.Thread(target=start, args=()) self._ws_thread.daemon = True self._ws_thread.start() future = asyncio.run_coroutine_threadsafe( self.mqtt_client.connect( self.mqtt_settings["MQTT_BROKER"], self.mqtt_settings["MQTT_PORT"], keepalive=self.mqtt_settings["MQTT_KEEPALIVE"], version=MQTTv311), self.event_loop) def publish(self, topic, payload, retain, qos): MQTT_Base.publish(self, topic, payload, retain, qos) if self.mqtt_connected is True: wrapped = functools.partial(self.mqtt_client.publish, topic, payload, retain=retain, qos=qos) self.event_loop.call_soon_threadsafe(wrapped) else: logger.warning( "Device MQTT publish NOT CONNECTED: {}, retain {}, qos {}, payload: {}" .format(topic, retain, qos, payload)) # future = asyncio.run_coroutine_threadsafe( # self.mqtt_client.publish(topic, payload, retain=retain, qos=qos), # self.event_loop # ) def subscribe(self, topic, qos): # subclass to provide MQTT_Base.subscribe(self, topic, qos) self.mqtt_client.subscribe(topic, qos) def unsubscribe(self, topic): # subclass to provide MQTT_Base.unsubscribe(self, topic) self.mqtt_client.unsubscribe(topic) def set_will(self, will, topic, retain, qos): MQTT_Base.set_will(self, will, topic, retain, qos) #self.mqtt_client.will_set(will, topic, retain, qos) def _on_connect(self, client, flags, rc, properties): logger.info("MQTT On Connect: {}".format(rc)) self.mqtt_connected = rc == 0 def _on_message(self, client, topic, payload, qos, properties): #topic = msg.topic #payload = msg.payload.decode("utf-8") MQTT_Base._on_message(self, topic, payload, False, qos) def _on_disconnect(self, client, packet, exc=None): self.mqtt_connected = False # note, change this uses the property setter, do not really need to catch this in the base class logger.warning("MQTT Disconnection {} {} {}".format( client, packet, exc)) MQTT_Base._on_disconnect(self, 0)