@@ -222,9 +222,42 @@ def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613
222
222
return infos
223
223
224
224
def showNodes (
225
- self , includeSelf : bool = True
225
+ self , includeSelf : bool = True , showFields : Optional [ List [ str ]] = None
226
226
) -> 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
+
228
261
229
262
def formatFloat (value , precision = 2 , unit = "" ) -> Optional [str ]:
230
263
"""Format a float value with precision."""
@@ -246,6 +279,29 @@ def getTimeAgo(ts) -> Optional[str]:
246
279
return None # not handling a timestamp from the future
247
280
return _timeago (delta_secs )
248
281
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
+
249
305
rows : List [Dict [str , Any ]] = []
250
306
if self .nodesByNum :
251
307
logging .debug (f"self.nodes:{ self .nodes } " )
@@ -254,66 +310,60 @@ def getTimeAgo(ts) -> Optional[str]:
254
310
continue
255
311
256
312
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
- )
285
313
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" )
292
324
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
305
360
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
315
362
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 )
317
367
318
368
rows .sort (key = lambda r : r .get ("LastHeard" ) or "0000" , reverse = True )
319
369
for i , row in enumerate (rows ):
0 commit comments