class EcovacsMqtt: """Handle mqtt connections.""" def __init__(self, *, continent: str, country: str): self._subscribers: MutableMapping[str, VacuumBot] = {} self._port = 443 self._hostname = f"mq-{continent}.ecouser.net" if country.lower() == "cn": self._hostname = "mq.ecouser.net" self._client: Optional[Client] = None self._received_set_commands: MutableMapping[str, SetCommand] = TTLCache( maxsize=60 * 60, ttl=60) # pylint: disable=unused-argument async def _on_message(client: Client, topic: str, payload: bytes, qos: int, properties: Dict) -> None: _LOGGER.debug("Got message: topic=%s; payload=%s;", topic, payload.decode()) topic_split = topic.split("/") if topic.startswith("iot/atr"): await self._handle_atr(topic_split, payload) elif topic.startswith("iot/p2p"): self._handle_p2p(topic_split, payload) else: _LOGGER.debug("Got unsupported topic: %s", topic) self.__on_message = _on_message async def initialize(self, auth: RequestAuth) -> None: """Initialize MQTT.""" if self._client is not None: self.disconnect() client_id = f"{auth.user_id}@ecouser/{auth.resource}" self._client = Client(client_id) self._client.on_message = self.__on_message self._client.set_auth_credentials(auth.user_id, auth.token) ssl_ctx = ssl.create_default_context() ssl_ctx.check_hostname = False ssl_ctx.verify_mode = ssl.CERT_NONE await self._client.connect(self._hostname, self._port, ssl=ssl_ctx, version=MQTTv311) async def subscribe(self, vacuum_bot: VacuumBot) -> None: """Subscribe for messages for given vacuum.""" if self._client is None: raise NotInitializedError vacuum = vacuum_bot.vacuum self._client.subscribe(_get_subscriptions(vacuum)) self._subscribers[vacuum.did] = vacuum_bot def unsubscribe(self, vacuum_bot: VacuumBot) -> None: """Unsubscribe given vacuum.""" vacuum = vacuum_bot.vacuum if self._subscribers.pop(vacuum.did, None) and self._client: for subscription in _get_subscriptions(vacuum): self._client.unsubscribe(subscription.topic) def disconnect(self) -> None: """Disconnect from MQTT.""" if self._client: self._client.disconnect() self._subscribers.clear() async def _handle_atr(self, topic_split: List[str], payload: bytes) -> None: try: bot = self._subscribers.get(topic_split[3]) if bot: data = json.loads(payload) await bot.handle(topic_split[2], data) except Exception: # pylint: disable=broad-except _LOGGER.error("An exception occurred during handling atr message", exc_info=True) def _handle_p2p(self, topic_split: List[str], payload: bytes) -> None: try: command_name = topic_split[2] if command_name not in SET_COMMAND_NAMES: # command doesn't need special treatment or is not supported yet return is_request = topic_split[9] == "q" request_id = topic_split[10] if is_request: payload_json = json.loads(payload) try: data = payload_json["body"]["data"] except KeyError: _LOGGER.warning( "Could not parse p2p payload: topic=%s; payload=%s", "/".join(topic_split), payload_json, ) return command_class = COMMANDS.get(command_name) if command_class and issubclass(command_class, SetCommand): self._received_set_commands[request_id] = command_class( **data) else: command = self._received_set_commands.get(request_id, None) if not command: _LOGGER.debug( "Response to setCommand came in probably to late. requestId=%s, commandName=%s", request_id, command_name, ) return bot = self._subscribers.get(topic_split[3]) if bot: data = json.loads(payload) if command.handle(bot.events, data) and isinstance( command.args, dict): command.get_command.handle(bot.events, command.args) except Exception: # pylint: disable=broad-except _LOGGER.error("An exception occurred during handling p2p message", exc_info=True)
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)