aprsd 3.3.3__py2.py3-none-any.whl → 3.4.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. aprsd/client.py +133 -20
  2. aprsd/clients/aprsis.py +6 -3
  3. aprsd/clients/fake.py +1 -1
  4. aprsd/clients/kiss.py +1 -1
  5. aprsd/cmds/completion.py +13 -27
  6. aprsd/cmds/fetch_stats.py +53 -57
  7. aprsd/cmds/healthcheck.py +32 -30
  8. aprsd/cmds/list_plugins.py +2 -2
  9. aprsd/cmds/listen.py +33 -17
  10. aprsd/cmds/send_message.py +2 -2
  11. aprsd/cmds/server.py +26 -9
  12. aprsd/cmds/webchat.py +34 -29
  13. aprsd/conf/common.py +46 -31
  14. aprsd/log/log.py +28 -6
  15. aprsd/main.py +20 -18
  16. aprsd/packets/__init__.py +3 -2
  17. aprsd/packets/collector.py +56 -0
  18. aprsd/packets/core.py +456 -321
  19. aprsd/packets/log.py +143 -0
  20. aprsd/packets/packet_list.py +83 -66
  21. aprsd/packets/seen_list.py +30 -19
  22. aprsd/packets/tracker.py +60 -62
  23. aprsd/packets/watch_list.py +64 -38
  24. aprsd/plugin.py +41 -16
  25. aprsd/plugins/email.py +35 -7
  26. aprsd/plugins/time.py +3 -2
  27. aprsd/plugins/version.py +4 -5
  28. aprsd/plugins/weather.py +0 -1
  29. aprsd/stats/__init__.py +20 -0
  30. aprsd/stats/app.py +46 -0
  31. aprsd/stats/collector.py +38 -0
  32. aprsd/threads/__init__.py +3 -2
  33. aprsd/threads/aprsd.py +67 -36
  34. aprsd/threads/keep_alive.py +55 -49
  35. aprsd/threads/log_monitor.py +46 -0
  36. aprsd/threads/rx.py +43 -24
  37. aprsd/threads/stats.py +44 -0
  38. aprsd/threads/tx.py +36 -17
  39. aprsd/utils/__init__.py +12 -0
  40. aprsd/utils/counter.py +6 -3
  41. aprsd/utils/json.py +20 -0
  42. aprsd/utils/objectstore.py +22 -17
  43. aprsd/web/admin/static/css/prism.css +4 -189
  44. aprsd/web/admin/static/js/charts.js +9 -7
  45. aprsd/web/admin/static/js/echarts.js +71 -9
  46. aprsd/web/admin/static/js/main.js +47 -6
  47. aprsd/web/admin/static/js/prism.js +11 -2246
  48. aprsd/web/admin/templates/index.html +18 -7
  49. aprsd/web/chat/static/js/gps.js +3 -1
  50. aprsd/web/chat/static/js/main.js +4 -3
  51. aprsd/web/chat/static/js/send-message.js +5 -2
  52. aprsd/web/chat/templates/index.html +1 -0
  53. aprsd/wsgi.py +62 -127
  54. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/METADATA +14 -16
  55. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/RECORD +60 -65
  56. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/WHEEL +1 -1
  57. aprsd-3.4.0.dist-info/pbr.json +1 -0
  58. aprsd/plugins/query.py +0 -81
  59. aprsd/rpc/__init__.py +0 -14
  60. aprsd/rpc/client.py +0 -165
  61. aprsd/rpc/server.py +0 -99
  62. aprsd/stats.py +0 -266
  63. aprsd/threads/store.py +0 -30
  64. aprsd/utils/converters.py +0 -15
  65. aprsd/web/admin/static/json-viewer/jquery.json-viewer.css +0 -57
  66. aprsd/web/admin/static/json-viewer/jquery.json-viewer.js +0 -158
  67. aprsd/web/chat/static/json-viewer/jquery.json-viewer.css +0 -57
  68. aprsd/web/chat/static/json-viewer/jquery.json-viewer.js +0 -158
  69. aprsd-3.3.3.dist-info/pbr.json +0 -1
  70. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/LICENSE +0 -0
  71. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/entry_points.txt +0 -0
  72. {aprsd-3.3.3.dist-info → aprsd-3.4.0.dist-info}/top_level.txt +0 -0
