77import json
88from collections import deque
99from datetime import datetime , timedelta
10- from typing import Optional , Dict , Any , Tuple
10+ from typing import Optional , Dict , Any , Tuple , List
1111import paho .mqtt .client as mqtt
1212from PIL import Image
1313
@@ -194,6 +194,9 @@ def _prune_stale_clients(self):
194194 def register_heartbeat (self , client_ip : str , client_id : int ):
195195 """Update the last seen timestamp for a connected client"""
196196 with self .connection_lock :
197+ # Prune stale clients during heartbeat (periodic cleanup)
198+ self ._prune_stale_clients ()
199+
197200 key = (client_ip , client_id )
198201 if key in self .connected_clients :
199202 # Only update last_seen, preserve connected_clients (start time)
@@ -348,12 +351,20 @@ def _get_arg(self, key: str, default: Any = None) -> str:
348351 def is_connected (self ) -> bool :
349352 """Check if any clients are connected"""
350353 with self .connection_lock :
351- self ._prune_stale_clients ()
352354 return len (self .connected_clients ) > 0
355+
356+ def is_client_connected (self , client_ip : str , client_id : int ) -> bool :
357+ """Check if a specific client is connected"""
358+ with self .connection_lock :
359+ key = (client_ip , client_id )
360+ return key in self .connected_clients
353361
354362 def connect (self , client_ip : str , client_id : int ):
355363 """Connect a client to the device"""
356364 with self .connection_lock :
365+ # Prune stale clients BEFORE adding new connection
366+ self ._prune_stale_clients ()
367+
357368 key = (client_ip , client_id )
358369 current_time = get_current_time (self .alpaca_config .timezone )
359370 self .connected_clients [key ] = current_time
@@ -373,13 +384,13 @@ def connect(self, client_ip: str, client_id: int):
373384 def disconnect (self , client_ip : str = None , client_id : int = None ):
374385 """Disconnect a client from the device"""
375386 with self .connection_lock :
376- self ._prune_stale_clients ()
377387 if client_ip is None or client_id is None :
378- # Disconnect all
388+ # Disconnect all - IMMEDIATE state change
389+ disc_time = get_current_time (self .alpaca_config .timezone )
379390 for key in list (self .connected_clients .keys ()):
380391 conn_time = self .connected_clients [key ]
381- disc_time = get_current_time (self .alpaca_config .timezone )
382392 self .disconnected_clients [key ] = (conn_time , disc_time )
393+
383394 self .connected_clients .clear ()
384395 self .client_last_seen .clear ()
385396 self .disconnected_at = disc_time
@@ -420,6 +431,30 @@ def is_safe(self) -> bool:
420431 with self .detection_lock :
421432 return self ._stable_safe_state
422433
434+ def get_safety_history (self ) -> List [Dict [str , Any ]]:
435+ """Get a thread-safe copy of the safety history"""
436+ with self .detection_lock :
437+ return list (self ._safety_history )
438+
439+ def get_connected_clients_info (self ) -> List [Dict [str , Any ]]:
440+ """Get detailed information about connected clients"""
441+ clients = []
442+ now = get_current_time (self .alpaca_config .timezone )
443+ with self .connection_lock :
444+ # Prune stale clients before returning list
445+ self ._prune_stale_clients ()
446+
447+ for (ip , client_id ), conn_time in self .connected_clients .items ():
448+ last_seen = self .client_last_seen .get ((ip , client_id ))
449+ clients .append ({
450+ "ip" : ip ,
451+ "client_id" : client_id ,
452+ "connected_at" : conn_time ,
453+ "last_seen" : last_seen ,
454+ "duration_seconds" : (now - conn_time ).total_seconds ()
455+ })
456+ return clients
457+
423458 def get_device_state (self ) -> list :
424459 """Get current operational state"""
425460 is_safe_val = self .is_safe ()
0 commit comments