@@ -222,9 +222,42 @@ def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613
222222 return infos
223223
224224 def showNodes (
225- self , includeSelf : bool = True
225+ self , includeSelf : bool = True , showFields : Optional [ List [ str ]] = None
226226 ) -> str : # pylint: disable=W0613
227- """Show table summary of nodes in mesh"""
227+ """Show table summary of nodes in mesh
228+
229+ Args:
230+ includeSelf (bool): Include ourself in the output?
231+ showFields (List[str]): List of fields to show in output
232+ """
233+
234+ def get_human_readable (name ):
235+ name_map = {
236+ "user.longName" : "User" ,
237+ "user.id" : "ID" ,
238+ "user.shortName" : "AKA" ,
239+ "user.hwModel" : "Hardware" ,
240+ "user.publicKey" : "Pubkey" ,
241+ "user.role" : "Role" ,
242+ "position.latitude" : "Latitude" ,
243+ "position.longitude" : "Longitude" ,
244+ "position.altitude" : "Altitude" ,
245+ "deviceMetrics.batteryLevel" : "Battery" ,
246+ "deviceMetrics.channelUtilization" : "Channel util." ,
247+ "deviceMetrics.airUtilTx" : "Tx air util." ,
248+ "snr" : "SNR" ,
249+ "hopsAway" : "Hops" ,
250+ "channel" : "Channel" ,
251+ "lastHeard" : "LastHeard" ,
252+ "since" : "Since" ,
253+
254+ }
255+
256+ if name in name_map :
257+ return name_map .get (name ) # Default to a formatted guess
258+ else :
259+ return name
260+
228261
229262 def formatFloat (value , precision = 2 , unit = "" ) -> Optional [str ]:
230263 """Format a float value with precision."""
@@ -246,6 +279,29 @@ def getTimeAgo(ts) -> Optional[str]:
246279 return None # not handling a timestamp from the future
247280 return _timeago (delta_secs )
248281
282+ def getNestedValue (node_dict : Dict [str , Any ], key_path : str ) -> Any :
283+ if key_path .index ("." ) < 0 :
284+ logging .debug ("getNestedValue was called without a nested path." )
285+ return None
286+ keys = key_path .split ("." )
287+ value : Optional [Union [str , dict ]] = node_dict
288+ for key in keys :
289+ if isinstance (value , dict ):
290+ value = value .get (key )
291+ else :
292+ return None
293+ return value
294+
295+ if showFields is None or len (showFields ) == 0 :
296+ # The default set of fields to show (e.g., the status quo)
297+ showFields = ["N" , "user.longName" , "user.id" , "user.shortName" , "user.hwModel" , "user.publicKey" ,
298+ "user.role" , "position.latitude" , "position.longitude" , "position.altitude" ,
299+ "deviceMetrics.batteryLevel" , "deviceMetrics.channelUtilization" ,
300+ "deviceMetrics.airUtilTx" , "snr" , "hopsAway" , "channel" , "lastHeard" , "since" ]
301+ else :
302+ # Always at least include the row number.
303+ showFields .insert (0 , "N" )
304+
249305 rows : List [Dict [str , Any ]] = []
250306 if self .nodesByNum :
251307 logging .debug (f"self.nodes:{ self .nodes } " )
@@ -254,66 +310,60 @@ def getTimeAgo(ts) -> Optional[str]:
254310 continue
255311
256312 presumptive_id = f"!{ node ['num' ]:08x} "
257- row = {
258- "N" : 0 ,
259- "User" : f"Meshtastic { presumptive_id [- 4 :]} " ,
260- "ID" : presumptive_id ,
261- }
262-
263- user = node .get ("user" )
264- if user :
265- row .update (
266- {
267- "User" : user .get ("longName" , "N/A" ),
268- "AKA" : user .get ("shortName" , "N/A" ),
269- "ID" : user ["id" ],
270- "Hardware" : user .get ("hwModel" , "UNSET" ),
271- "Pubkey" : user .get ("publicKey" , "UNSET" ),
272- "Role" : user .get ("role" , "N/A" ),
273- }
274- )
275-
276- pos = node .get ("position" )
277- if pos :
278- row .update (
279- {
280- "Latitude" : formatFloat (pos .get ("latitude" ), 4 , "°" ),
281- "Longitude" : formatFloat (pos .get ("longitude" ), 4 , "°" ),
282- "Altitude" : formatFloat (pos .get ("altitude" ), 0 , " m" ),
283- }
284- )
285313
286- metrics = node .get ("deviceMetrics" )
287- if metrics :
288- batteryLevel = metrics .get ("batteryLevel" )
289- if batteryLevel is not None :
290- if batteryLevel == 0 :
291- batteryString = "Powered"
314+ # This allows the user to specify fields that wouldn't otherwise be included.
315+ fields = {}
316+ for field in showFields :
317+ if "." in field :
318+ raw_value = getNestedValue (node , field )
319+ else :
320+ # The "since" column is synthesized, it's not retrieved from the device. Get the
321+ # lastHeard value here, and then we'll format it properly below.
322+ if field == "since" :
323+ raw_value = node .get ("lastHeard" )
292324 else :
293- batteryString = str (batteryLevel ) + "%"
294- row .update ({"Battery" : batteryString })
295- row .update (
296- {
297- "Channel util." : formatFloat (
298- metrics .get ("channelUtilization" ), 2 , "%"
299- ),
300- "Tx air util." : formatFloat (
301- metrics .get ("airUtilTx" ), 2 , "%"
302- ),
303- }
304- )
325+ raw_value = node .get (field )
326+
327+ formatted_value : Optional [str ] = ""
328+
329+ # Some of these need special formatting or processing.
330+ if field == "channel" :
331+ if raw_value is None :
332+ formatted_value = "0"
333+ elif field == "deviceMetrics.channelUtilization" :
334+ formatted_value = formatFloat (raw_value , 2 , "%" )
335+ elif field == "deviceMetrics.airUtilTx" :
336+ formatted_value = formatFloat (raw_value , 2 , "%" )
337+ elif field == "deviceMetrics.batteryLevel" :
338+ if raw_value in (0 , 101 ):
339+ formatted_value = "Powered"
340+ else :
341+ formatted_value = formatFloat (raw_value , 0 , "%" )
342+ elif field == "lastHeard" :
343+ formatted_value = getLH (raw_value )
344+ elif field == "position.latitude" :
345+ formatted_value = formatFloat (raw_value , 4 , "°" )
346+ elif field == "position.longitude" :
347+ formatted_value = formatFloat (raw_value , 4 , "°" )
348+ elif field == "position.altitude" :
349+ formatted_value = formatFloat (raw_value , 0 , "m" )
350+ elif field == "since" :
351+ formatted_value = getTimeAgo (raw_value ) or "N/A"
352+ elif field == "snr" :
353+ formatted_value = formatFloat (raw_value , 0 , " dB" )
354+ elif field == "user.shortName" :
355+ formatted_value = raw_value if raw_value is not None else f'Meshtastic { presumptive_id [- 4 :]} '
356+ elif field == "user.id" :
357+ formatted_value = raw_value if raw_value is not None else presumptive_id
358+ else :
359+ formatted_value = raw_value # No special formatting
305360
306- row .update (
307- {
308- "SNR" : formatFloat (node .get ("snr" ), 2 , " dB" ),
309- "Hops" : node .get ("hopsAway" , "?" ),
310- "Channel" : node .get ("channel" , 0 ),
311- "LastHeard" : getLH (node .get ("lastHeard" )),
312- "Since" : getTimeAgo (node .get ("lastHeard" )),
313- }
314- )
361+ fields [field ] = formatted_value
315362
316- rows .append (row )
363+ # Filter out any field in the data set that was not specified.
364+ filteredData = {get_human_readable (k ): v for k , v in fields .items () if k in showFields }
365+ filteredData .update ({get_human_readable (k ): v for k , v in fields .items ()})
366+ rows .append (filteredData )
317367
318368 rows .sort (key = lambda r : r .get ("LastHeard" ) or "0000" , reverse = True )
319369 for i , row in enumerate (rows ):
0 commit comments