aprsd/client.py CHANGED
@@ -1,15 +1,18 @@
1
1
  import abc
2
+ import datetime
2
3
  import logging
4
+ import threading
3
5
  import time
4
6
 
5
7
  import aprslib
6
8
  from aprslib.exceptions import LoginError
7
9
  from oslo_config import cfg
10
+ import wrapt
8
11
 
9
12
  from aprsd import exception
10
13
  from aprsd.clients import aprsis, fake, kiss
11
- from aprsd.packets import core, packet_list
12
- from aprsd.utils import trace
14
+ from aprsd.packets import core
15
+ from aprsd.utils import singleton, trace
13
16
 
14
17
 
15
18
  CONF = cfg.CONF
@@ -25,6 +28,34 @@ TRANSPORT_FAKE = "fake"
25
28
  factory = None
26
29
 
27
30
 
31
+ @singleton
32
+ class APRSClientStats:
33
+
34
+ lock = threading.Lock()
35
+
36
+ @wrapt.synchronized(lock)
37
+ def stats(self, serializable=False):
38
+ client = factory.create()
39
+ stats = {
40
+ "transport": client.transport(),
41
+ "filter": client.filter,
42
+ "connected": client.connected,
43
+ }
44
+
45
+ if client.transport() == TRANSPORT_APRSIS:
46
+ stats["server_string"] = client.client.server_string
47
+ keepalive = client.client.aprsd_keepalive
48
+ if serializable:
49
+ keepalive = keepalive.isoformat()
50
+ stats["server_keepalive"] = keepalive
51
+ elif client.transport() == TRANSPORT_TCPKISS:
52
+ stats["host"] = CONF.kiss_tcp.host
53
+ stats["port"] = CONF.kiss_tcp.port
54
+ elif client.transport() == TRANSPORT_SERIALKISS:
55
+ stats["device"] = CONF.kiss_serial.device
56
+ return stats
57
+
58
+
28
59
  class Client:
29
60
  """Singleton client class that constructs the aprslib connection."""
30
61
 
@@ -32,16 +63,21 @@ class Client:
32
63
  _client = None
33
64
 
34
65
  connected = False
35
- server_string = None
36
66
  filter = None
67
+ lock = threading.Lock()
37
68
 
38
69
  def __new__(cls, *args, **kwargs):
39
70
  """This magic turns this into a singleton."""
40
71
  if cls._instance is None:
41
72
  cls._instance = super().__new__(cls)
42
73
  # Put any initialization here.
74
+ cls._instance._create_client()
43
75
  return cls._instance
44
76
 
77
+ @abc.abstractmethod
78
+ def stats(self) -> dict:
79
+ pass
80
+
45
81
  def set_filter(self, filter):
46
82
  self.filter = filter
47
83
  if self._client:
@@ -50,21 +86,32 @@ class Client:
50
86
  @property
51
87
  def client(self):
52
88
  if not self._client:
53
- LOG.info("Creating APRS client")
54
- self._client = self.setup_connection()
55
- if self.filter:
56
- LOG.info("Creating APRS client filter")
57
- self._client.set_filter(self.filter)
89
+ self._create_client()
58
90
  return self._client
59
91
 
92
+ def _create_client(self):
93
+ self._client = self.setup_connection()
94
+ if self.filter:
95
+ LOG.info("Creating APRS client filter")
96
+ self._client.set_filter(self.filter)
97
+
98
+ def stop(self):
99
+ if self._client:
100
+ LOG.info("Stopping client connection.")
101
+ self._client.stop()
102
+
60
103
  def send(self, packet: core.Packet):
61
- packet_list.PacketList().tx(packet)
104
+ """Send a packet to the network."""
62
105
  self.client.send(packet)
63
106
 
107
+ @wrapt.synchronized(lock)
64
108
  def reset(self):
65
109
  """Call this to force a rebuild/reconnect."""
110
+ LOG.info("Resetting client connection.")
66
111
  if self._client:
