@@ -81,19 +81,26 @@ def __init__(
8181 and thus receiving messages from specific topics. If no msg_cb
8282 is provided, it is not possible to subscribe, and only emitting
8383 is permitted. msg_cb must be a callable that accepts at least
84- those keyword arguments: data, topic, payload. Ideally, to be
85- future-proof, it should be declared with:
84+ those keyword arguments: data, topic, payload, retain . Ideally,
85+ to be future-proof, it should be declared with:
8686 def my_cb(
8787 *_args,
88- *,
8988 data: Any,
9089 topic: str,
9190 payload: bytes,
91+ retain: bool | int,
9292 **_kwargs,
9393 ) -> None
9494
9595 If msg_cb_data is specified, it is passed as the data keyword
9696 argument of msg_cb, otherwise None is passed.
97+
98+ Note: the retain flag will be set to False if the message was
99+ not published in retain; it will be set to True if the message
100+ was published in retain without a Message Expiry Interval; it
101+ will be set to a non-zero integer representing the remaining
102+ Message Expiry Interval if the message was published in retain
103+ with a non-zero Message Expiry Interval.
97104 """
98105
99106 self .msg_cb = msg_cb
@@ -171,27 +178,49 @@ def stop(self):
171178 self .client .disconnect ()
172179 self .client .loop_stop ()
173180
174- def publish (self , * , topic : str , payload : bytes | str ):
181+ def publish (
182+ self ,
183+ * ,
184+ topic : str ,
185+ payload : bytes | str ,
186+ retain : bool | int = False ,
187+ ):
175188 """Publish an MQTT message
176189
177190 :param topic: The MQTT topic to post on.
178191 :param payload: The payload to post.
192+ :param retain: Whether the message should be set to retain; can be
193+ either a bool(), in which case the message will not
194+ be retained (False) or retained indefinitely (True),
195+ or a strictly positive int(), in which case the message
196+ will be sent in retain with a Message Expiry Interval
197+ set to the specified value (min: 1, max: 2^32-1).
179198 """
180199 with self .span_ctxmgr_cb (
181200 name = "IoT3 Core MQTT Message" ,
182201 kind = otel .SpanKind .PRODUCER ,
183202 ) as span :
184- new_traceparent = span .to_traceparent ()
185- span .set_attribute (key = "iot3.core.mqtt.topic" , value = topic )
186- span .set_attribute (key = "iot3.core.mqtt.payload_size" , value = len (payload ))
187203 properties = paho .mqtt .properties .Properties (
188204 paho .mqtt .packettypes .PacketTypes .PUBLISH ,
189205 )
206+ new_traceparent = span .to_traceparent ()
207+ span .set_attribute (key = "iot3.core.mqtt.topic" , value = topic )
208+ span .set_attribute (key = "iot3.core.mqtt.payload_size" , value = len (payload ))
209+ if retain :
210+ span .set_attribute (key = "iot3.core.mqtt.retain" , value = True )
211+ if isinstance (retain , int ):
212+ if retain < 1 :
213+ raise ValueError (
214+ f"retain must be strictly positive (not: { retain } )"
215+ )
216+ properties .MessageExpiryInterval = retain
217+ retain = True
190218 if new_traceparent :
191219 properties .UserProperty = ("traceparent" , new_traceparent )
192220 msg_info = self .client .publish (
193221 topic = topic ,
194222 payload = payload ,
223+ retain = retain ,
195224 properties = properties ,
196225 )
197226 if msg_info .rc :
@@ -214,7 +243,7 @@ def subscribe(self, *, topics: list[str]):
214243 with self .subscriptions_lock :
215244 sub = topics .difference (self .subscriptions )
216245 if sub and self .client .is_connected ():
217- self .client . subscribe ( list ( map ( lambda t : ( t , 0 ), sub )) )
246+ self ._do_subscribe ( topics = sub )
218247 self .subscriptions .update (topics )
219248
220249 def subscribe_replace (self , * , topics : list [str ]):
@@ -242,7 +271,7 @@ def subscribe_replace(self, *, topics: list[str]):
242271 if unsub :
243272 self .client .unsubscribe (list (unsub ))
244273 if sub :
245- self .client . subscribe ( list ( map ( lambda t : ( t , 0 ), sub )) )
274+ self ._do_subscribe ( topics = sub )
246275 self .subscriptions .clear ()
247276 self .subscriptions .update (topics )
248277
@@ -270,6 +299,19 @@ def unsubscribe_all(self):
270299 with self .subscriptions_lock :
271300 self .unsubscribe (topics = self .subscriptions )
272301
302+ def _do_subscribe (
303+ self ,
304+ * ,
305+ topics : list [str ],
306+ ):
307+ opts = paho .mqtt .client .SubscribeOptions (
308+ qos = 2 ,
309+ retainAsPublished = True ,
310+ )
311+ self .client .subscribe (
312+ list (map (lambda t : (t , opts ), self .subscriptions )),
313+ )
314+
273315 # In theory, we would not need this method, as we could very well
274316 # have set self.client.on_message = msg_cb and be done with
275317 # that. Having this intermediate __on_message() allows us to do
@@ -285,9 +327,13 @@ def __on_message(
285327 "kind" : otel .SpanKind .CONSUMER ,
286328 }
287329 try :
288- properties = dict (message .properties .UserProperty )
289- span_kwargs ["span_links" ] = [properties ["traceparent" ]]
290- except Exception :
330+ properties = message .properties
331+ except TypeError :
332+ properties = None
333+ try :
334+ user_properties = dict (properties .UserProperty )
335+ span_kwargs ["span_links" ] = [user_properties ["traceparent" ]]
336+ except (AttributeError , KeyError ):
291337 # There was ultimately no traceparent in that message, ignore
292338 pass
293339 with self .span_ctxmgr_cb (** span_kwargs ) as span :
@@ -297,10 +343,22 @@ def __on_message(
297343 key = "iot3.core.mqtt.payload_size" ,
298344 value = len (message .payload ),
299345 )
346+ if message .retain :
347+ span .set_attribute (key = "iot3.core.mqtt.retain" , value = True )
348+ try :
349+ retain = properties .MessageExpiryInterval
350+ if retain < 1 :
351+ # Glitch, it should not happen: can't be zero
352+ retain = 1
353+ except AttributeError :
354+ retain = True
355+ else :
356+ retain = False
300357 self .msg_cb (
301358 data = self .msg_cb_data ,
302359 topic = message .topic ,
303360 payload = message .payload ,
361+ retain = retain ,
304362 )
305363
306364 def __on_connect (
@@ -313,6 +371,4 @@ def __on_connect(
313371 ):
314372 with self .subscriptions_lock :
315373 if self .subscriptions :
316- self .client .subscribe (
317- list (map (lambda t : (t , 0 ), self .subscriptions )),
318- )
374+ self ._do_subscribe (topics = self .subscriptions )
0 commit comments