aprsd 1.0.0__py3-none-any.whl → 3.4.2__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 (140) hide show
  1. aprsd/__init__.py +6 -4
  2. aprsd/cli_helper.py +151 -0
  3. aprsd/client/__init__.py +13 -0
  4. aprsd/client/aprsis.py +132 -0
  5. aprsd/client/base.py +105 -0
  6. aprsd/client/drivers/__init__.py +0 -0
  7. aprsd/client/drivers/aprsis.py +228 -0
  8. aprsd/client/drivers/fake.py +73 -0
  9. aprsd/client/drivers/kiss.py +119 -0
  10. aprsd/client/factory.py +88 -0
  11. aprsd/client/fake.py +48 -0
  12. aprsd/client/kiss.py +103 -0
  13. aprsd/client/stats.py +38 -0
  14. aprsd/cmds/__init__.py +0 -0
  15. aprsd/cmds/completion.py +22 -0
  16. aprsd/cmds/dev.py +162 -0
  17. aprsd/cmds/fetch_stats.py +156 -0
  18. aprsd/cmds/healthcheck.py +86 -0
  19. aprsd/cmds/list_plugins.py +319 -0
  20. aprsd/cmds/listen.py +231 -0
  21. aprsd/cmds/send_message.py +171 -0
  22. aprsd/cmds/server.py +137 -0
  23. aprsd/cmds/webchat.py +674 -0
  24. aprsd/conf/__init__.py +56 -0
  25. aprsd/conf/client.py +131 -0
  26. aprsd/conf/common.py +301 -0
  27. aprsd/conf/log.py +65 -0
  28. aprsd/conf/opts.py +80 -0
  29. aprsd/conf/plugin_common.py +182 -0
  30. aprsd/conf/plugin_email.py +105 -0
  31. aprsd/exception.py +13 -0
  32. aprsd/log/__init__.py +0 -0
  33. aprsd/log/log.py +138 -0
  34. aprsd/main.py +104 -867
  35. aprsd/packets/__init__.py +20 -0
  36. aprsd/packets/collector.py +79 -0
  37. aprsd/packets/core.py +823 -0
  38. aprsd/packets/log.py +161 -0
  39. aprsd/packets/packet_list.py +110 -0
  40. aprsd/packets/seen_list.py +49 -0
  41. aprsd/packets/tracker.py +103 -0
  42. aprsd/packets/watch_list.py +119 -0
  43. aprsd/plugin.py +474 -284
  44. aprsd/plugin_utils.py +86 -0
  45. aprsd/plugins/__init__.py +0 -0
  46. aprsd/plugins/email.py +709 -0
  47. aprsd/plugins/fortune.py +61 -0
  48. aprsd/plugins/location.py +179 -0
  49. aprsd/plugins/notify.py +61 -0
  50. aprsd/plugins/ping.py +31 -0
  51. aprsd/plugins/time.py +115 -0
  52. aprsd/plugins/version.py +31 -0
  53. aprsd/plugins/weather.py +405 -0
  54. aprsd/stats/__init__.py +20 -0
  55. aprsd/stats/app.py +49 -0
  56. aprsd/stats/collector.py +37 -0
  57. aprsd/threads/__init__.py +11 -0
  58. aprsd/threads/aprsd.py +119 -0
  59. aprsd/threads/keep_alive.py +131 -0
  60. aprsd/threads/log_monitor.py +121 -0
  61. aprsd/threads/registry.py +56 -0
  62. aprsd/threads/rx.py +354 -0
  63. aprsd/threads/stats.py +44 -0
  64. aprsd/threads/tx.py +255 -0
  65. aprsd/utils/__init__.py +218 -0
  66. aprsd/utils/counter.py +51 -0
  67. aprsd/utils/json.py +80 -0
  68. aprsd/utils/objectstore.py +123 -0
  69. aprsd/utils/ring_buffer.py +40 -0
  70. aprsd/utils/trace.py +180 -0
  71. aprsd/web/__init__.py +0 -0
  72. aprsd/web/admin/__init__.py +0 -0
  73. aprsd/web/admin/static/css/index.css +84 -0
  74. aprsd/web/admin/static/css/prism.css +4 -0
  75. aprsd/web/admin/static/css/tabs.css +35 -0
  76. aprsd/web/admin/static/images/Untitled.png +0 -0
  77. aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
  78. aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
  79. aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
  80. aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
  81. aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
  82. aprsd/web/admin/static/js/charts.js +235 -0
  83. aprsd/web/admin/static/js/echarts.js +465 -0
  84. aprsd/web/admin/static/js/logs.js +26 -0
  85. aprsd/web/admin/static/js/main.js +231 -0
  86. aprsd/web/admin/static/js/prism.js +12 -0
  87. aprsd/web/admin/static/js/send-message.js +114 -0
  88. aprsd/web/admin/static/js/tabs.js +28 -0
  89. aprsd/web/admin/templates/index.html +196 -0
  90. aprsd/web/chat/static/css/chat.css +115 -0
  91. aprsd/web/chat/static/css/index.css +66 -0
  92. aprsd/web/chat/static/css/style.css.map +1 -0
  93. aprsd/web/chat/static/css/tabs.css +41 -0
  94. aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
  95. aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
  96. aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
  97. aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
  98. aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
  99. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
  100. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
  101. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
  102. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
  103. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
  104. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
  105. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
  106. aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
  107. aprsd/web/chat/static/images/Untitled.png +0 -0
  108. aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
  109. aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
  110. aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
  111. aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
  112. aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
  113. aprsd/web/chat/static/images/globe.svg +3 -0
  114. aprsd/web/chat/static/js/gps.js +84 -0
  115. aprsd/web/chat/static/js/main.js +45 -0
  116. aprsd/web/chat/static/js/send-message.js +585 -0
  117. aprsd/web/chat/static/js/tabs.js +28 -0
  118. aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
  119. aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
  120. aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
  121. aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
  122. aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
  123. aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
  124. aprsd/web/chat/templates/index.html +139 -0
  125. aprsd/wsgi.py +315 -0
  126. aprsd-3.4.2.dist-info/AUTHORS +13 -0
  127. aprsd-3.4.2.dist-info/LICENSE +175 -0
  128. aprsd-3.4.2.dist-info/METADATA +793 -0
  129. aprsd-3.4.2.dist-info/RECORD +133 -0
  130. {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/WHEEL +1 -1
  131. aprsd-3.4.2.dist-info/entry_points.txt +8 -0
  132. aprsd/fake_aprs.py +0 -83
  133. aprsd/utils.py +0 -166
  134. aprsd-1.0.0.dist-info/AUTHORS +0 -6
  135. aprsd-1.0.0.dist-info/METADATA +0 -181
  136. aprsd-1.0.0.dist-info/RECORD +0 -13
  137. aprsd-1.0.0.dist-info/entry_points.txt +0 -4
  138. aprsd-1.0.0.dist-info/pbr.json +0 -1
  139. /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
  140. {aprsd-1.0.0.dist-info → aprsd-3.4.2.dist-info}/top_level.txt +0 -0
aprsd/packets/log.py ADDED
@@ -0,0 +1,161 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from geopy.distance import geodesic
5
+ from loguru import logger
6
+ from oslo_config import cfg
7
+
8
+ from aprsd import utils
9
+ from aprsd.packets.core import AckPacket, GPSPacket, RejectPacket
10
+
11
+
12
+ LOG = logging.getLogger()
13
+ LOGU = logger
14
+ CONF = cfg.CONF
15
+
16
+ FROM_COLOR = "fg #C70039"
17
+ TO_COLOR = "fg #D033FF"
18
+ TX_COLOR = "red"
19
+ RX_COLOR = "green"
20
+ PACKET_COLOR = "cyan"
21
+ DISTANCE_COLOR = "fg #FF5733"
22
+ DEGREES_COLOR = "fg #FFA900"
23
+
24
+
25
+ def log_multiline(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
26
+ """LOG a packet to the logfile."""
27
+ if not CONF.enable_packet_logging:
28
+ return
29
+ if CONF.log_packet_format == "compact":
30
+ return
31
+
32
+ # asdict(packet)
33
+ logit = ["\n"]
34
+ name = packet.__class__.__name__
35
+
36
+ if isinstance(packet, AckPacket):
37
+ pkt_max_send_count = CONF.default_ack_send_count
38
+ else:
39
+ pkt_max_send_count = CONF.default_packet_send_count
40
+
41
+ if header:
42
+ if tx:
43
+ header_str = f"<{TX_COLOR}>TX</{TX_COLOR}>"
44
+ logit.append(
45
+ f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}> "
46
+ f"TX:{packet.send_count + 1} of {pkt_max_send_count}",
47
+ )
48
+ else:
49
+ header_str = f"<{RX_COLOR}>RX</{RX_COLOR}>"
50
+ logit.append(
51
+ f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)",
52
+ )
53
+
54
+ else:
55
+ header_str = ""
56
+ logit.append(f"__________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
57
+ # log_list.append(f" Packet : {packet.__class__.__name__}")
58
+ if packet.msgNo:
59
+ logit.append(f" Msg # : {packet.msgNo}")
60
+ if packet.from_call:
61
+ logit.append(f" From : <{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}>")
62
+ if packet.to_call:
63
+ logit.append(f" To : <{TO_COLOR}>{packet.to_call}</{TO_COLOR}>")
64
+ if hasattr(packet, "path") and packet.path:
65
+ logit.append(f" Path : {'=>'.join(packet.path)}")
66
+ if hasattr(packet, "via") and packet.via:
67
+ logit.append(f" VIA : {packet.via}")
68
+
69
+ if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
70
+ msg = packet.human_info
71
+
72
+ if msg:
73
+ msg = msg.replace("<", "\\<")
74
+ logit.append(f" Info : <light-yellow><b>{msg}</b></light-yellow>")
75
+
76
+ if hasattr(packet, "comment") and packet.comment:
77
+ logit.append(f" Comment : {packet.comment}")
78
+
79
+ raw = packet.raw.replace("<", "\\<")
80
+ logit.append(f" Raw : <fg #828282>{raw}</fg #828282>")
81
+ logit.append(f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
82
+
83
+ LOGU.opt(colors=True).info("\n".join(logit))
84
+ LOG.debug(repr(packet))
85
+
86
+
87
+ def log(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
88
+ if not CONF.enable_packet_logging:
89
+ return
90
+ if CONF.log_packet_format == "multiline":
91
+ log_multiline(packet, tx, header)
92
+ return
93
+
94
+ logit = []
95
+ name = packet.__class__.__name__
96
+ if isinstance(packet, AckPacket):
97
+ pkt_max_send_count = CONF.default_ack_send_count
98
+ else:
99
+ pkt_max_send_count = CONF.default_packet_send_count
100
+
101
+ if header:
102
+ if tx:
103
+ via_color = "red"
104
+ arrow = f"<{via_color}>\u2192</{via_color}>"
105
+ logit.append(
106
+ f"<red>TX\u2191</red> "
107
+ f"<cyan>{name}</cyan>"
108
+ f":{packet.msgNo}"
109
+ f" ({packet.send_count + 1} of {pkt_max_send_count})",
110
+ )
111
+ else:
112
+ via_color = "fg #1AA730"
113
+ arrow = f"<{via_color}>\u2192</{via_color}>"
114
+ f"<{via_color}><-</{via_color}>"
115
+ logit.append(
116
+ f"<fg #1AA730>RX\u2193</fg #1AA730> "
117
+ f"<cyan>{name}</cyan>"
118
+ f":{packet.msgNo}",
119
+ )
120
+ else:
121
+ via_color = "green"
122
+ arrow = f"<{via_color}>-></{via_color}>"
123
+ logit.append(
124
+ f"<cyan>{name}</cyan>"
125
+ f":{packet.msgNo}",
126
+ )
127
+
128
+ tmp = None
129
+ if packet.path:
130
+ tmp = f"{arrow}".join(packet.path) + f"{arrow} "
131
+
132
+ logit.append(
133
+ f"<{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}> {arrow}"
134
+ f"{tmp if tmp else ' '}"
135
+ f"<{TO_COLOR}>{packet.to_call}</{TO_COLOR}>",
136
+ )
137
+
138
+ if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
139
+ logit.append(":")
140
+ msg = packet.human_info
141
+
142
+ if msg:
143
+ msg = msg.replace("<", "\\<")
144
+ logit.append(f"<light-yellow><b>{msg}</b></light-yellow>")
145
+
146
+ # is there distance information?
147
+ if isinstance(packet, GPSPacket) and CONF.latitude and CONF.longitude:
148
+ my_coords = (CONF.latitude, CONF.longitude)
149
+ packet_coords = (packet.latitude, packet.longitude)
150
+ try:
151
+ bearing = utils.calculate_initial_compass_bearing(my_coords, packet_coords)
152
+ except Exception as e:
153
+ LOG.error(f"Failed to calculate bearing: {e}")
154
+ bearing = 0
155
+ logit.append(
156
+ f" : <{DEGREES_COLOR}>{utils.degrees_to_cardinal(bearing, full_string=True)}</{DEGREES_COLOR}>"
157
+ f"<{DISTANCE_COLOR}>@{geodesic(my_coords, packet_coords).miles:.2f}miles</{DISTANCE_COLOR}>",
158
+ )
159
+
160
+ LOGU.opt(colors=True).info(" ".join(logit))
161
+ log_multiline(packet, tx, header)
@@ -0,0 +1,110 @@
1
+ from collections import OrderedDict
2
+ import logging
3
+
4
+ from oslo_config import cfg
5
+
6
+ from aprsd.packets import core
7
+ from aprsd.utils import objectstore
8
+
9
+
10
+ CONF = cfg.CONF
11
+ LOG = logging.getLogger("APRSD")
12
+
13
+
14
+ class PacketList(objectstore.ObjectStoreMixin):
15
+ """Class to keep track of the packets we tx/rx."""
16
+ _instance = None
17
+ _total_rx: int = 0
18
+ _total_tx: int = 0
19
+ maxlen: int = 100
20
+
21
+ def __new__(cls, *args, **kwargs):
22
+ if cls._instance is None:
23
+ cls._instance = super().__new__(cls)
24
+ cls._instance.maxlen = CONF.packet_list_maxlen
25
+ cls._instance._init_data()
26
+ return cls._instance
27
+
28
+ def _init_data(self):
29
+ self.data = {
30
+ "types": {},
31
+ "packets": OrderedDict(),
32
+ }
33
+
34
+ def rx(self, packet: type[core.Packet]):
35
+ """Add a packet that was received."""
36
+ with self.lock:
37
+ self._total_rx += 1
38
+ self._add(packet)
39
+ ptype = packet.__class__.__name__
40
+ if ptype not in self.data["types"]:
41
+ self.data["types"][ptype] = {"tx": 0, "rx": 0}
42
+ self.data["types"][ptype]["rx"] += 1
43
+
44
+ def tx(self, packet: type[core.Packet]):
45
+ """Add a packet that was received."""
46
+ with self.lock:
47
+ self._total_tx += 1
48
+ self._add(packet)
49
+ ptype = packet.__class__.__name__
50
+ if ptype not in self.data["types"]:
51
+ self.data["types"][ptype] = {"tx": 0, "rx": 0}
52
+ self.data["types"][ptype]["tx"] += 1
53
+
54
+ def add(self, packet):
55
+ with self.lock:
56
+ self._add(packet)
57
+
58
+ def _add(self, packet):
59
+ if not self.data.get("packets"):
60
+ self._init_data()
61
+ if packet.key in self.data["packets"]:
62
+ self.data["packets"].move_to_end(packet.key)
63
+ elif len(self.data["packets"]) == self.maxlen:
64
+ self.data["packets"].popitem(last=False)
65
+ self.data["packets"][packet.key] = packet
66
+
67
+ def find(self, packet):
68
+ with self.lock:
69
+ return self.data["packets"][packet.key]
70
+
71
+ def __len__(self):
72
+ with self.lock:
73
+ return len(self.data["packets"])
74
+
75
+ def total_rx(self):
76
+ with self.lock:
77
+ return self._total_rx
78
+
79
+ def total_tx(self):
80
+ with self.lock:
81
+ return self._total_tx
82
+
83
+ def stats(self, serializable=False) -> dict:
84
+ # limit the number of packets to return to 50
85
+ with self.lock:
86
+ tmp = OrderedDict(
87
+ reversed(
88
+ list(
89
+ self.data.get("packets", OrderedDict()).items(),
90
+ ),
91
+ ),
92
+ )
93
+ pkts = []
94
+ count = 1
95
+ for packet in tmp:
96
+ pkts.append(tmp[packet])
97
+ count += 1
98
+ if count > CONF.packet_list_stats_maxlen:
99
+ break
100
+
101
+ stats = {
102
+ "total_tracked": self._total_rx + self._total_rx,
103
+ "rx": self._total_rx,
104
+ "tx": self._total_tx,
105
+ "types": self.data.get("types", []),
106
+ "packet_count": len(self.data.get("packets", [])),
107
+ "maxlen": self.maxlen,
108
+ "packets": pkts,
109
+ }
110
+ return stats
@@ -0,0 +1,49 @@
1
+ import datetime
2
+ import logging
3
+
4
+ from oslo_config import cfg
5
+
6
+ from aprsd.packets import core
7
+ from aprsd.utils import objectstore
8
+
9
+
10
+ CONF = cfg.CONF
11
+ LOG = logging.getLogger("APRSD")
12
+
13
+
14
+ class SeenList(objectstore.ObjectStoreMixin):
15
+ """Global callsign seen list."""
16
+
17
+ _instance = None
18
+ data: dict = {}
19
+
20
+ def __new__(cls, *args, **kwargs):
21
+ if cls._instance is None:
22
+ cls._instance = super().__new__(cls)
23
+ cls._instance.data = {}
24
+ return cls._instance
25
+
26
+ def stats(self, serializable=False):
27
+ """Return the stats for the PacketTrack class."""
28
+ with self.lock:
29
+ return self.data
30
+
31
+ def rx(self, packet: type[core.Packet]):
32
+ """When we get a packet from the network, update the seen list."""
33
+ with self.lock:
34
+ callsign = None
35
+ if packet.from_call:
36
+ callsign = packet.from_call
37
+ else:
38
+ LOG.warning(f"Can't find FROM in packet {packet}")
39
+ return
40
+ if callsign not in self.data:
41
+ self.data[callsign] = {
42
+ "last": None,
43
+ "count": 0,
44
+ }
45
+ self.data[callsign]["last"] = datetime.datetime.now()
46
+ self.data[callsign]["count"] += 1
47
+
48
+ def tx(self, packet: type[core.Packet]):
49
+ """We don't care about TX packets."""
@@ -0,0 +1,103 @@
1
+ import datetime
2
+ import logging
3
+
4
+ from oslo_config import cfg
5
+
6
+ from aprsd.packets import core
7
+ from aprsd.utils import objectstore
8
+
9
+
10
+ CONF = cfg.CONF
11
+ LOG = logging.getLogger("APRSD")
12
+
13
+
14
+ class PacketTrack(objectstore.ObjectStoreMixin):
15
+ """Class to keep track of outstanding text messages.
16
+
17
+ This is a thread safe class that keeps track of active
18
+ messages.
19
+
20
+ When a message is asked to be sent, it is placed into this
21
+ class via it's id. The TextMessage class's send() method
22
+ automatically adds itself to this class. When the ack is
23
+ recieved from the radio, the message object is removed from
24
+ this class.
25
+ """
26
+
27
+ _instance = None
28
+ _start_time = None
29
+
30
+ data: dict = {}
31
+ total_tracked: int = 0
32
+
33
+ def __new__(cls, *args, **kwargs):
34
+ if cls._instance is None:
35
+ cls._instance = super().__new__(cls)
36
+ cls._instance._start_time = datetime.datetime.now()
37
+ cls._instance._init_store()
38
+ return cls._instance
39
+
40
+ def __getitem__(self, name):
41
+ with self.lock:
42
+ return self.data[name]
43
+
44
+ def __iter__(self):
45
+ with self.lock:
46
+ return iter(self.data)
47
+
48
+ def keys(self):
49
+ with self.lock:
50
+ return self.data.keys()
51
+
52
+ def items(self):
53
+ with self.lock:
54
+ return self.data.items()
55
+
56
+ def values(self):
57
+ with self.lock:
58
+ return self.data.values()
59
+
60
+ def stats(self, serializable=False):
61
+ with self.lock:
62
+ stats = {
63
+ "total_tracked": self.total_tracked,
64
+ }
65
+ pkts = {}
66
+ for key in self.data:
67
+ last_send_time = self.data[key].last_send_time
68
+ pkts[key] = {
69
+ "last_send_time": last_send_time,
70
+ "send_count": self.data[key].send_count,
71
+ "retry_count": self.data[key].retry_count,
72
+ "message": self.data[key].raw,
73
+ }
74
+ stats["packets"] = pkts
75
+ return stats
76
+
77
+ def rx(self, packet: type[core.Packet]) -> None:
78
+ """When we get a packet from the network, check if we should remove it."""
79
+ if isinstance(packet, core.AckPacket):
80
+ self._remove(packet.msgNo)
81
+ elif isinstance(packet, core.RejectPacket):
82
+ self._remove(packet.msgNo)
83
+ elif hasattr(packet, "ackMsgNo"):
84
+ # Got a piggyback ack, so remove the original message
85
+ self._remove(packet.ackMsgNo)
86
+
87
+ def tx(self, packet: type[core.Packet]) -> None:
88
+ """Add a packet that was sent."""
89
+ with self.lock:
90
+ key = packet.msgNo
91
+ packet.send_count = 0
92
+ self.data[key] = packet
93
+ self.total_tracked += 1
94
+
95
+ def remove(self, key):
96
+ self._remove(key)
97
+
98
+ def _remove(self, key):
99
+ with self.lock:
100
+ try:
101
+ del self.data[key]
102
+ except KeyError:
103
+ pass
@@ -0,0 +1,119 @@
1
+ import datetime
2
+ import logging
3
+
4
+ from oslo_config import cfg
5
+
6
+ from aprsd import utils
7
+ from aprsd.packets import core
8
+ from aprsd.utils import objectstore
9
+
10
+
11
+ CONF = cfg.CONF
12
+ LOG = logging.getLogger("APRSD")
13
+
14
+
15
+ class WatchList(objectstore.ObjectStoreMixin):
16
+ """Global watch list and info for callsigns."""
17
+
18
+ _instance = None
19
+ data = {}
20
+
21
+ def __new__(cls, *args, **kwargs):
22
+ if cls._instance is None:
23
+ cls._instance = super().__new__(cls)
24
+ return cls._instance
25
+
26
+ def __init__(self):
27
+ super().__init__()
28
+ self._update_from_conf()
29
+
30
+ def _update_from_conf(self, config=None):
31
+ with self.lock:
32
+ if CONF.watch_list.enabled and CONF.watch_list.callsigns:
33
+ for callsign in CONF.watch_list.callsigns:
34
+ call = callsign.replace("*", "")
35
+ # FIXME(waboring) - we should fetch the last time we saw
36
+ # a beacon from a callsign or some other mechanism to find
37
+ # last time a message was seen by aprs-is. For now this
38
+ # is all we can do.
39
+ if call not in self.data:
40
+ self.data[call] = {
41
+ "last": None,
42
+ "packet": None,
43
+ }
44
+
45
+ def stats(self, serializable=False) -> dict:
46
+ stats = {}
47
+ with self.lock:
48
+ for callsign in self.data:
49
+ stats[callsign] = {
50
+ "last": self.data[callsign]["last"],
51
+ "packet": self.data[callsign]["packet"],
52
+ "age": self.age(callsign),
53
+ "old": self.is_old(callsign),
54
+ }
55
+ return stats
56
+
57
+ def is_enabled(self):
58
+ return CONF.watch_list.enabled
59
+
60
+ def callsign_in_watchlist(self, callsign):
61
+ with self.lock:
62
+ return callsign in self.data
63
+
64
+ def rx(self, packet: type[core.Packet]) -> None:
65
+ """Track when we got a packet from the network."""
66
+ callsign = packet.from_call
67
+
68
+ if self.callsign_in_watchlist(callsign):
69
+ with self.lock:
70
+ self.data[callsign]["last"] = datetime.datetime.now()
71
+ self.data[callsign]["packet"] = packet
72
+
73
+ def tx(self, packet: type[core.Packet]) -> None:
74
+ """We don't care about TX packets."""
75
+
76
+ def last_seen(self, callsign):
77
+ with self.lock:
78
+ if self.callsign_in_watchlist(callsign):
79
+ return self.data[callsign]["last"]
80
+
81
+ def age(self, callsign):
82
+ now = datetime.datetime.now()
83
+ last_seen_time = self.last_seen(callsign)
84
+ if last_seen_time:
85
+ return str(now - last_seen_time)
86
+ else:
87
+ return None
88
+
89
+ def max_delta(self, seconds=None):
90
+ if not seconds:
91
+ seconds = CONF.watch_list.alert_time_seconds
92
+ max_timeout = {"seconds": seconds}
93
+ return datetime.timedelta(**max_timeout)
94
+
95
+ def is_old(self, callsign, seconds=None):
96
+ """Watch list callsign last seen is old compared to now?
97
+
98
+ This tests to see if the last time we saw a callsign packet,
99
+ if that is older than the allowed timeout in the config.
100
+
101
+ We put this here so any notification plugin can use this
102
+ same test.
103
+ """
104
+ if not self.callsign_in_watchlist(callsign):
105
+ return False
106
+
107
+ age = self.age(callsign)
108
+ if age:
109
+ delta = utils.parse_delta_str(age)
110
+ d = datetime.timedelta(**delta)
111
+
112
+ max_delta = self.max_delta(seconds=seconds)
113
+
114
+ if d > max_delta:
115
+ return True
116
+ else:
117
+ return False
118
+ else:
119
+ return False