Example #1
0
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)
Example #2
0
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})
Example #3
0
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)