@@ -40,6 +40,7 @@ def check_tb_paho_mqtt_installed():
40
40
raise ImportError ("tb-paho-mqtt-client is not installed, please install it manually." ) from e
41
41
42
42
import paho .mqtt .client as paho
43
+ from paho .mqtt .enums import CallbackAPIVersion
43
44
from math import ceil
44
45
45
46
try :
@@ -395,6 +396,7 @@ def reach_limit(self):
395
396
log .info ("Received disconnection due to rate limit for \" %s\" rate limit, waiting for tokens in bucket for %s seconds" ,
396
397
self .name ,
397
398
target_duration )
399
+ return self .__reached_limit_index , self .__reached_limit_index_time
398
400
399
401
@property
400
402
def __dict__ (self ):
@@ -475,7 +477,7 @@ def __init__(self, host, port=1883, username=None, password=None, quality_of_ser
475
477
messages_rate_limit = messages_rate_limit if kwargs .get ('rate_limit' ) == "DEFAULT_RATE_LIMIT" else kwargs .get ('rate_limit' , messages_rate_limit ) # noqa
476
478
telemetry_rate_limit = telemetry_rate_limit if kwargs .get ('rate_limit' ) == "DEFAULT_RATE_LIMIT" else kwargs .get ('rate_limit' , telemetry_rate_limit ) # noqa
477
479
telemetry_dp_rate_limit = telemetry_dp_rate_limit if kwargs .get ('dp_rate_limit' ) == "DEFAULT_RATE_LIMIT" else kwargs .get ('dp_rate_limit' , telemetry_dp_rate_limit ) # noqa
478
- self ._client = paho .Client (protocol = 5 , client_id = client_id )
480
+ self ._client = paho .Client (protocol = 5 , client_id = client_id , callback_api_version = CallbackAPIVersion . VERSION2 )
479
481
self .quality_of_service = quality_of_service if quality_of_service is not None else 1
480
482
self .__host = host
481
483
self .__port = port
@@ -512,7 +514,7 @@ def __init__(self, host, port=1883, username=None, password=None, quality_of_ser
512
514
self .__timeout_thread .daemon = True
513
515
self .__timeout_thread .start ()
514
516
self ._client .on_connect = self ._on_connect
515
- # self._client.on_publish = self._on_publish
517
+ self ._client .on_publish = self ._on_publish
516
518
self ._client .on_message = self ._on_message
517
519
self ._client .on_disconnect = self ._on_disconnect
518
520
self .current_firmware_info = {
@@ -528,6 +530,8 @@ def __init__(self, host, port=1883, username=None, password=None, quality_of_ser
528
530
self .__request_service_configuration_required = False
529
531
self .__service_loop = Thread (target = self .__service_loop , name = "Service loop" , daemon = True )
530
532
self .__service_loop .start ()
533
+ self .__messages_limit_reached_set_time = (0 ,0 )
534
+ self .__datapoints_limit_reached_set_time = (0 ,0 )
531
535
532
536
def __service_loop (self ):
533
537
while not self .stopped :
@@ -550,51 +554,74 @@ def __service_loop(self):
550
554
self .firmware_received = False
551
555
sleep (0.05 )
552
556
553
- def _on_publish (self , client , userdata , mid ):
554
- # log.debug("Message %s was published, by client with id: %r", mid ,id(client))
555
- pass
556
-
557
- def _on_disconnect (self , client : paho .Client , userdata , result_code , properties = None ):
557
+ def _on_publish (self , client , userdata , mid , rc = None , properties = None ):
558
+ if isinstance (rc , ReasonCodes ) and rc .value != 0 :
559
+ log .debug ("Publish failed with result code %s (%s) " , str (rc .value ), rc .getName ())
560
+ if rc .value in [151 , 131 ]:
561
+ if self .__messages_limit_reached_set_time [1 ] - monotonic () > self .__messages_limit_reached_set_time [0 ]:
562
+ self .__messages_limit_reached_set_time = self ._messages_rate_limit .reach_limit ()
563
+ if self .__datapoints_limit_reached_set_time [1 ] - monotonic () > self .__datapoints_limit_reached_set_time [0 ]:
564
+ self ._telemetry_dp_rate_limit .reach_limit ()
565
+ if rc .value == 0 :
566
+ if self .__messages_limit_reached_set_time [0 ] > 0 and self .__messages_limit_reached_set_time [1 ] > 0 :
567
+ self .__messages_limit_reached_set_time = (0 , 0 )
568
+ if self .__datapoints_limit_reached_set_time [0 ] > 0 and self .__datapoints_limit_reached_set_time [1 ] > 0 :
569
+ self .__datapoints_limit_reached_set_time = (0 , 0 )
570
+
571
+ def _on_disconnect (self , client : paho .Client , userdata , disconnect_flags , reason = None , properties = None ):
558
572
self .__is_connected = False
559
- client ._out_packet .clear ()
560
- client ._out_messages .clear ()
573
+ with self ._client ._out_message_mutex :
574
+ client ._out_packet .clear ()
575
+ client ._out_messages .clear ()
561
576
client ._in_messages .clear ()
562
577
self .__attr_request_number = 0
563
578
self .__device_max_sub_id = 0
564
579
self .__device_client_rpc_number = 0
565
580
self .__device_sub_dict = {}
566
581
self .__device_client_rpc_dict = {}
567
582
self .__attrs_request_timeout = {}
568
- log .warning ("MQTT client was disconnected with reason code %s (%s) " ,
569
- str (result_code ), TBPublishInfo .ERRORS_DESCRIPTION .get (result_code , "Description not found." ))
583
+ result_code = reason .value
584
+ if disconnect_flags .is_disconnect_packet_from_server :
585
+ log .warning ("MQTT client was disconnected by server with reason code %s (%s) " ,
586
+ str (result_code ), reason .getName ())
587
+ else :
588
+ log .info ("MQTT client was disconnected by client with reason code %s (%s) " ,
589
+ str (result_code ), reason .getName ())
570
590
log .debug ("Client: %s, user data: %s, result code: %s. Description: %s" ,
571
591
str (client ), str (userdata ),
572
- str (result_code ), TBPublishInfo . ERRORS_DESCRIPTION . get ( result_code , "Description not found." ))
592
+ str (result_code ), reason . getName ( ))
573
593
574
- def _on_connect (self , client , userdata , flags , result_code , * extra_params ):
594
+ def _on_connect (self , client , userdata , connect_flags , result_code , properties , * extra_params ):
575
595
if result_code == 0 :
576
596
self .__is_connected = True
577
597
log .info ("MQTT client %r - Connected!" , client )
598
+ if properties :
599
+ log .debug ("MQTT client %r - CONACK Properties: %r" , client , properties )
600
+ config = {
601
+ "maxPayloadSize" : int (properties .MaximumPacketSize * DEFAULT_RATE_LIMIT_PERCENTAGE / 100 ),
602
+ "maxInflightMessages" : properties .ReceiveMaximum ,
603
+ }
604
+ self .on_service_configuration (None , config )
578
605
self ._subscribe_to_topic (ATTRIBUTES_TOPIC , qos = self .quality_of_service )
579
606
self ._subscribe_to_topic (ATTRIBUTES_TOPIC + "/response/+" , qos = self .quality_of_service )
580
607
self ._subscribe_to_topic (RPC_REQUEST_TOPIC + '+' , qos = self .quality_of_service )
581
608
self ._subscribe_to_topic (RPC_RESPONSE_TOPIC + '+' , qos = self .quality_of_service )
582
609
self .__request_service_configuration_required = True
583
610
else :
584
- if isinstance (result_code , int ):
585
- if result_code in RESULT_CODES :
586
- log .error ("connection FAIL with error %s %s" , result_code , RESULT_CODES [result_code ])
587
- else :
588
- log .error ("connection FAIL with unknown error" )
589
- elif isinstance (result_code , ReasonCodes ):
590
- log .error ("connection FAIL with error %s %s" , result_code , result_code .getName ())
611
+ log .error ("Connection failed with result code %s (%s) " ,
612
+ str (result_code .value ), result_code .getName ())
591
613
592
614
if callable (self .__connect_callback ):
593
615
sleep (.2 )
594
616
if "tb_client" in signature (self .__connect_callback ).parameters :
595
- self .__connect_callback (client , userdata , flags , result_code , * extra_params , tb_client = self )
617
+ self .__connect_callback (client , userdata , connect_flags , result_code , properties , * extra_params , tb_client = self )
596
618
else :
597
- self .__connect_callback (client , userdata , flags , result_code , * extra_params )
619
+ self .__connect_callback (client , userdata , connect_flags , result_code , * extra_params )
620
+
621
+ if result_code .value in [159 , 151 ]:
622
+ log .debug ("Connection rate limit reached, waiting before reconnecting..." )
623
+ sleep (1 ) # Wait for 1 second before reconnecting, if connection rate limit is reached
624
+ log .debug ("Reconnecting allowed..." )
598
625
599
626
def get_firmware_update (self ):
600
627
self ._client .subscribe ("v2/fw/response/+" )
@@ -954,40 +981,67 @@ def _wait_for_rate_limit_released(self, timeout, message_rate_limit, dp_rate_lim
954
981
log .debug ("Rate limit released, sending data to ThingsBoard..." )
955
982
956
983
def _wait_until_current_queued_messages_processed (self ):
957
- previous_notification_time = 0
958
- current_out_messages = len (self ._client ._out_messages ) * 2
959
- max_inflight_messages = self ._client ._max_inflight_messages if self ._client ._max_inflight_messages > 0 else 5
960
984
logger = None
961
- waiting_started = int (monotonic ())
962
- connection_was_lost = False
963
- timeout_for_break = 300
964
-
965
- if current_out_messages > 0 :
966
- while current_out_messages >= max_inflight_messages and not self .stopped :
967
- current_out_messages = len (self ._client ._out_messages )
968
- elapsed = monotonic () - waiting_started
969
- remaining = timeout_for_break - elapsed
970
-
971
- if int (monotonic ()) - previous_notification_time > 5 and current_out_messages > max_inflight_messages :
972
- if logger is None :
973
- logger = logging .getLogger ('tb_connection' )
974
- logger .debug (
975
- "Waiting for messages to be processed: current queue size: %r, max inflight: %r. "
976
- "Elapsed time: %.2f seconds, remaining timeout: %.2f seconds" ,
977
- current_out_messages , max_inflight_messages , elapsed , remaining
978
- )
979
- previous_notification_time = int (monotonic ())
980
-
981
- connection_was_lost = True
982
-
983
- if current_out_messages >= max_inflight_messages :
984
- sleep (.01 )
985
-
986
- if (elapsed > timeout_for_break and not connection_was_lost ) or self .stopped :
987
- logger .debug ("Breaking wait loop after %.2f seconds due to timeout or stop signal." , elapsed )
988
- break
989
-
990
- sleep (.001 )
985
+
986
+ max_wait_time = 300
987
+ log_interval = 5
988
+ stuck_threshold = 15
989
+ polling_interval = 0.05
990
+ max_inflight = self ._client ._max_inflight_messages
991
+
992
+ if len (self ._client ._out_messages ) < max_inflight or max_inflight == 0 :
993
+ return
994
+
995
+ waiting_start = monotonic ()
996
+ last_log_time = waiting_start
997
+ last_queue_size = len (self ._client ._out_messages )
998
+ last_queue_change_time = waiting_start
999
+
1000
+ while not self .stopped :
1001
+ now = monotonic ()
1002
+ elapsed = now - waiting_start
1003
+ current_queue_size = len (self ._client ._out_messages )
1004
+
1005
+ if current_queue_size < max_inflight :
1006
+ return
1007
+
1008
+ if current_queue_size != last_queue_size :
1009
+ last_queue_size = current_queue_size
1010
+ last_queue_change_time = now
1011
+
1012
+ if (now - last_queue_change_time > stuck_threshold
1013
+ and not self ._client .is_connected ()):
1014
+ if logger is None :
1015
+ logger = logging .getLogger ('tb_connection' )
1016
+ logger .warning (
1017
+ "MQTT out_messages queue is stuck (%d messages) and client is disconnected. "
1018
+ "Clearing queue after %.2f seconds." ,
1019
+ current_queue_size , now - last_queue_change_time
1020
+ )
1021
+ with self ._client ._out_message_mutex :
1022
+ self ._client ._out_packet .clear ()
1023
+ return
1024
+
1025
+ if now - last_log_time >= log_interval :
1026
+ if logger is None :
1027
+ logger = logging .getLogger ('tb_connection' )
1028
+ logger .debug (
1029
+ "Waiting for MQTT queue to drain: %d messages (max inflight %d). "
1030
+ "Elapsed: %.2f s" ,
1031
+ current_queue_size , max_inflight , elapsed
1032
+ )
1033
+ last_log_time = now
1034
+
1035
+ if elapsed > max_wait_time :
1036
+ if logger is None :
1037
+ logger = logging .getLogger ('tb_connection' )
1038
+ logger .warning (
1039
+ "MQTT wait timeout reached (%.2f s). Queue still has %d messages." ,
1040
+ elapsed , current_queue_size
1041
+ )
1042
+ return
1043
+
1044
+ sleep (polling_interval )
991
1045
992
1046
def _send_request (self , _type , kwargs , timeout = DEFAULT_TIMEOUT , device = None ,
993
1047
msg_rate_limit = None , dp_rate_limit = None ):
0 commit comments