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()
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)
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!")
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)