Example #1
0
    def start(self):
        """
        Starts the connection to the MQTT broker
        :return:
        """
        l.debug("Initializing the MQTT connection...")
        self._mqtt_client.connect(self.domain, self.port, keepalive=30)

        # Starts a new thread that handles mqtt protocol and calls us back via callbacks
        l.debug("(Re)Starting the MQTT loop.")
        self._mqtt_client.loop_stop(True)
        self._mqtt_client.loop_start()
        self.connect_event.wait()

        # Subscribe to the corresponding topics ...
        self.device_topic = build_device_request_topic(self.target_device_uuid)
        self.client_response_topic = build_client_response_topic(
            self.user_id, self._app_id)
        self.user_topic = build_client_user_topic(self.user_id)

        l.info(f"Subscribing to topic: {self.device_topic}")
        self._mqtt_client.subscribe(self.device_topic)
        self.subscribe_event.wait()
        self.subscribe_event.clear()

        l.info(f"Subscribing to topic: {self.client_response_topic}")
        self._mqtt_client.subscribe(self.client_response_topic)
        self.subscribe_event.wait()
        self.subscribe_event.clear()

        l.info("Subscribing to topic: {self.user_topic}")
        self._mqtt_client.subscribe(self.user_topic)
        self.subscribe_event.wait()
        self.subscribe_event.clear()
Example #2
0
    def __init__(self,
                 http_client: MerossHttpClient,
                 auto_reconnect: Optional[bool] = True,
                 domain: Optional[str] = "iot.meross.com",
                 port: Optional[int] = 2001,
                 ca_cert: Optional[str] = None,
                 loop: Optional[AbstractEventLoop] = None,
                 *args,
                 **kwords) -> None:

        # Store local attributes
        self.__initialized = False
        self._http_client = http_client
        self._cloud_creds = self._http_client.cloud_credentials
        self._auto_reconnect = auto_reconnect
        self._domain = domain
        self._port = port
        self._ca_cert = ca_cert
        self._app_id, self._client_id = generate_client_and_app_id()
        self._pending_messages_futures = {}
        self._device_registry = DeviceRegistry()
        self._push_coros = []

        # Setup mqtt client
        mqtt_pass = generate_mqtt_password(user_id=self._cloud_creds.user_id,
                                           key=self._cloud_creds.key)
        self._mqtt_client = mqtt.Client(client_id=self._client_id,
                                        protocol=mqtt.MQTTv311)
        self._mqtt_client.on_connect = self._on_connect
        self._mqtt_client.on_message = self._on_message
        self._mqtt_client.on_disconnect = self._on_disconnect
        self._mqtt_client.on_subscribe = self._on_subscribe
        self._mqtt_client.username_pw_set(username=self._cloud_creds.user_id,
                                          password=mqtt_pass)
        self._mqtt_client.tls_set(ca_certs=self._ca_cert,
                                  certfile=None,
                                  keyfile=None,
                                  cert_reqs=ssl.CERT_REQUIRED,
                                  tls_version=ssl.PROTOCOL_TLS,
                                  ciphers=None)

        # Setup synchronization primitives
        self._loop = asyncio.get_event_loop() if loop is None else loop
        self._mqtt_connected_and_subscribed = asyncio.Event()

        # Prepare MQTT topic names
        self._client_response_topic = build_client_response_topic(
            user_id=self._cloud_creds.user_id, app_id=self._app_id)
        self._user_topic = build_client_user_topic(
            user_id=self._cloud_creds.user_id)