112
+ self._client.close()
67
113
  del self._client
114
+ self._create_client()
68
115
  else:
69
116
  LOG.warning("Client not initialized, nothing to reset.")
70
117
 
@@ -89,11 +136,38 @@ class Client:
89
136
  def decode_packet(self, *args, **kwargs):
90
137
  pass
91
138
 
139
+ @abc.abstractmethod
140
+ def consumer(self, callback, blocking=False, immortal=False, raw=False):
141
+ pass
142
+
143
+ @abc.abstractmethod
144
+ def is_alive(self):
145
+ pass
146
+
147
+ @abc.abstractmethod
148
+ def close(self):
149
+ pass
150
+
92
151
 
93
152
  class APRSISClient(Client):
94
153
 
95
154
  _client = None
96
155
 
156
+ def __init__(self):
157
+ max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
158
+ self.max_delta = datetime.timedelta(**max_timeout)
159
+
160
+ def stats(self) -> dict:
161
+ stats = {}
162
+ if self.is_configured():
163
+ stats = {
164
+ "server_string": self._client.server_string,
165
+ "sever_keepalive": self._client.aprsd_keepalive,
166
+ "filter": self.filter,
167
+ }
168
+
169
+ return stats
170
+
97
171
  @staticmethod
98
172
  def is_enabled():
99
173
  # Defaults to True if the enabled flag is non existent
@@ -125,44 +199,56 @@ class APRSISClient(Client):
125
199
  return True
126
200
  return True
127
201
 
202
+ def _is_stale_connection(self):
203
+ delta = datetime.datetime.now() - self._client.aprsd_keepalive
204
+ if delta > self.max_delta:
205
+ LOG.error(f"Connection is stale, last heard {delta} ago.")
206
+ return True
207
+
128
208
  def is_alive(self):
129
209
  if self._client:
130
- return self._client.is_alive()
210
+ return self._client.is_alive() and not self._is_stale_connection()
131
211
  else:
212
+ LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!")
132
213
  return False
133
214
 
215
+ def close(self):
216
+ if self._client:
217
+ self._client.stop()
218
+ self._client.close()
219
+
134
220
  @staticmethod
135
221
  def transport():
136
222
  return TRANSPORT_APRSIS
137
223
 
138
224
  def decode_packet(self, *args, **kwargs):
139
225
  """APRS lib already decodes this."""
140
- return core.Packet.factory(args[0])
226
+ return core.factory(args[0])
141
227
 
142
228
  def setup_connection(self):
143
229
  user = CONF.aprs_network.login
144
230
  password = CONF.aprs_network.password
145
231
  host = CONF.aprs_network.host
146
232
  port = CONF.aprs_network.port
147
- connected = False
233
+ self.connected = False
148
234
  backoff = 1
149
235
  aprs_client = None
150
- while not connected:
236
+ while not self.connected:
151
237
  try:
152
- LOG.info("Creating aprslib client")
238
+ LOG.info(f"Creating aprslib client({host}:{port}) and logging in {user}.")
153
239
  aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
154
240
  # Force the log to be the same
155
241
  aprs_client.logger = LOG
156
242
  aprs_client.connect()
157
- connected = True
243
+ self.connected = True
158
244
  backoff = 1
159
245
  except LoginError as e:
160
246
  LOG.error(f"Failed to login to APRS-IS Server '{e}'")
161
- connected = False
247
+ self.connected = False
162
248
  time.sleep(backoff)
163
249
  except Exception as e:
164
250
  LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
165
- connected = False
251
+ self.connected = False
166
252
  time.sleep(backoff)
167
253
  # Don't allow the backoff to go to inifinity.
168
254
  if backoff > 5:
@@ -170,15 +256,28 @@ class APRSISClient(Client):
170
256
  else:
171
257
  backoff += 1
172
258
  continue
173
- LOG.debug(f"Logging in to APRS-IS with user '{user}'")
174
259
  self._client = aprs_client
175
260
  return aprs_client
176
261
 
262
+ def consumer(self, callback, blocking=False, immortal=False, raw=False):
263
+ self._client.consumer(
264
+ callback, blocking=blocking,
265
+ immortal=immortal, raw=raw,
266
+ )
267
+
177
268
 
