8
8
from typing import Any
9
9
10
10
import paho .mqtt .client as mqtt
11
+ from paho .mqtt .enums import MQTTErrorCode
12
+
13
+ # Mypy is not seeing this for some reason. It wants me to use the depreciated ReasonCodes
14
+ from paho .mqtt .reasoncodes import ReasonCode # type: ignore
11
15
12
16
from .api import KEEPALIVE , RoborockClient
13
17
from .containers import DeviceData , UserData
@@ -67,7 +71,8 @@ def __init__(self, user_data: UserData, device_info: DeviceData) -> None:
67
71
self ._mqtt_client = _Mqtt ()
68
72
self ._mqtt_client .on_connect = self ._mqtt_on_connect
69
73
self ._mqtt_client .on_message = self ._mqtt_on_message
70
- self ._mqtt_client .on_disconnect = self ._mqtt_on_disconnect
74
+ # Due to the incorrect ReasonCode, it is confused by typing
75
+ self ._mqtt_client .on_disconnect = self ._mqtt_on_disconnect # type: ignore
71
76
if mqtt_params .tls :
72
77
self ._mqtt_client .tls_set ()
73
78
@@ -76,43 +81,57 @@ def __init__(self, user_data: UserData, device_info: DeviceData) -> None:
76
81
self ._mutex = Lock ()
77
82
self ._decoder : Decoder = create_mqtt_decoder (device_info .device .local_key )
78
83
self ._encoder : Encoder = create_mqtt_encoder (device_info .device .local_key )
84
+ self .received_message_since_last_disconnect = False
85
+ self ._topic = f"rr/m/o/{ self ._mqtt_user } /{ self ._hashed_user } /{ self .device_info .device .duid } "
79
86
80
- def _mqtt_on_connect (self , * args , ** kwargs ):
81
- _ , __ , ___ , rc , ____ = args
87
+ def _mqtt_on_connect (
88
+ self ,
89
+ client : mqtt .Client ,
90
+ userdata : object ,
91
+ flags : dict [str , int ],
92
+ rc : ReasonCode ,
93
+ properties : mqtt .Properties | None = None ,
94
+ ):
82
95
connection_queue = self ._waiting_queue .get (CONNECT_REQUEST_ID )
83
- if rc != mqtt . MQTT_ERR_SUCCESS :
84
- message = f"Failed to connect ({ mqtt . error_string ( rc ) } )"
96
+ if rc . is_failure :
97
+ message = f"Failed to connect ({ rc } )"
85
98
self ._logger .error (message )
86
99
if connection_queue :
87
100
connection_queue .set_exception (VacuumError (message ))
88
101
else :
89
102
self ._logger .debug ("Failed to notify connect future, not in queue" )
90
103
return
91
104
self ._logger .info (f"Connected to mqtt { self ._mqtt_host } :{ self ._mqtt_port } " )
92
- topic = f"rr/m/o/{ self ._mqtt_user } /{ self ._hashed_user } /{ self .device_info .device .duid } "
93
- (result , mid ) = self ._mqtt_client .subscribe (topic )
105
+ (result , mid ) = self ._mqtt_client .subscribe (self ._topic )
94
106
if result != 0 :
95
- message = f"Failed to subscribe ({ mqtt . error_string (rc )} )"
107
+ message = f"Failed to subscribe ({ str (rc )} )"
96
108
self ._logger .error (message )
97
109
if connection_queue :
98
110
connection_queue .set_exception (VacuumError (message ))
99
111
return
100
- self ._logger .info (f"Subscribed to topic { topic } " )
112
+ self ._logger .info (f"Subscribed to topic { self . _topic } " )
101
113
if connection_queue :
102
114
connection_queue .set_result (True )
103
115
104
116
def _mqtt_on_message (self , * args , ** kwargs ):
117
+ self .received_message_since_last_disconnect = True
105
118
client , __ , msg = args
106
119
try :
107
120
messages = self ._decoder (msg .payload )
108
121
super ().on_message_received (messages )
109
122
except Exception as ex :
110
123
self ._logger .exception (ex )
111
124
112
- def _mqtt_on_disconnect (self , * args , ** kwargs ):
113
- _ , __ , rc , ___ = args
125
+ def _mqtt_on_disconnect (
126
+ self ,
127
+ client : mqtt .Client ,
128
+ data : object ,
129
+ flags : dict [str , int ],
130
+ rc : ReasonCode | None ,
131
+ properties : mqtt .Properties | None = None ,
132
+ ):
114
133
try :
115
- exc = RoborockException (mqtt . error_string (rc )) if rc != mqtt . MQTT_ERR_SUCCESS else None
134
+ exc = RoborockException (str (rc )) if rc is not None and rc . is_failure else None
116
135
super ().on_connection_lost (exc )
117
136
connection_queue = self ._waiting_queue .get (DISCONNECT_REQUEST_ID )
118
137
if connection_queue :
@@ -138,7 +157,7 @@ def _sync_disconnect(self) -> Any:
138
157
139
158
if rc != mqtt .MQTT_ERR_SUCCESS :
140
159
disconnected_future .cancel ()
141
- raise RoborockException (f"Failed to disconnect ({ mqtt . error_string (rc )} )" )
160
+ raise RoborockException (f"Failed to disconnect ({ str (rc )} )" )
142
161
143
162
return disconnected_future
144
163
@@ -178,3 +197,63 @@ def _send_msg_raw(self, msg: bytes) -> None:
178
197
)
179
198
if info .rc != mqtt .MQTT_ERR_SUCCESS :
180
199
raise RoborockException (f"Failed to publish ({ mqtt .error_string (info .rc )} )" )
200
+
201
+ async def _unsubscribe (self ) -> MQTTErrorCode :
202
+ """Unsubscribe from the topic."""
203
+ loop = asyncio .get_running_loop ()
204
+ (result , mid ) = await loop .run_in_executor (None , self ._mqtt_client .unsubscribe , self ._topic )
205
+
206
+ if result != 0 :
207
+ message = f"Failed to unsubscribe ({ mqtt .error_string (result )} )"
208
+ self ._logger .error (message )
209
+ else :
210
+ self ._logger .info (f"Unsubscribed from topic { self ._topic } " )
211
+ return result
212
+
213
+ async def _subscribe (self ) -> MQTTErrorCode :
214
+ """Subscribe to the topic."""
215
+ loop = asyncio .get_running_loop ()
216
+ (result , mid ) = await loop .run_in_executor (None , self ._mqtt_client .subscribe , self ._topic )
217
+
218
+ if result != 0 :
219
+ message = f"Failed to subscribe ({ mqtt .error_string (result )} )"
220
+ self ._logger .error (message )
221
+ else :
222
+ self ._logger .info (f"Subscribed to topic { self ._topic } " )
223
+ return result
224
+
225
+ async def _reconnect (self ) -> None :
226
+ """Reconnect to the MQTT broker."""
227
+ await self .async_disconnect ()
228
+ await self .async_connect ()
229
+
230
+ async def _validate_connection (self ) -> None :
231
+ """Override the default validate connection to try to re-subscribe rather than disconnect.
232
+ When something seems to be wrong with our connection, we should follow the following steps:
233
+ 1. Try to unsubscribe and resubscribe from the topic.
234
+ 2. If we don't end up getting a message, we should completely disconnect and reconnect to the MQTT broker.
235
+ 3. We will continue to try to disconnect and reconnect until we get a message.
236
+ 4. If we get a message, the next time connection is lost, We will go back to step 1.
237
+ """
238
+ # If we should no longer keep the current connection alive...
239
+ if not self .should_keepalive ():
240
+ self ._logger .info ("Resetting Roborock connection due to keepalive timeout" )
241
+ if not self .received_message_since_last_disconnect :
242
+ # If we have already tried to unsub and resub, and we are still in this state,
243
+ # we should try to reconnect.
244
+ return await self ._reconnect ()
245
+ try :
246
+ # Mark that we have tried to unsubscribe and resubscribe
247
+ self .received_message_since_last_disconnect = False
248
+ if await self ._unsubscribe () != 0 :
249
+ # If we fail to unsubscribe, reconnect to the broker
250
+ return await self ._reconnect ()
251
+ if await self ._subscribe () != 0 :
252
+ # If we fail to subscribe, reconnected to the broker.
253
+ return await self ._reconnect ()
254
+
255
+ except Exception : # noqa
256
+ # If we get any errors at all, we should just reconnect.
257
+ return await self ._reconnect ()
258
+ # Call connect to make sure everything is still in a good state.
259
+ await self .async_connect ()
0 commit comments