Example #3
0
    def _on_message(self, client, userdata, msg):
        # NOTE! This method is called by the paho-mqtt thread, thus any invocation to the
        # asyncio platform must be scheduled via `self._loop.call_soon_threadsafe()` method.
        _LOGGER.debug(
            f"Received message from topic {msg.topic}: {str(msg.payload)}")

        # In order to correctly dispatch a message, we should look at:
        # - message destination topic
        # - message methods
        # - source device (from value in header)
        # Based on the network capture of Meross Devices, we know that there are 4 kinds of messages:
        # 1. COMMANDS sent from the app to the device (/appliance/<uuid>/subscribe) topic.
        #    Such commands have "from" header populated with "/app/<userid>-<appuuid>/subscribe" as that tells the
        #    device where to send its command ACK. Valid methods are GET/SET
        # 2. COMMAND-ACKS, which are sent back from the device to the app requesting the command execution on the
        #    "/app/<userid>-<appuuid>/subscribe" topic. Valid methods are GETACK/SETACK/ERROR
        # 3. PUSH notifications, which are sent to the "/app/46884/subscribe" topic from the device (which populates
        #    the from header with its topic /appliance/<uuid>/subscribe). In this case, only the PUSH
        #    method is allowed.
        # Case 1 is not of our interest, as we don't want to get notified when the device receives the command.
        # Instead we care about case 2 to acknowledge commands from devices and case 3, triggered when another app
        # has successfully changed the state of some device on the network.

        # Let's parse the message
        message = json.loads(str(msg.payload, "utf8"))
        header = message['header']
        if not verify_message_signature(header, self._cloud_creds.key):
            _LOGGER.error(
                f"Invalid signature received. Message will be discarded. Message: {msg.payload}"
            )
            return

        _LOGGER.debug("Message signature OK")

        # Let's retrieve the destination topic, message method and source party:
        destination_topic = msg.topic
        message_method = header.get('method')
        source_topic = header.get('from')

        # Dispatch the message.
        # Check case 2: COMMAND_ACKS. In this case, we don't check the source topic address, as we trust it's
        # originated by a device on this network that we contacted previously.
        if destination_topic == build_client_response_topic(self._cloud_creds.user_id, self._app_id) and \
                message_method in ['SETACK', 'GETACK', 'ERROR']:
            _LOGGER.debug(
                "This message is an ACK to a command this client has send.")

            # If the message is a PUSHACK/GETACK/ERROR, check if there is any pending command waiting for it and, if so,
            # resolve its future
            message_id = header.get('messageId')
            future = self._pending_messages_futures.get(message_id)
            if future is not None:
                _LOGGER.debug(
                    "Found a pending command waiting for response message")
                if message_method == 'ERROR':
                    err = CommandError(error_payload=message.payload)
                    self._loop.call_soon_threadsafe(_handle_future, future,
                                                    None, err)
                elif message_method in ('SETACK', 'GETACK'):
                    self._loop.call_soon_threadsafe(
                        _handle_future, future, message,
                        None)  # future.set_exception
                else:
                    _LOGGER.error(
                        f"Unhandled message method {message_method}. Please report it to the developer."
                        f"raw_msg: {msg}")
        # Check case 3: PUSH notification.
        # Again, here we don't check the source topic, we trust that's legitimate.
        elif destination_topic == build_client_user_topic(
                self._cloud_creds.user_id) and message_method == 'PUSH':
            namespace = header.get('namespace')
            payload = message.get('payload')
            origin_device_uuid = device_uuid_from_push_notification(
                source_topic)

            parsed_push_notification = parse_push_notification(
                namespace=namespace,
                message_payload=payload,
                originating_device_uuid=origin_device_uuid)
            if parsed_push_notification is None:
                _LOGGER.error(
                    "Push notification parsing failed. That message won't be dispatched."
                )
            else:
                asyncio.run_coroutine_threadsafe(
                    self._handle_and_dispatch_push_notification(
                        parsed_push_notification), self._loop)
        else:
            _LOGGER.warning(
                f"The current implementation of this library does not handle messages received on topic "
                f"({destination_topic}) and when the message method is {message_method}. "
                "If you see this message many times, it means Meross has changed the way its protocol "
                "works. Contact the developer if that happens!")
Example #4
0
    def __init__(self,
                 http_client: MerossHttpClient,
                 auto_reconnect: Optional[bool] = True,
                 domain: Optional[str] = "iot.meross.com",
                 port: Optional[int] = 2001,
                 ca_cert: Optional[str] = None,
                 loop: Optional[AbstractEventLoop] = None,
                 over_limit_threshold_percentage: float = 300,
                 burst_requests_per_second_limit: int = 2,
                 requests_per_second_limit: int = 1,
                 *args,
                 **kwords) -> None:

        # Store local attributes
        self.__initialized = False
        self._http_client = http_client
        self._cloud_creds = self._http_client.cloud_credentials
        self._auto_reconnect = auto_reconnect
        self._domain = domain
        self._port = port
        self._ca_cert = ca_cert
        self._app_id, self._client_id = generate_client_and_app_id()
        self._pending_messages_futures = {}
        self._device_registry = DeviceRegistry()
        self._push_coros = []

        # Setup mqtt client
        mqtt_pass = generate_mqtt_password(user_id=self._cloud_creds.user_id, key=self._cloud_creds.key)
        self._mqtt_client = mqtt.Client(client_id=self._client_id, protocol=mqtt.MQTTv311)
        self._mqtt_client.on_connect = self._on_connect
        self._mqtt_client.on_message = self._on_message
        self._mqtt_client.on_disconnect = self._on_disconnect
        self._mqtt_client.on_subscribe = self._on_subscribe
        self._mqtt_client.username_pw_set(username=self._cloud_creds.user_id, password=mqtt_pass)
        self._mqtt_client.tls_set(ca_certs=self._ca_cert, certfile=None,
                                  keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
                                  tls_version=ssl.PROTOCOL_TLS,
                                  ciphers=None)

        # Setup synchronization primitives
        self._loop = asyncio.get_event_loop() if loop is None else loop
        self._mqtt_connected_and_subscribed = asyncio.Event(loop=self._loop)

        # Prepare MQTT topic names
        self._client_response_topic = build_client_response_topic(user_id=self._cloud_creds.user_id,
                                                                  app_id=self._app_id)
        self._user_topic = build_client_user_topic(user_id=self._cloud_creds.user_id)

        # Setup a rate limiter
        self._over_limit_threshold = over_limit_threshold_percentage
        self._limiter = RateLimitChecker(
            global_burst_rate=burst_requests_per_second_limit,
            device_burst_rate=burst_requests_per_second_limit,
            global_tokens_per_interval=requests_per_second_limit,
            device_tokens_per_interval=requests_per_second_limit
        )
        _LOGGER.info("Applying rate-limit checker config: \n "
                     "- Global Max Burst Rate: %d" 
                     "- Per-Device Max Burst Rate: %d" 
                     "- Global Burst Rate: %d"
                     "- Per-Device Burst Rate: %d",
                     burst_requests_per_second_limit,
                     burst_requests_per_second_limit,
                     requests_per_second_limit,
                     requests_per_second_limit)