178
269
  class KISSClient(Client):
179
270
 
180
271
  _client = None
181
272
 
273
+ def stats(self) -> dict:
274
+ stats = {}
275
+ if self.is_configured():
276
+ return {
277
+ "transport": self.transport(),
278
+ }
279
+ return stats
280
+
182
281
  @staticmethod
183
282
  def is_enabled():
184
283
  """Return if tcp or serial KISS is enabled."""
@@ -217,6 +316,10 @@ class KISSClient(Client):
217
316
  else:
218
317
  return False
219
318
 
319
+ def close(self):
320
+ if self._client:
321
+ self._client.stop()
322
+
220
323
  @staticmethod
221
324
  def transport():
222
325
  if CONF.kiss_serial.enabled:
@@ -238,7 +341,7 @@ class KISSClient(Client):
238
341
  # LOG.debug(f"Decoding {msg}")
239
342
 
240
343
  raw = aprslib.parse(str(frame))
241
- packet = core.Packet.factory(raw)
344
+ packet = core.factory(raw)
242
345
  if isinstance(packet, core.ThirdParty):
243
346
  return packet.subpacket
244
347
  else:
@@ -246,11 +349,18 @@ class KISSClient(Client):
246
349
 
247
350
  def setup_connection(self):
248
351
  self._client = kiss.KISS3Client()
352
+ self.connected = True
249
353
  return self._client
250
354
 
355
+ def consumer(self, callback, blocking=False, immortal=False, raw=False):
356
+ self._client.consumer(callback)
357
+
251
358
 
252
359
  class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
253
360
 
361
+ def stats(self) -> dict:
362
+ return {}
363
+
254
364
  @staticmethod
255
365
  def is_enabled():
256
366
  if CONF.fake_client.enabled:
@@ -264,7 +374,11 @@ class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
264
374
  def is_alive(self):
265
375
  return True
266
376
 
377
+ def close(self):
378
+ pass
379
+
267
380
  def setup_connection(self):
381
+ self.connected = True
268
382
  return fake.APRSDFakeClient()
269
383
 
270
384
  @staticmethod
@@ -304,7 +418,6 @@ class ClientFactory:
304
418
  key = TRANSPORT_FAKE
305
419
 
306
420
  builder = self._builders.get(key)
307
- LOG.debug(f"Creating client {key}")
308
421
  if not builder:
309
422
  raise ValueError(key)
310
423
  return builder()
aprsd/clients/aprsis.py CHANGED
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  import logging
2
3
  import select
3
4
  import threading
