29
29
MIN_AUTH_RESPONSE = 20
30
30
MIN_MSG_LENGTH = 56
31
31
MIN_V2_FACTUAL_MSG_LENGTH = 6
32
+ RESPONSE_TIMEOUT = 12 # main loop socket recv timeout, 12 * 10s = 120s
32
33
SOCKET_TIMEOUT = 10 # socket connection default timeout
33
34
QUERY_TIMEOUT = 2 # query response in 1s, 0xAC have more queries, set to 2s
34
35
@@ -150,7 +151,7 @@ def fetch_v2_message(msg: bytes) -> tuple[list, bytes]:
150
151
break
151
152
return result , msg
152
153
153
- def connect (self , init : bool = False , reconnect : bool = False ) -> bool :
154
+ def connect (self , check_protocol : bool = False ) -> bool :
154
155
"""Connect to device."""
155
156
connected = False
156
157
try :
@@ -168,10 +169,8 @@ def connect(self, init: bool = False, reconnect: bool = False) -> bool:
168
169
self .authenticate ()
169
170
# 1. midea_ac_lan add device verify token with connect and auth
170
171
# 2. init connection, check_protocol
171
- # 3. reconnect, skip check_protocol
172
- if reconnect or init :
173
- self .refresh_status (check_protocol = init )
174
- if init :
172
+ if check_protocol :
173
+ self .refresh_status (check_protocol = check_protocol )
175
174
self .get_capabilities ()
176
175
connected = True
177
176
except TimeoutError :
@@ -194,7 +193,9 @@ def connect(self, init: bool = False, reconnect: bool = False) -> bool:
194
193
exc_info = e ,
195
194
)
196
195
self ._socket = None
197
- self .set_available (connected )
196
+ # enable/disable device in init connection
197
+ if check_protocol :
198
+ self .set_available (connected )
198
199
return connected
199
200
200
201
def authenticate (self ) -> None :
@@ -246,19 +247,10 @@ def send_message_v2(self, data: bytes, query: bool = False) -> None:
246
247
# raise exception to main loop
247
248
raise SocketException
248
249
try :
249
- _LOGGER .debug (
250
- "[%s] send_message_v2 with data %s" ,
251
- self ._device_id ,
252
- data .hex (),
253
- )
254
250
# query msg, set timeout to QUERY_TIMEOUT
255
251
if query :
256
252
self ._socket .settimeout (QUERY_TIMEOUT )
257
253
self ._socket .send (data )
258
- _LOGGER .debug (
259
- "[%s] send_message_v2 success" ,
260
- self ._device_id ,
261
- )
262
254
except TimeoutError :
263
255
_LOGGER .debug (
264
256
"[%s] send_message_v2 timed out" ,
@@ -307,15 +299,6 @@ def build_send(self, cmd: MessageRequest, query: bool = False) -> None:
307
299
_LOGGER .debug ("[%s] Sending: %s, query is %s" , self ._device_id , cmd , query )
308
300
msg = PacketBuilder (self ._device_id , data ).finalize ()
309
301
self .send_message (msg , query = query )
310
- # after send set command, force refresh_status
311
- if cmd .message_type == MessageType .set :
312
- _LOGGER .debug (
313
- "[%s] Force refresh after set status to: %s" ,
314
- self ._device_id ,
315
- cmd ,
316
- )
317
- now = time .time ()
318
- self ._previous_refresh = now - self ._refresh_interval
319
302
320
303
def get_capabilities (self ) -> None :
321
304
"""Get device capabilities."""
@@ -340,39 +323,33 @@ def refresh_status(self, check_protocol: bool = False) -> None:
340
323
# set socket QUERY_TIMEOUT for query msg
341
324
# build_send exception should be catch by connect/run
342
325
self .build_send (cmd , query = True )
343
- try :
344
- while True :
345
- if not self ._socket :
346
- _LOGGER .debug (
347
- "[%s] authenticate failure, device socket is none" ,
348
- self ._device_id ,
349
- )
350
- # raise exception to connect/main loop
351
- raise SocketException
352
- msg = self ._socket .recv (512 )
353
- if len (msg ) == 0 :
354
- raise OSError ("Empty message received." )
355
- result = self .parse_message (msg )
356
- # Prevent infinite loop
357
- if result == MessageResult .SUCCESS :
358
- break
359
- elif result == MessageResult .PADDING : # noqa: RET508
360
- continue
361
- else :
362
- raise ResponseException # noqa: TRY301
363
- # recovery SOCKET_TIMEOUT after recv msg
364
- self ._socket .settimeout (SOCKET_TIMEOUT )
365
- # only catch TimoutError for check_protocol
366
- # unexpected exception in recv/settimeout, catch by main loop
367
- except TimeoutError :
368
- _LOGGER .debug (
369
- "[%s] protocol %s, cmd %s, timeout" ,
370
- self ._device_id ,
371
- cmd .__class__ .__name__ ,
372
- cmd ,
373
- )
374
- # init check_protocol, skip timeout exception
375
- if check_protocol :
326
+ # init check_protocol, skip timeout exception
327
+ if check_protocol :
328
+ try :
329
+ while True :
330
+ if not self ._socket :
331
+ _LOGGER .debug (
332
+ "[%s] device socket is none" ,
333
+ self ._device_id ,
334
+ )
335
+ # raise exception to connect/main loop
336
+ raise SocketException
337
+ msg = self ._socket .recv (512 )
338
+ if len (msg ) == 0 :
339
+ raise ConnectionResetError ("Connection closed by peer." )
340
+ result = self .parse_message (msg )
341
+ # Prevent infinite loop
342
+ if result == MessageResult .SUCCESS :
343
+ break
344
+ elif result == MessageResult .PADDING : # noqa: RET508
345
+ continue
346
+ else :
347
+ raise ResponseException # noqa: TRY301
348
+ # recovery SOCKET_TIMEOUT after recv msg
349
+ self ._socket .settimeout (SOCKET_TIMEOUT )
350
+ # only catch TimoutError for check_protocol
351
+ # unexpected exception in recv/settimeout, catch by main loop
352
+ except TimeoutError :
376
353
error_count += 1
377
354
self ._unsupported_protocol .append (cmd .__class__ .__name__ )
378
355
_LOGGER .debug (
@@ -381,16 +358,24 @@ def refresh_status(self, check_protocol: bool = False) -> None:
381
358
cmd .__class__ .__name__ ,
382
359
cmd ,
383
360
)
384
- # refresh_status, raise timeout exception to main loop
385
- else :
386
- raise
387
- except ResponseException :
388
- # parse msg error
389
- error_count += 1
361
+ except ResponseException :
362
+ # parse msg error
363
+ error_count += 1
364
+ _LOGGER .debug (
365
+ "[%s] refresh_status ResponseException %s, cmd %s" ,
366
+ self ._device_id ,
367
+ cmd .__class__ .__name__ ,
368
+ cmd ,
369
+ )
390
370
else :
371
+ _LOGGER .debug (
372
+ "[%s] refresh_status with cmd: %s, unsupported protocol, SKIP" ,
373
+ self ._device_id ,
374
+ cmd ,
375
+ )
391
376
error_count += 1
392
- # init check_protocol and all the query failed
393
- if check_protocol and error_count == len (cmds ):
377
+ # all the query failed
378
+ if error_count == len (cmds ):
394
379
_LOGGER .debug (
395
380
"[%s] all the query cmds failed %s, please report bug" ,
396
381
self ._device_id ,
@@ -556,11 +541,9 @@ def close(self) -> None:
556
541
self ._is_run = False
557
542
self .close_socket ()
558
543
559
- def close_socket (self , init : bool = False ) -> None :
544
+ def close_socket (self ) -> None :
560
545
"""Close socket."""
561
- # init connection, check_protocol
562
- if init :
563
- self ._unsupported_protocol = []
546
+ self ._unsupported_protocol = []
564
547
self ._buffer = b""
565
548
if self ._socket :
566
549
try :
@@ -577,7 +560,7 @@ def set_ip_address(self, ip_address: str) -> None:
577
560
if self ._ip_address != ip_address :
578
561
_LOGGER .debug ("[%s] Update IP address to %s" , self ._device_id , ip_address )
579
562
self ._ip_address = ip_address
580
- self .close_socket (init = True )
563
+ self .close_socket ()
581
564
582
565
def set_refresh_interval (self , refresh_interval : int ) -> None :
583
566
"""Set refresh interval."""
@@ -593,93 +576,114 @@ def _check_heartbeat(self, now: float) -> None:
593
576
self .send_heartbeat ()
594
577
self ._previous_heartbeat = now
595
578
596
- def run (self ) -> None :
579
+ def _connect_loop (self ) -> None :
580
+ """Connect loop until device online."""
581
+ # connect loop until online
582
+ connection_retries = 0
583
+ while self ._socket is None :
584
+ _LOGGER .debug ("[%s] Socket is None, try to connect" , self ._device_id )
585
+ if self .connect (check_protocol = True ) is False :
586
+ self .close_socket ()
587
+ connection_retries += 1
588
+ # Sleep time with exponential backoff, maximum 600 seconds
589
+ sleep_time = min (5 * (2 ** (connection_retries - 1 )), 600 )
590
+ _LOGGER .warning (
591
+ "[%s] Unable to connect, sleep %s seconds and retry" ,
592
+ self ._device_id ,
593
+ sleep_time ,
594
+ )
595
+ # sleep and reconnect loop until device online
596
+ time .sleep (sleep_time )
597
+
598
+ def run (self ) -> None : # noqa: PLR0915
597
599
"""Run loop brief description.
598
600
599
601
1. first/init connection, self._socket is None
600
602
1.1 connect() device loop, pass, enable device
601
603
1.2 auth for v3 device, MUST pass for v3 device
602
- 1.3 init refresh_status, send all query and check supported protocol
604
+ 1.3 init refresh_status, send query and check supported protocol
603
605
1.3.1 set socket timeout to QUERY_TIMEOUT before send query
604
606
1.3.2 get response and add timeout query cmd to not supported
605
607
1.3.1 parse recv response/status for supported protocol
606
608
1.4 get_capabilities()
607
- 2. after socket/device connected, loop for heartbeat/refresh_status
609
+ 2. after socket/device connected, check for heartbeat/refresh_status
608
610
3. job1: check refresh_interval
609
611
3.1 socket/device connection should exist
610
- 3.2 send only supported query to get response and refresh status
611
- 3.3 set socket query timeout and recovery after recv msg
612
+ 3.2 send only supported query and refresh status in main loop recv
613
+ 3.3 set socket timeout before socket recv
612
614
4. job2: check heartbeat interval
613
- 4.1 socket connection should exist
615
+ 4.1 socket/device connection should exist
614
616
4.2 send heartbeat packet to keep alive
615
617
616
618
scenario/bug fix:
617
619
1. while True loop should sleep 0.1 second to prevent cpu usage issue
618
620
2. device running and power off become offline, status update
619
621
3. device disconnected and power on, become online, status update
622
+ 4. set command call build_send, main loop recv socket msg and refresh
620
623
621
624
"""
622
625
# service loop
623
626
while self ._is_run :
624
- # connect loop until online
625
- connection_retries = 0
626
- while self ._socket is None :
627
- _LOGGER .debug ("[%s] Socket is None, try to connect" , self ._device_id )
628
- if self .connect (init = True ) is False :
629
- self .close_socket (init = True )
630
- connection_retries += 1
631
- # Sleep time with exponential backoff, maximum 600 seconds
632
- sleep_time = min (5 * (2 ** (connection_retries - 1 )), 600 )
633
- _LOGGER .warning (
634
- "[%s] Unable to connect, sleep %s seconds and retry" ,
635
- self ._device_id ,
636
- sleep_time ,
637
- )
638
- # sleep and reconnect loop until device online
639
- time .sleep (sleep_time )
640
- connection_retries = 0
627
+ # connect loop until device online
628
+ self ._connect_loop ()
629
+ # socket recv msg timeout counter
630
+ timeout_counter = 0
641
631
start = time .time ()
642
632
self ._previous_refresh = self ._previous_heartbeat = start
643
- # main loop after connected
633
+ # refresh/recv msg loop after connected
644
634
while True :
645
- reconnect = False
646
635
try :
636
+ if not self ._socket :
637
+ _LOGGER .debug ("[%s] Socket is none" , self ._device_id )
638
+ raise SocketException # noqa: TRY301
647
639
now = time .time ()
640
+ # refresh_status only send supported query msg
648
641
self ._check_refresh (now )
649
642
self ._check_heartbeat (now )
643
+ # set SOCKET_TIMEOUT before recv socket msg
644
+ self ._socket .settimeout (SOCKET_TIMEOUT )
645
+ # refresh status after set/query
646
+ msg = self ._socket .recv (512 )
647
+ if len (msg ) == 0 :
648
+ raise ConnectionResetError ("Connection closed by peer" ) # noqa: TRY301
649
+ # parse msg and update latest status
650
+ result = self .parse_message (msg )
651
+ if result == MessageResult .SUCCESS :
652
+ timeout_counter = 0
653
+ if result == MessageResult .ERROR :
654
+ _LOGGER .debug ("[%s] Message 'ERROR' received" , self ._device_id )
655
+ self .close_socket ()
656
+ break
650
657
except TimeoutError :
651
- _LOGGER .debug ("[%s] Socket timed out" , self ._device_id )
652
- reconnect = True
658
+ timeout_counter += 1
659
+ if timeout_counter >= RESPONSE_TIMEOUT :
660
+ _LOGGER .debug ("[%s] Heartbeat timed out" , self ._device_id )
661
+ self .close_socket ()
662
+ break
653
663
except SocketException : # refresh_status
654
664
_LOGGER .debug ("[%s] Socket Exception" , self ._device_id )
655
- reconnect = True
665
+ self .close_socket ()
666
+ break
656
667
except NoSupportedProtocol :
657
668
_LOGGER .debug ("[%s] No Supported protocol" , self ._device_id )
658
669
# ignore and continue loop
659
670
continue
660
671
except ConnectionResetError : # refresh_status -> build_send exception
661
672
_LOGGER .debug ("[%s] Connection reset by peer" , self ._device_id )
662
- reconnect = True
673
+ self .close_socket ()
674
+ break
663
675
except OSError : # refresh_status
664
676
_LOGGER .debug ("[%s] OS error" , self ._device_id )
665
- reconnect = True
677
+ self .close_socket ()
678
+ break
666
679
except Exception as e :
667
680
_LOGGER .exception (
668
681
"[%s] Unexpected error" ,
669
682
self ._device_id ,
670
683
exc_info = e ,
671
684
)
672
- reconnect = True
673
- # reconnect socket and try to skip check_protocol
674
- if reconnect :
675
685
self .close_socket ()
676
- if self .connect (reconnect = True ):
677
- # pass, continue while True loop
678
- continue
679
- # device disconnect, break while True loop, start main loop
680
686
break
681
- # prevent while True loop cpu 100%
682
- time .sleep (0.1 )
683
687
684
688
def set_attribute (self , attr : str , value : bool | int | str ) -> None :
685
689
"""Set attribute."""
0 commit comments