77import functools
88import os
99import requests
10- import pandas as pd
1110
1211
1312logger = logging .getLogger ("client" )
@@ -28,7 +27,7 @@ def api_worker_thread():
2827 logger .info ("[Device ID API] Starting worker thread to periodically call the API for each device." )
2928 while True :
3029 time .sleep (15 )
31- device_list = get_devices ()
30+ device_list = get_all_devices ()
3231 logger .info ("[Device ID API] 15 seconds passed, will start calling API for each device if needed." )
3332
3433 # Getting inputs and calling API
@@ -50,8 +49,10 @@ def api_worker_thread():
5049 finally :
5150 # If API is down, just try using OUI vendor if no custom name is set in config.json
5251 if common .config_get (custom_name_key , default = '' ) == '' :
53- common .config_set (custom_name_key ,
54- meta_data .get ('oui_vendor' , 'Unknown Device, likely a Mobile Phone' ))
52+ vendor = (meta_data .get ('oui_vendor' ) or '' ).strip ()
53+ if not vendor :
54+ vendor = 'Unknown Device, likely a Mobile Phone'
55+ common .config_set (custom_name_key , vendor )
5556
5657
5758@functools .cache
@@ -122,7 +123,7 @@ def call_predict_api(meta_data_string: str, remote_hostnames: str,
122123 return result
123124
124125
125- def get_devices () -> list [dict ]:
126+ def get_all_devices () -> list [dict ]:
126127 """
127128 Get the list of devices from the database.
128129
@@ -143,23 +144,48 @@ def get_devices() -> list[dict]:
143144 return device_list
144145
145146
146- @st .fragment (run_every = 1 )
147+ def get_device_data (mac_address : str ) -> dict :
148+ """
149+ Get the device details for a specific device by MAC address.
150+
151+ Returns:
152+ mac_address (str): The MAC address of the device.
153+ """
154+ sql = """
155+ SELECT * FROM devices
156+ WHERE mac_address = ?
157+ """
158+ db_conn , rwlock = libinspector .global_state .db_conn_and_lock
159+ with rwlock :
160+ row = db_conn .execute (sql , (mac_address ,)).fetchone ()
161+ if row :
162+ return dict (row )
163+ else :
164+ return dict ()
165+
166+
147167def show_list (toast_obj : st .toast ):
148168 """
149169 The main page that creates a "card" for each device that was found via IoT Inspector
150170 """
151171 human_readable_time = common .get_human_readable_time ()
152- st .markdown (f'Updated : { human_readable_time } ' )
172+ st .markdown (f'Last Page Refresh : { human_readable_time } ' )
153173
154- device_list = get_devices ()
174+ device_list = get_all_devices ()
155175 if not device_list :
156176 st .warning ('We are still scanning the network for devices. Please wait a moment. This page will refresh automatically.' )
177+ time .sleep (5 )
178+ st .rerun ()
157179
158- device_upload , device_download = common .bar_graph_data_frame (int (time .time ()))
159180 # Create a card entry for the discovered device
160181 for device_dict in device_list :
161182 st .markdown ('---' )
162- show_device_card (device_dict , device_upload , device_download )
183+ c1 , c2 = st .columns ([6 , 4 ], gap = 'small' )
184+ with c1 :
185+ show_device_bar_graph (device_dict )
186+ # Set whether a device is to be inspected, favorite, or blocked
187+ with c2 :
188+ toggle_device_fragment (device_dict ["mac_address" ])
163189
164190 # Create a pop-up showing if any new devices were found
165191 prev_device_count = st .session_state .get ('prev_device_count' , 0 )
@@ -169,21 +195,20 @@ def show_list(toast_obj: st.toast):
169195 time .sleep (1.5 ) # Give the user a moment to read the toast
170196
171197
172- def show_device_card (device_dict : dict , device_upload : pd . DataFrame , device_download : pd . DataFrame ):
198+ def update_device_inspected_status (device_dict : dict ):
173199 """
174- Process the data for a discovered device into a list of cards.
175-
200+ Update the 'is_inspected' status of a device in the database based on user interaction.
176201 Args:
177202 device_dict (dict): information about the device, from the 'devices' table
178- device_upload (pd.DataFrame): DataFrame containing upload traffic data
179- device_download (pd.DataFrame): DataFrame containing download traffic data
180203 """
181204 # Check if the user has previously inspected this device
182205 device_inspected_config_key = f'device_is_inspected_{ device_dict ["mac_address" ]} '
183206 is_inspected = common .config_get (device_inspected_config_key , False )
184207
185208 # Update the inspected status in the database if it is different
186209 if is_inspected != (device_dict ['is_inspected' ] == 1 ):
210+ # Note, on start time, the is_inspected sets to 0 for all devices, then changes to 1 once the user clicks the Inspect button
211+ logger .info (f"[Device List Page] Updating 'is_inspected' status for device to { is_inspected } " )
187212 db_conn , rwlock = libinspector .global_state .db_conn_and_lock
188213 with rwlock :
189214 sql = """
@@ -193,89 +218,100 @@ def show_device_card(device_dict: dict, device_upload: pd.DataFrame, device_down
193218 """
194219 db_conn .execute (sql , (is_inspected , device_dict ['mac_address' ]))
195220
196- # Extra information on the device's metadata, e.g., OUI
197- metadata_dict = json .loads (device_dict ['metadata_json' ])
198221
199- # Get the device's custom name as set by the user
222+ @st .fragment (run_every = 1 )
223+ def show_device_bar_graph (device_dict : dict ):
224+ """
225+ Show the upload/download bar graph for a device.
226+
227+ Args:
228+ device_dict (dict): information about the device, from the 'devices' table
229+ """
230+ # Get the Bar Graph data for upload/download
231+ device_upload , device_download = common .bar_graph_data_frame (int (time .time ()))
232+
233+ # Extra information on the device's metadata, e.g., OUI
234+ metadata_dict = json .loads (device_dict ['metadata_json' ]) # Get the device's custom name as set by the user
200235 device_custom_name = common .get_device_custom_name (device_dict ['mac_address' ])
236+ device_detail_url = f"/device_details?device_mac_address={ device_dict ['mac_address' ]} "
237+ title_text = f'**[{ device_custom_name } ]({ device_detail_url } )**'
238+ st .markdown (title_text )
239+ caption = f'{ device_dict ["ip_address" ]} | { device_dict ["mac_address" ]} '
240+ if "oui_vendor" in metadata_dict :
241+ caption += f' | { metadata_dict ["oui_vendor" ]} '
242+
243+ api_output = common .config_get (f'device_details@{ device_dict ["mac_address" ]} ' , default = {})
244+ if "Vendor" in api_output or "Explanation" in api_output :
245+ vendor = api_output .get ("Vendor" , "" )
246+ explanation = api_output .get ("Explanation" , "" )
247+ if vendor or explanation :
248+ caption += f"| Vendor: { vendor } | Explanation: { explanation } "
249+ st .caption (caption , help = 'IP address, MAC address, manufacturer OUI, and Device Identification API output' )
250+
251+ # --- Add bar charts for upload/download ---
252+ chart_col_upload , chart_col_download = st .columns (2 )
253+ with chart_col_upload :
254+ device_upload_graph = device_upload [device_upload ['mac_address' ] == device_dict ["mac_address" ]]
255+ common .plot_traffic_volume (device_upload_graph ,
256+ "Upload Traffic (sent by device) in the last 60 seconds" )
257+ with chart_col_download :
258+ device_download_graph = device_download [device_download ['mac_address' ] == device_dict ["mac_address" ]]
259+ common .plot_traffic_volume (device_download_graph ,
260+ "Download Traffic (sent by device) in the last 60 seconds" )
261+
262+
263+ @st .fragment (run_every = 3 )
264+ def toggle_device_fragment (mac_address : str ):
265+ # Maps short options to long options for display
266+ option_dict = {
267+ 'inspected' : ':material/troubleshoot: Inspected' ,
268+ 'favorite' : ':material/favorite: Favorite' ,
269+ 'blocked' : ':material/block: Blocked'
270+ }
201271
202- c1 , c2 = st .columns ([6 , 4 ], gap = 'small' )
203-
204- # show high level information, IP, Mac, OUI
205- with c1 :
206- device_detail_url = f"/device_details?device_mac_address={ device_dict ['mac_address' ]} "
207- title_text = f'**[{ device_custom_name } ]({ device_detail_url } )**'
208- st .markdown (title_text )
209- caption = f'{ device_dict ["ip_address" ]} | { device_dict ["mac_address" ]} '
210- if "oui_vendor" in metadata_dict :
211- caption += f' | { metadata_dict ["oui_vendor" ]} '
212-
213- api_output = common .config_get (f'device_details@{ device_dict ["mac_address" ]} ' , default = {})
214- if "Vendor" in api_output or "Explanation" in api_output :
215- vendor = api_output .get ("Vendor" , "" )
216- explanation = api_output .get ("Explanation" , "" )
217- if vendor or explanation :
218- caption += f"| Vendor: { vendor } | Explanation: { explanation } "
219- st .caption (caption , help = 'IP address, MAC address, manufacturer OUI, and Device Identification API output' )
220-
221- # --- Add bar charts for upload/download ---
222- chart_col_upload , chart_col_download = st .columns (2 )
223- with chart_col_upload :
224- device_upload_graph = device_upload [device_upload ['mac_address' ] == device_dict ["mac_address" ]]
225- common .plot_traffic_volume (device_upload_graph ,
226- "Upload Traffic (sent by device) in the last 60 seconds" )
227- with chart_col_download :
228- device_download_graph = device_download [device_download ['mac_address' ] == device_dict ["mac_address" ]]
229- common .plot_traffic_volume (device_download_graph ,
230- "Download Traffic (sent by device) in the last 60 seconds" )
231- # Set whether a device is to be inspected, favorite, or blocked
232- with c2 :
233- # Maps short options to long options for display
234- option_dict = {
235- 'inspected' : ':material/troubleshoot: Inspected' ,
236- 'favorite' : ':material/favorite: Favorite' ,
237- 'blocked' : ':material/block: Blocked'
238- }
272+ device_data = get_device_data (mac_address )
273+ update_device_inspected_status (device_data )
274+
275+ # Reverse the option_dict to map long options back to short options
276+ option_reversed_dict = {v : k for k , v in option_dict .items ()}
239277
240- # Reverse the option_dict to map long options back to short options
241- option_reversed_dict = {v : k for k , v in option_dict .items ()}
242-
243- # Read the device's favorite and blocked status from the config
244- device_is_favorite_config_key = f'device_is_favorite_{ device_dict ["mac_address" ]} '
245- device_is_favorite = common .config_get (device_is_favorite_config_key , False )
246-
247- device_is_blocked_config_key = f'device_is_blocked_{ device_dict ["mac_address" ]} '
248- device_is_blocked = common .config_get (device_is_blocked_config_key , False )
249-
250- # Create a list of default options based on the device's status
251- default_option_list = []
252- if is_inspected :
253- default_option_list .append (option_dict ['inspected' ])
254- if device_is_favorite :
255- default_option_list .append (option_dict ['favorite' ])
256- if device_is_blocked :
257- default_option_list .append (option_dict ['blocked' ])
258-
259- def _device_options_changed_callback ():
260-
261- # The long options selected by the user
262- selected_list = st .session_state [f'device_options_{ device_dict ["mac_address" ]} ' ]
263-
264- # Transform the long options back to short options
265- selected_list = [option_reversed_dict [option ] for option in selected_list ]
266-
267- # Reset the device's status based on the selected options
268- common .config_set (device_is_favorite_config_key , 'favorite' in selected_list )
269- common .config_set (device_is_blocked_config_key , 'blocked' in selected_list )
270- common .config_set (device_inspected_config_key , 'inspected' in selected_list )
271-
272- # Create a list of options to display
273- st .pills (
274- 'Options' ,
275- options = option_dict .values (),
276- selection_mode = 'multi' ,
277- default = default_option_list ,
278- label_visibility = 'collapsed' ,
279- key = f"device_options_{ device_dict ['mac_address' ]} " ,
280- on_change = _device_options_changed_callback
281- )
278+ # Read the device's favorite and blocked status from the config
279+ device_is_favorite_config_key = f'device_is_favorite_{ mac_address } '
280+ device_is_favorite = common .config_get (device_is_favorite_config_key , False )
281+
282+ device_is_blocked_config_key = f'device_is_blocked_{ mac_address } '
283+ device_is_blocked = common .config_get (device_is_blocked_config_key , False )
284+
285+ # Create a list of default options based on the device's status
286+ default_option_list = []
287+ device_inspected_config_key = f'device_is_inspected_{ mac_address } '
288+ is_inspected = common .config_get (device_inspected_config_key , False )
289+ if is_inspected :
290+ default_option_list .append (option_dict ['inspected' ])
291+ if device_is_favorite :
292+ default_option_list .append (option_dict ['favorite' ])
293+ if device_is_blocked :
294+ default_option_list .append (option_dict ['blocked' ])
295+
296+ def _device_options_changed_callback ():
297+ # The long options selected by the user
298+ selected_list = st .session_state [f'device_options_{ mac_address } ' ]
299+
300+ # Transform the long options back to short options
301+ selected_list = [option_reversed_dict [option ] for option in selected_list ]
302+
303+ # Reset the device's status based on the selected options
304+ common .config_set (device_is_favorite_config_key , 'favorite' in selected_list )
305+ common .config_set (device_is_blocked_config_key , 'blocked' in selected_list )
306+ common .config_set (device_inspected_config_key , 'inspected' in selected_list )
307+
308+ # Create a list of options to display
309+ st .pills (
310+ 'Options' ,
311+ options = option_dict .values (),
312+ selection_mode = 'multi' ,
313+ default = default_option_list ,
314+ label_visibility = 'collapsed' ,
315+ key = f"device_options_{ mac_address } " ,
316+ on_change = _device_options_changed_callback
317+ )
0 commit comments