Asset Connector Documentation
Loading...
Searching...
No Matches
mqtt_client.py
Go to the documentation of this file.
1"""MQTTConnector module for connecting to MQTT topics and caching messages.
2
3Provides the MQTTConnector class for subscribing to topics, receiving messages,
4and storing the latest payload for each topic.
5"""
6
7import logging
8import ssl
9from urllib.parse import urlparse
10
11from aas_standard_parser.aid_parser import BasicAuthenticationDetails, IAuthenticationDetails
12from paho.mqtt.client import Client
13
14logger = logging.getLogger(__name__)
15
16
17class MqttClient:
18 """Connector for managing connections to MQTT topics."""
19
20 base_url: str
21 host: str
22 port: int
23 path: str
24
25 def __init__(self, base_url: str, topics: list[str], auth: IAuthenticationDetails):
26 """Initialize the MQTTConnector with broker host and port."""
27 self.cache = {}
28 for t in topics:
29 self.cache[t] = None
30
31 self.base_urlbase_url = base_url
32 self.hosthost, self.portport, self.pathpath, use_tls = self._parse_url()
33
34 self.client = Client(transport=self._detect_transport())
35
36 if isinstance(auth, BasicAuthenticationDetails):
37 self.client.username_pw_set(auth.user, auth.password)
38
39 if use_tls:
40 self.client.tls_set(cert_reqs=ssl.CERT_REQUIRED, ca_certs=None)
41 self.client.tls_insecure_set(True)
42
43 if self._detect_transport() == "websockets" and self.pathpath != "/":
44 self.client.ws_set_options(path=self.pathpath)
45
46 self._is_connected = False
47
48 self.client.on_connect = self._on_connect
49 self.client.on_message = self._on_message
50
51 def _parse_url(self):
52 """Extract host, port, and path from broker_url."""
53 parsed = urlparse(self.base_urlbase_url)
55 # scheme like mqtt, mqtts, ws, wss
56 scheme = parsed.scheme or "mqtt"
57
58 # host
59 host = parsed.hostname
60
61 # port (fall back to defaults if missing)
62 defaults = {"mqtt": 1883, "mqtts": 8883, "ws": 80, "wss": 443}
63 port = parsed.port or defaults.get(scheme, 1883)
64
65 # path (default to "/" if not given)
66 path = parsed.path or "/" if "ws" in parsed.scheme else None
67
68 # TLS required?
69 use_tls = scheme in ("mqtts", "wss")
70
71 return host, port, path, use_tls
72
73 def _detect_transport(self):
74 """Determine if plain MQTT or MQTT over WebSocket is needed."""
75 if self.base_urlbase_url.startswith("ws://") or self.base_urlbase_url.startswith("wss://"):
76 return "websockets"
77 return "tcp"
79 def connect(self):
80 """Connect to the MQTT broker and subscribe to all topics.
81
82 This method should be called after initializing the MQTTConnector.
83 """
84 try:
85 logger.debug(f"Connecting to MQTT broker at '{self.base_url}:{self.port}'")
86 self.client.connect(self.hosthost, self.portport, 60)
87 except Exception as e:
88 raise ConnectionError(f"Error connecting to MQTT broker at '{self.base_url}': {e}")
89
90 def _on_connect(self, client, userdata, flags, rc): # noqa: ARG002
91 """Handle response from the server.
92
93 :param client: The client instance for this callback.
94 :param userdata: The private user data as set in Client() or userdata_set().
95 :param flags: Response flags sent by the broker.
96 :param rc: The connection result.
97 """
98 logger.info(f"Connected with result code {rc}")
99
100 # Subscribe to all topics in self.topics
101 if rc == 0:
102 self._is_connected = True
103 for topic in self.cache.keys():
104 self.client.subscribe(topic)
105 logger.info(f"Subscribed to topic: {topic}")
106
107 def _on_message(self, client, userdata, message): # noqa: ARG002
108 """Handle incoming messages from subscribed topics.
109
110 :param client: The client instance for this callback.
111 :param userdata: The private user data as set in Client() or userdata_set().
112 :param message: The message instance containing topic and payload.
113 """
114 topic = message.topic
115 payload = message.payload.decode("utf-8")
116 logger.info(f"Received message '{payload}' on topic '{topic}'")
117
118 # Cache the message
119 self.cache[topic] = payload
120
121 def start_async(self):
122 """Start the MQTT client loop in a separate thread for non-blocking operation.
123
124 :return: True if the loop started successfully, False otherwise.
125 """
126 return self.client.loop_start()
127
128 def stop(self):
129 """Stop the MQTT client loop and disconnect from the broker."""
130 self.client.loop_stop()
131 self.client.disconnect()
132
133 def disconnect(self):
134 """Disconnect from the MQTT broker gracefully."""
135 self.client.disconnect()
136
137 def dispose(self):
138 """Clean up resources and disconnect from the MQTT broker.
139
140 This method should be called when the connector is no longer needed
141 to ensure proper cleanup of resources.
142 """
143 try:
144 # Unsubscribe from all topics
145 for topic in self.topics:
146 self.client.unsubscribe(topic)
147
148 # Stop the client loop and disconnect
149 self.client.loop_stop()
151
152 # Clear internal state
153 self.topics.clear()
154 self.cache.clear()
155
156 except (OSError, ConnectionError, RuntimeError) as e:
157 logger.error(f"Error during disposal: {e}")
158
159 def __enter__(self):
160 """Context manager entry point.
161
162 :return: The MQTTConnector instance.
163 """
164 return self
165
166 def __exit__(self, exc_type, exc_val, exc_tb):
167 """Context manager exit point - ensures proper cleanup.
168
169 :param exc_type: Exception type if an exception was raised.
170 :param exc_val: Exception value if an exception was raised.
171 :param exc_tb: Exception traceback if an exception was raised.
172 """
173 self.dispose()
174
175 def add_topics(self, topics):
176 """Add topics to the MQTTConnector."""
177 self.topics = topics
178
179 def get_cached_value(self, topic):
180 """Retrieve the cached value for a specific topic.
181
182 :param topic: The topic to retrieve the cached value for.
183 :return: The cached value if it exists, otherwise None.
184 """
185 return self.cache.get(topic, None)
186
187 @property
188 def is_connected(self):
189 return self._is_connected
190
191 def publish(self, topic: str, payload: str):
192 """Publish a message to a specific topic.
193
194 :param topic: The topic to publish the message to.
195 :param payload: The message payload to be published.
196 """
197 self.client.publish(topic, payload)
Connector for managing connections to MQTT topics.
get_cached_value(self, topic)
Retrieve the cached value for a specific topic.
start_async(self)
Start the MQTT client loop in a separate thread for non-blocking operation.
stop(self)
Stop the MQTT client loop and disconnect from the broker.
_on_message(self, client, userdata, message)
Handle incoming messages from subscribed topics.
dispose(self)
Clean up resources and disconnect from the MQTT broker.
publish(self, str topic, str payload)
Publish a message to a specific topic.
__exit__(self, exc_type, exc_val, exc_tb)
Context manager exit point - ensures proper cleanup.
disconnect(self)
Disconnect from the MQTT broker gracefully.
connect(self)
Connect to the MQTT broker and subscribe to all topics.
__init__(self, str base_url, list[str] topics, IAuthenticationDetails auth)
Initialize the MQTTConnector with broker host and port.
_on_connect(self, client, userdata, flags, rc)
Handle response from the server.
_detect_transport(self)
Determine if plain MQTT or MQTT over WebSocket is needed.
__enter__(self)
Context manager entry point.
_parse_url(self)
Extract host, port, and path from broker_url.
add_topics(self, topics)
Add topics to the MQTTConnector.