Repository: zpieslak/mobilus-client

Issue:
Fix exception in on_message: 'public_key'"
On shared topic `clients` sometimes client A may receive message that was sent by another client B but still client A is not authorized and therefore `public_key` is not available yet. 

Ignore message on `clients` topic if client is not authenticated yet.

https://github.com/zpieslak/mobilus-client-home-assistant/issues/19

Discussion:

Patch:
diff --git a/README.md b/README.md
index e2d2cd6..2ac4ddd 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,9 @@
 
 ![test workflow](https://github.com/zpieslak/mobilus-client/actions/workflows/test.yml/badge.svg)
 
-This code provides a native Python client for the Mobilus Cosmo GTW. It connects directly to the gateway's MQTT broker and sends message commands to control the associated devices. The connection is established locally, so the client must be run on the same network as the gateway. Currently, the only tested and supported devices are radio shutters (Mobilus COSMO 2WAY).
+This code provides a native Python client for the Mobilus Cosmo GTW. It connects directly to the gateway's MQTT broker and sends message commands to control the associated devices. The connection is established locally, so the client must be run on the same network as the gateway. Note: internet access is not required and can be disabled on the device.
+
+Currently, the only tested and supported devices are radio shutters (Mobilus COSMO 2WAY).
 
 In order to use the client, configuration and setup need to be done on the Mobilus Cosmo GTW side. This includes creating a user and pairing the devices with the gateway.
 
diff --git a/mobilus_client/app.py b/mobilus_client/app.py
index b6ba531..267f951 100644
--- a/mobilus_client/app.py
+++ b/mobilus_client/app.py
@@ -35,7 +35,6 @@ class App:
             if not client.connect_and_authenticate():
                 return self._empty_response()
 
-
             # Execute the provided commands
             for command, params in commands:
                 client.send_request(command, **params)
diff --git a/mobilus_client/client.py b/mobilus_client/client.py
index 8899780..0343c4f 100644
--- a/mobilus_client/client.py
+++ b/mobilus_client/client.py
@@ -25,6 +25,8 @@ class Client:
             self, client_id: str, config: Config, key_registry: KeyRegistry, message_registry: MessageRegistry) -> None:
         self.config = config
         self.client_id = client_id
+        self.shared_topic = "clients"
+        self.command_topic = "module"
         self.key_registry = key_registry
         self.message_registry = message_registry
         self.authenticated_event = threading.Event()
@@ -68,7 +70,7 @@ class Client:
             self.key_registry,
         )
 
-        self.mqtt_client.publish("module", encrypted_message)
+        self.mqtt_client.publish(self.command_topic, encrypted_message)
 
     def terminate(self) -> None:
         self.mqtt_client.disconnect()
@@ -81,7 +83,7 @@ class Client:
             self, _client: mqtt.Client, _userdata: None, _flags: dict[str, Any], _reason_code: int) -> None:
         self.mqtt_client.subscribe([
             (self.client_id, 0),
-            ("clients", 0),
+            (self.shared_topic, 0),
         ])
 
     def on_subscribe_callback(self, _client: mqtt.Client, _userdata: None, _mid: int, _granted_qos: tuple[int]) -> None:
@@ -94,6 +96,11 @@ class Client:
     def on_message_callback(self, _client: mqtt.Client, _userdata: None, mqtt_message: mqtt.MQTTMessage) -> None:
         logger.info("Received message on topic - %s", mqtt_message.topic)
 
+        # Ignore messages from the shared topic sent by other clients until the client is authenticated.
+        if mqtt_message.topic == self.shared_topic and not self.authenticated_event.is_set():
+            logger.info("Client is not authenticated yet. Ignoring message.")
+            return
+
         message = MessageEncryptor.decrypt(mqtt_message.payload, self.key_registry)
         logger.info("Decrypted message - %s", type(message).__name__)
 


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 319d599..2d8e299 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -245,3 +245,11 @@ class TestClient(unittest.TestCase):
         self.client.on_message_callback(self.client.mqtt_client, None, message)
         self.assertEqual(self.message_registry.get_responses(), [devices_list_response])
         self.assertFalse(self.client.completed_event.is_set())
+
+    def test_on_message_shared_topic_when_not_authenticated(self) -> None:
+        login_response = LoginResponseFactory(private_key=b"test_private_key")
+        devices_list_response = DevicesListResponseFactory()
+        encrypted_message = encrypt_message(devices_list_response, login_response.private_key)
+        message = Mock(payload=encrypted_message, topic="clients")
+
+        self.client.on_message_callback(self.client.mqtt_client, None, message)