@@ -11,7 +12,6 @@ from aprslib.exceptions import (
11
12
  import wrapt
12
13
 
13
14
  import aprsd
14
- from aprsd import stats
15
15
  from aprsd.packets import core
16
16
 
17
17
 
@@ -24,6 +24,9 @@ class Aprsdis(aprslib.IS):
24
24
  # flag to tell us to stop
25
25
  thread_stop = False
26
26
 
27
+ # date for last time we heard from the server
28
+ aprsd_keepalive = datetime.datetime.now()
29
+
27
30
  # timeout in seconds
28
31
  select_timeout = 1
29
32
  lock = threading.Lock()
@@ -142,7 +145,6 @@ class Aprsdis(aprslib.IS):
142
145
 
143
146
  self.logger.info(f"Connected to {server_string}")
144
147
  self.server_string = server_string
145
- stats.APRSDStats().set_aprsis_server(server_string)
146
148
 
147
149
  except LoginError as e:
148
150
  self.logger.error(str(e))
@@ -176,13 +178,14 @@ class Aprsdis(aprslib.IS):
176
178
  try:
177
179
  for line in self._socket_readlines(blocking):
178
180
  if line[0:1] != b"#":
181
+ self.aprsd_keepalive = datetime.datetime.now()
179
182
  if raw:
180
183
  callback(line)
181
184
  else:
182
185
  callback(self._parse(line))
183
186
  else:
184
187
  self.logger.debug("Server: %s", line.decode("utf8"))
185
- stats.APRSDStats().set_aprsis_keepalive()
188
+ self.aprsd_keepalive = datetime.datetime.now()
186
189
  except ParseError as exp:
187
190
  self.logger.log(
188
191
  11,
aprsd/clients/fake.py CHANGED
@@ -67,7 +67,7 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
67
67
  # Generate packets here?
68
68
  raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
69
69
  pkt_raw = aprslib.parse(raw)
70
- pkt = core.Packet.factory(pkt_raw)
70
+ pkt = core.factory(pkt_raw)
71
71
  callback(packet=pkt)
72
72
  LOG.debug(f"END blocking FAKE consumer {self}")
73
73
  time.sleep(8)
aprsd/clients/kiss.py CHANGED
@@ -81,7 +81,7 @@ class KISS3Client:
81
81
  LOG.error("Failed to parse bytes received from KISS interface.")
82
82
  LOG.exception(ex)
83
83
 
84
- def consumer(self, callback, blocking=False, immortal=False, raw=False):
84
+ def consumer(self, callback):
85
85
  LOG.debug("Start blocking KISS consumer")
86
86
  self._parse_callback = callback
87
87
  self.kiss.read(callback=self.parse_frame, min_frames=None)
aprsd/cmds/completion.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import click
2
- import click_completion
2
+ import click.shell_completion
3
3
 
4
4
  from aprsd.main import cli
5
5
 
@@ -7,30 +7,16 @@ from aprsd.main import cli
7
7
  CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
8
8
 
9
9
 
10
- @cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS)
11
- @click.pass_context
12
- def completion(ctx):
13
- pass
10
+ @cli.command()
11
+ @click.argument("shell", type=click.Choice(list(click.shell_completion._available_shells)))
12
+ def completion(shell):
13
+ """Show the shell completion code"""
14
+ from click.utils import _detect_program_name
14
15
 
15
-
16
- # show dumps out the completion code for a particular shell
17
- @completion.command(help="Show completion code for shell", name="show")
18
- @click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
19
- @click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
20
- def show(shell, case_insensitive):
21
- """Show the click-completion-command completion code"""
22
- extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
23
- click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
24
-
25
-
26
- # install will install the completion code for a particular shell
27
- @completion.command(help="Install completion code for a shell", name="install")
28
- @click.option("--append/--overwrite", help="Append the completion code to the file", default=None)
29
- @click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
30
- @click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
31
- @click.argument("path", required=False)
32
- def install(append, case_insensitive, shell, path):
33
- """Install the click-completion-command completion"""
34
- extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
35
- shell, path = click_completion.core.install(shell=shell, path=path, append=append, extra_env=extra_env)
36
- click.echo(f"{shell} completion installed in {path}")
16
+ cls = click.shell_completion.get_completion_class(shell)
17
+ prog_name = _detect_program_name()
18
+ complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
19
+ print(cls(cli, {}, prog_name, complete_var).source())
20
+ print("# Add the following line to your shell configuration file to have aprsd command line completion")
21
+ print("# but remove the leading '#' character.")
22
+ print(f"# eval \"$(aprsd completion {shell})\"")
aprsd/cmds/fetch_stats.py CHANGED
@@ -1,10 +1,9 @@
1
- # Fetch active stats from a remote running instance of aprsd server
2
- # This uses the RPC server to fetch the stats from the remote server.
3
-
1
+ # Fetch active stats from a remote running instance of aprsd admin web interface.
4
2
  import logging
5
3
 
6
4
  import click
7
5
  from oslo_config import cfg
6
+ import requests
8
7
  from rich.console import Console
9
8
  from rich.table import Table
10
9
 
@@ -12,7 +11,6 @@ from rich.table import Table
12
11
  import aprsd
13
12
  from aprsd import cli_helper
14
13
  from aprsd.main import cli
15
- from aprsd.rpc import client as rpc_client
16
14
 
17
15
 
18
16
  # setup the global logger
@@ -26,87 +24,80 @@ CONF = cfg.CONF
26
24
  @click.option(
27
25
  "--host", type=str,
28
26
  default=None,
29
- help="IP address of the remote aprsd server to fetch stats from.",
27
+ help="IP address of the remote aprsd admin web ui fetch stats from.",
30
28
  )
31
29
  @click.option(
32
30
  "--port", type=int,
33
31
  default=None,
34
- help="Port of the remote aprsd server rpc port to fetch stats from.",
35
- )
36
- @click.option(
37
- "--magic-word", type=str,
38
- default=None,
39
- help="Magic word of the remote aprsd server rpc port to fetch stats from.",
32
+ help="Port of the remote aprsd web admin interface to fetch stats from.",
40
33
  )
41
34
  @click.pass_context
42
35
  @cli_helper.process_standard_options
43
- def fetch_stats(ctx, host, port, magic_word):
44
- """Fetch stats from a remote running instance of aprsd server."""
45
- LOG.info(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
36
+ def fetch_stats(ctx, host, port):
37
+ """Fetch stats from a APRSD admin web interface."""
38
+ console = Console()
39
+ console.print(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
46
40
 
47
41
  CONF.log_opt_values(LOG, logging.DEBUG)
48
42
  if not host:
49
- host = CONF.rpc_settings.ip
43
+ host = CONF.admin.web_ip
50
44
  if not port:
51
- port = CONF.rpc_settings.port
52
- if not magic_word:
53
- magic_word = CONF.rpc_settings.magic_word
45
+ port = CONF.admin.web_port
54
46
 
55
- msg = f"Fetching stats from {host}:{port} with magic word '{magic_word}'"
56
- console = Console()
47
+ msg = f"Fetching stats from {host}:{port}"
57
48
  console.print(msg)
58
49
  with console.status(msg):
59
- client = rpc_client.RPCClient(host, port, magic_word)
60
- stats = client.get_stats_dict()
61
- if stats:
62
- console.print_json(data=stats)
63
- else:
64
- LOG.error(f"Failed to fetch stats via RPC aprsd server at {host}:{port}")
50
+ response = requests.get(f"http://{host}:{port}/stats", timeout=120)
51
+ if not response:
52
+ console.print(
53
+ f"Failed to fetch stats from {host}:{port}?",
54
+ style="bold red",
55
+ )
65
56
  return
57
+
58
+ stats = response.json()
59
+ if not stats:
60
+ console.print(
61
+ f"Failed to fetch stats from aprsd admin ui at {host}:{port}",
62
+ style="bold red",
63
+ )
64
+ return
65
+
66
66
  aprsd_title = (
67
67
  "APRSD "
68
- f"[bold cyan]v{stats['aprsd']['version']}[/] "
69
- f"Callsign [bold green]{stats['aprsd']['callsign']}[/] "
70
- f"Uptime [bold yellow]{stats['aprsd']['uptime']}[/]"
68
+ f"[bold cyan]v{stats['APRSDStats']['version']}[/] "
69
+ f"Callsign [bold green]{stats['APRSDStats']['callsign']}[/] "
70
+ f"Uptime [bold yellow]{stats['APRSDStats']['uptime']}[/]"
71
71
  )
72
72
 
73
- console.rule(f"Stats from {host}:{port} with magic word '{magic_word}'")
73
+ console.rule(f"Stats from {host}:{port}")
74
74
  console.print("\n\n")
75
75
  console.rule(aprsd_title)
76
76
 
77
77
  # Show the connection to APRS
78
78
  # It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
79
79
  if "aprs-is" in stats:
80
- title = f"APRS-IS Connection {stats['aprs-is']['server']}"
80
+ title = f"APRS-IS Connection {stats['APRSClientStats']['server_string']}"
81
81
  table = Table(title=title)
82
82
  table.add_column("Key")
83
83
  table.add_column("Value")
84
- for key, value in stats["aprs-is"].items():
84
+ for key, value in stats["APRSClientStats"].items():
85
85
  table.add_row(key, value)
86
86
  console.print(table)
87
87
 
88
88
  threads_table = Table(title="Threads")
89
89
  threads_table.add_column("Name")
90
90
  threads_table.add_column("Alive?")
91
- for name, alive in stats["aprsd"]["threads"].items():
91
+ for name, alive in stats["APRSDThreadList"].items():
92
92
  threads_table.add_row(name, str(alive))
93
93
 
94
94
  console.print(threads_table)
95
95
 
96
- msgs_table = Table(title="Messages")
97
- msgs_table.add_column("Key")
98
- msgs_table.add_column("Value")
99
- for key, value in stats["messages"].items():
100
- msgs_table.add_row(key, str(value))
101
-
102
- console.print(msgs_table)
103
-
104
96
  packet_totals = Table(title="Packet Totals")
105
97
  packet_totals.add_column("Key")
106
98
  packet_totals.add_column("Value")
107
- packet_totals.add_row("Total Received", str(stats["packets"]["total_received"]))
108
- packet_totals.add_row("Total Sent", str(stats["packets"]["total_sent"]))
109
- packet_totals.add_row("Total Tracked", str(stats["packets"]["total_tracked"]))
99
+ packet_totals.add_row("Total Received", str(stats["PacketList"]["rx"]))
100
+ packet_totals.add_row("Total Sent", str(stats["PacketList"]["tx"]))
110
101
  console.print(packet_totals)
111
102
 
112
103
  # Show each of the packet types
@@ -114,47 +105,52 @@ def fetch_stats(ctx, host, port, magic_word):
114
105
  packets_table.add_column("Packet Type")
115
106
  packets_table.add_column("TX")
116
107
  packets_table.add_column("RX")
117
- for key, value in stats["packets"]["by_type"].items():
108
+ for key, value in stats["PacketList"]["packets"].items():
118
109
  packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
119
110
 
120
111
  console.print(packets_table)
121
112
 
122
113
  if "plugins" in stats:
123
- count = len(stats["plugins"])
114
+ count = len(stats["PluginManager"])
124
115
  plugins_table = Table(title=f"Plugins ({count})")
125
116
  plugins_table.add_column("Plugin")
126
117
  plugins_table.add_column("Enabled")
127
118
  plugins_table.add_column("Version")
128
119
  plugins_table.add_column("TX")
129
120
  plugins_table.add_column("RX")
130
- for key, value in stats["plugins"].items():
121
+ plugins = stats["PluginManager"]
122
+ for key, value in plugins.items():
131
123
  plugins_table.add_row(
132
124
  key,
133
- str(stats["plugins"][key]["enabled"]),
134
- stats["plugins"][key]["version"],
135
- str(stats["plugins"][key]["tx"]),
136
- str(stats["plugins"][key]["rx"]),
125
+ str(plugins[key]["enabled"]),
126
+ plugins[key]["version"],
127
+ str(plugins[key]["tx"]),
128
+ str(plugins[key]["rx"]),
137
129
  )
138
130
 
139
131
  console.print(plugins_table)
140
132
 
141
- if "seen_list" in stats["aprsd"]:
142
- count = len(stats["aprsd"]["seen_list"])
133
+ seen_list = stats.get("SeenList")
134
+
135
+ if seen_list:
136
+ count = len(seen_list)
143
137
  seen_table = Table(title=f"Seen List ({count})")
144
138
  seen_table.add_column("Callsign")
145
139
  seen_table.add_column("Message Count")
146
140
  seen_table.add_column("Last Heard")
147
- for key, value in stats["aprsd"]["seen_list"].items():
141
+ for key, value in seen_list.items():
148
142
  seen_table.add_row(key, str(value["count"]), value["last"])
149
143
 
150
144
  console.print(seen_table)
151
145
 
152
- if "watch_list" in stats["aprsd"]:
153
- count = len(stats["aprsd"]["watch_list"])
146
+ watch_list = stats.get("WatchList")
147
+
148
+ if watch_list:
149
+ count = len(watch_list)
154
150
  watch_table = Table(title=f"Watch List ({count})")
155
151
  watch_table.add_column("Callsign")
156
152
  watch_table.add_column("Last Heard")
157
- for key, value in stats["aprsd"]["watch_list"].items():
153
+ for key, value in watch_list.items():
158
154
  watch_table.add_row(key, value["last"])
159
155
 
160
156
  console.print(watch_table)