aprsd 1.0.0__py3-none-any.whl → 3.4.1__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.
- aprsd/__init__.py +6 -4
- aprsd/cli_helper.py +151 -0
- aprsd/client/__init__.py +13 -0
- aprsd/client/aprsis.py +132 -0
- aprsd/client/base.py +105 -0
- aprsd/client/drivers/__init__.py +0 -0
- aprsd/client/drivers/aprsis.py +224 -0
- aprsd/client/drivers/fake.py +73 -0
- aprsd/client/drivers/kiss.py +119 -0
- aprsd/client/factory.py +88 -0
- aprsd/client/fake.py +48 -0
- aprsd/client/kiss.py +103 -0
- aprsd/client/stats.py +38 -0
- aprsd/cmds/__init__.py +0 -0
- aprsd/cmds/completion.py +22 -0
- aprsd/cmds/dev.py +162 -0
- aprsd/cmds/fetch_stats.py +156 -0
- aprsd/cmds/healthcheck.py +86 -0
- aprsd/cmds/list_plugins.py +319 -0
- aprsd/cmds/listen.py +230 -0
- aprsd/cmds/send_message.py +174 -0
- aprsd/cmds/server.py +142 -0
- aprsd/cmds/webchat.py +681 -0
- aprsd/conf/__init__.py +56 -0
- aprsd/conf/client.py +131 -0
- aprsd/conf/common.py +302 -0
- aprsd/conf/log.py +65 -0
- aprsd/conf/opts.py +80 -0
- aprsd/conf/plugin_common.py +191 -0
- aprsd/conf/plugin_email.py +105 -0
- aprsd/exception.py +13 -0
- aprsd/log/__init__.py +0 -0
- aprsd/log/log.py +138 -0
- aprsd/main.py +104 -867
- aprsd/messaging.py +4 -0
- aprsd/packets/__init__.py +12 -0
- aprsd/packets/collector.py +56 -0
- aprsd/packets/core.py +823 -0
- aprsd/packets/log.py +143 -0
- aprsd/packets/packet_list.py +116 -0
- aprsd/packets/seen_list.py +54 -0
- aprsd/packets/tracker.py +109 -0
- aprsd/packets/watch_list.py +122 -0
- aprsd/plugin.py +475 -284
- aprsd/plugin_utils.py +86 -0
- aprsd/plugins/__init__.py +0 -0
- aprsd/plugins/email.py +709 -0
- aprsd/plugins/fortune.py +61 -0
- aprsd/plugins/location.py +179 -0
- aprsd/plugins/notify.py +61 -0
- aprsd/plugins/ping.py +31 -0
- aprsd/plugins/time.py +115 -0
- aprsd/plugins/version.py +31 -0
- aprsd/plugins/weather.py +405 -0
- aprsd/stats/__init__.py +20 -0
- aprsd/stats/app.py +49 -0
- aprsd/stats/collector.py +38 -0
- aprsd/threads/__init__.py +11 -0
- aprsd/threads/aprsd.py +119 -0
- aprsd/threads/keep_alive.py +124 -0
- aprsd/threads/log_monitor.py +121 -0
- aprsd/threads/registry.py +56 -0
- aprsd/threads/rx.py +354 -0
- aprsd/threads/stats.py +44 -0
- aprsd/threads/tx.py +255 -0
- aprsd/utils/__init__.py +163 -0
- aprsd/utils/counter.py +51 -0
- aprsd/utils/json.py +80 -0
- aprsd/utils/objectstore.py +123 -0
- aprsd/utils/ring_buffer.py +40 -0
- aprsd/utils/trace.py +180 -0
- aprsd/web/__init__.py +0 -0
- aprsd/web/admin/__init__.py +0 -0
- aprsd/web/admin/static/css/index.css +84 -0
- aprsd/web/admin/static/css/prism.css +4 -0
- aprsd/web/admin/static/css/tabs.css +35 -0
- aprsd/web/admin/static/images/Untitled.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/admin/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/admin/static/js/charts.js +235 -0
- aprsd/web/admin/static/js/echarts.js +465 -0
- aprsd/web/admin/static/js/logs.js +26 -0
- aprsd/web/admin/static/js/main.js +231 -0
- aprsd/web/admin/static/js/prism.js +12 -0
- aprsd/web/admin/static/js/send-message.js +114 -0
- aprsd/web/admin/static/js/tabs.js +28 -0
- aprsd/web/admin/templates/index.html +196 -0
- aprsd/web/chat/static/css/chat.css +115 -0
- aprsd/web/chat/static/css/index.css +66 -0
- aprsd/web/chat/static/css/style.css.map +1 -0
- aprsd/web/chat/static/css/tabs.css +41 -0
- aprsd/web/chat/static/css/upstream/bootstrap.min.css +6 -0
- aprsd/web/chat/static/css/upstream/font.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/google-fonts.css +23 -0
- aprsd/web/chat/static/css/upstream/jquery-ui.css +1311 -0
- aprsd/web/chat/static/css/upstream/jquery.toast.css +28 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Bold.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/LatoLatin-Regular.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/icons.woff2 +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff +0 -0
- aprsd/web/chat/static/css/upstream/themes/default/assets/fonts/outline-icons.woff2 +0 -0
- aprsd/web/chat/static/images/Untitled.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-16-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-0.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-1.png +0 -0
- aprsd/web/chat/static/images/aprs-symbols-64-2.png +0 -0
- aprsd/web/chat/static/images/globe.svg +3 -0
- aprsd/web/chat/static/js/gps.js +84 -0
- aprsd/web/chat/static/js/main.js +45 -0
- aprsd/web/chat/static/js/send-message.js +585 -0
- aprsd/web/chat/static/js/tabs.js +28 -0
- aprsd/web/chat/static/js/upstream/bootstrap.bundle.min.js +7 -0
- aprsd/web/chat/static/js/upstream/jquery-3.7.1.min.js +2 -0
- aprsd/web/chat/static/js/upstream/jquery-ui.min.js +13 -0
- aprsd/web/chat/static/js/upstream/jquery.toast.js +374 -0
- aprsd/web/chat/static/js/upstream/semantic.min.js +11 -0
- aprsd/web/chat/static/js/upstream/socket.io.min.js +7 -0
- aprsd/web/chat/templates/index.html +139 -0
- aprsd/wsgi.py +315 -0
- aprsd-3.4.1.dist-info/AUTHORS +13 -0
- aprsd-3.4.1.dist-info/LICENSE +175 -0
- aprsd-3.4.1.dist-info/METADATA +799 -0
- aprsd-3.4.1.dist-info/RECORD +134 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/WHEEL +1 -1
- aprsd-3.4.1.dist-info/entry_points.txt +8 -0
- aprsd/fake_aprs.py +0 -83
- aprsd/utils.py +0 -166
- aprsd-1.0.0.dist-info/AUTHORS +0 -6
- aprsd-1.0.0.dist-info/METADATA +0 -181
- aprsd-1.0.0.dist-info/RECORD +0 -13
- aprsd-1.0.0.dist-info/entry_points.txt +0 -4
- aprsd-1.0.0.dist-info/pbr.json +0 -1
- /aprsd/{fuzzyclock.py → utils/fuzzyclock.py} +0 -0
- {aprsd-1.0.0.dist-info → aprsd-3.4.1.dist-info}/top_level.txt +0 -0
aprsd/plugins/weather.py
ADDED
@@ -0,0 +1,405 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import re
|
4
|
+
|
5
|
+
from oslo_config import cfg
|
6
|
+
import requests
|
7
|
+
|
8
|
+
from aprsd import plugin, plugin_utils
|
9
|
+
from aprsd.utils import trace
|
10
|
+
|
11
|
+
|
12
|
+
CONF = cfg.CONF
|
13
|
+
LOG = logging.getLogger("APRSD")
|
14
|
+
|
15
|
+
|
16
|
+
class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
17
|
+
"""USWeather Command
|
18
|
+
|
19
|
+
Returns a weather report for the calling weather station
|
20
|
+
inside the United States only. This uses the
|
21
|
+
forecast.weather.gov API to fetch the weather.
|
22
|
+
|
23
|
+
This service does not require an apiKey.
|
24
|
+
|
25
|
+
How to Call: Send a message to aprsd
|
26
|
+
"weather" - returns weather near the calling callsign
|
27
|
+
"""
|
28
|
+
|
29
|
+
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
30
|
+
command_regex = r"^[wW]"
|
31
|
+
|
32
|
+
command_name = "USWeather"
|
33
|
+
short_description = "Provide USA only weather of GPS Beacon location"
|
34
|
+
|
35
|
+
def setup(self):
|
36
|
+
self.ensure_aprs_fi_key()
|
37
|
+
|
38
|
+
@trace.trace
|
39
|
+
def process(self, packet):
|
40
|
+
LOG.info("Weather Plugin")
|
41
|
+
fromcall = packet.from_call
|
42
|
+
message = packet.get("message_text", None)
|
43
|
+
# message = packet.get("message_text", None)
|
44
|
+
# ack = packet.get("msgNo", "0")
|
45
|
+
a = re.search(r"^.*\s+(.*)", message)
|
46
|
+
if a is not None:
|
47
|
+
searchcall = a.group(1)
|
48
|
+
searchcall = searchcall.upper()
|
49
|
+
else:
|
50
|
+
searchcall = fromcall
|
51
|
+
api_key = CONF.aprs_fi.apiKey
|
52
|
+
try:
|
53
|
+
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
54
|
+
except Exception as ex:
|
55
|
+
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
56
|
+
return "Failed to fetch aprs.fi location"
|
57
|
+
|
58
|
+
LOG.debug(f"LocationPlugin: aprs_data = {aprs_data}")
|
59
|
+
if not len(aprs_data["entries"]):
|
60
|
+
LOG.error("Didn't get any entries from aprs.fi")
|
61
|
+
return "Failed to fetch aprs.fi location"
|
62
|
+
|
63
|
+
lat = aprs_data["entries"][0]["lat"]
|
64
|
+
lon = aprs_data["entries"][0]["lng"]
|
65
|
+
|
66
|
+
try:
|
67
|
+
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
68
|
+
except Exception as ex:
|
69
|
+
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
|
70
|
+
return "Unable to get weather"
|
71
|
+
|
72
|
+
LOG.info(f"WX data {wx_data}")
|
73
|
+
|
74
|
+
reply = (
|
75
|
+
"%sF(%sF/%sF) %s. %s, %s."
|
76
|
+
% (
|
77
|
+
wx_data["currentobservation"]["Temp"],
|
78
|
+
wx_data["data"]["temperature"][0],
|
79
|
+
wx_data["data"]["temperature"][1],
|
80
|
+
wx_data["data"]["weather"][0],
|
81
|
+
wx_data["time"]["startPeriodName"][1],
|
82
|
+
wx_data["data"]["weather"][1],
|
83
|
+
)
|
84
|
+
).rstrip()
|
85
|
+
LOG.debug(f"reply: '{reply}' ")
|
86
|
+
return reply
|
87
|
+
|
88
|
+
|
89
|
+
class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
90
|
+
"""METAR Command
|
91
|
+
|
92
|
+
This provides a METAR weather report from a station near the caller
|
93
|
+
or callsign using the forecast.weather.gov api. This only works
|
94
|
+
for stations inside the United States.
|
95
|
+
|
96
|
+
This service does not require an apiKey.
|
97
|
+
|
98
|
+
How to Call: Send a message to aprsd
|
99
|
+
"metar" - returns metar report near the calling callsign
|
100
|
+
"metar CALLSIGN" - returns metar report near CALLSIGN
|
101
|
+
|
102
|
+
"""
|
103
|
+
|
104
|
+
command_regex = r"^([m]|[M]|[m]\s|metar)"
|
105
|
+
command_name = "USMetar"
|
106
|
+
short_description = "USA only METAR of GPS Beacon location"
|
107
|
+
|
108
|
+
def setup(self):
|
109
|
+
self.ensure_aprs_fi_key()
|
110
|
+
|
111
|
+
@trace.trace
|
112
|
+
def process(self, packet):
|
113
|
+
fromcall = packet.get("from")
|
114
|
+
message = packet.get("message_text", None)
|
115
|
+
# ack = packet.get("msgNo", "0")
|
116
|
+
LOG.info(f"WX Plugin '{message}'")
|
117
|
+
a = re.search(r"^.*\s+(.*)", message)
|
118
|
+
if a is not None:
|
119
|
+
searchcall = a.group(1)
|
120
|
+
station = searchcall.upper()
|
121
|
+
try:
|
122
|
+
resp = plugin_utils.get_weather_gov_metar(station)
|
123
|
+
except Exception as e:
|
124
|
+
LOG.debug(f"Weather failed with: {str(e)}")
|
125
|
+
reply = "Unable to find station METAR"
|
126
|
+
else:
|
127
|
+
station_data = json.loads(resp.text)
|
128
|
+
reply = station_data["properties"]["rawMessage"]
|
129
|
+
|
130
|
+
return reply
|
131
|
+
else:
|
132
|
+
# if no second argument, search for calling station
|
133
|
+
fromcall = fromcall
|
134
|
+
|
135
|
+
api_key = CONF.aprs_fi.apiKey
|
136
|
+
|
137
|
+
try:
|
138
|
+
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
139
|
+
except Exception as ex:
|
140
|
+
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
141
|
+
return "Failed to fetch aprs.fi location"
|
142
|
+
|
143
|
+
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
144
|
+
if not len(aprs_data["entries"]):
|
145
|
+
LOG.error("Found no entries from aprs.fi!")
|
146
|
+
return "Failed to fetch aprs.fi location"
|
147
|
+
|
148
|
+
lat = aprs_data["entries"][0]["lat"]
|
149
|
+
lon = aprs_data["entries"][0]["lng"]
|
150
|
+
|
151
|
+
try:
|
152
|
+
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
153
|
+
except Exception as ex:
|
154
|
+
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
|
155
|
+
return "Unable to metar find station."
|
156
|
+
|
157
|
+
if wx_data["location"]["metar"]:
|
158
|
+
station = wx_data["location"]["metar"]
|
159
|
+
try:
|
160
|
+
resp = plugin_utils.get_weather_gov_metar(station)
|
161
|
+
except Exception as e:
|
162
|
+
LOG.debug(f"Weather failed with: {str(e)}")
|
163
|
+
reply = "Failed to get Metar"
|
164
|
+
else:
|
165
|
+
station_data = json.loads(resp.text)
|
166
|
+
reply = station_data["properties"]["rawMessage"]
|
167
|
+
else:
|
168
|
+
# Couldn't find a station
|
169
|
+
reply = "No Metar station found"
|
170
|
+
|
171
|
+
return reply
|
172
|
+
|
173
|
+
|
174
|
+
class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
175
|
+
"""OpenWeatherMap Weather Command
|
176
|
+
|
177
|
+
This provides weather near the caller or callsign.
|
178
|
+
|
179
|
+
How to Call: Send a message to aprsd
|
180
|
+
"weather" - returns the weather near the calling callsign
|
181
|
+
"weather CALLSIGN" - returns the weather near CALLSIGN
|
182
|
+
|
183
|
+
This plugin uses the openweathermap API to fetch
|
184
|
+
location and weather information.
|
185
|
+
|
186
|
+
To use this plugin you need to get an openweathermap
|
187
|
+
account and apikey.
|
188
|
+
|
189
|
+
https://home.openweathermap.org/api_keys
|
190
|
+
|
191
|
+
"""
|
192
|
+
|
193
|
+
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
194
|
+
command_regex = r"^[wW]"
|
195
|
+
|
196
|
+
command_name = "OpenWeatherMap"
|
197
|
+
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
198
|
+
|
199
|
+
def setup(self):
|
200
|
+
if not CONF.owm_weather_plugin.apiKey:
|
201
|
+
LOG.error("Config.owm_weather_plugin.apiKey is not set. Disabling")
|
202
|
+
self.enabled = False
|
203
|
+
else:
|
204
|
+
self.enabled = True
|
205
|
+
|
206
|
+
def help(self):
|
207
|
+
_help = [
|
208
|
+
"openweathermap: Send {} to get weather "
|
209
|
+
"from your location".format(self.command_regex),
|
210
|
+
"openweathermap: Send {} <callsign> to get "
|
211
|
+
"weather from <callsign>".format(self.command_regex),
|
212
|
+
]
|
213
|
+
return _help
|
214
|
+
|
215
|
+
@trace.trace
|
216
|
+
def process(self, packet):
|
217
|
+
fromcall = packet.get("from_call")
|
218
|
+
message = packet.get("message_text", None)
|
219
|
+
# ack = packet.get("msgNo", "0")
|
220
|
+
LOG.info(f"OWMWeather Plugin '{message}'")
|
221
|
+
a = re.search(r"^.*\s+(.*)", message)
|
222
|
+
if a is not None:
|
223
|
+
searchcall = a.group(1)
|
224
|
+
searchcall = searchcall.upper()
|
225
|
+
else:
|
226
|
+
searchcall = fromcall
|
227
|
+
|
228
|
+
api_key = CONF.aprs_fi.apiKey
|
229
|
+
|
230
|
+
try:
|
231
|
+
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
232
|
+
except Exception as ex:
|
233
|
+
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
234
|
+
return "Failed to fetch location"
|
235
|
+
|
236
|
+
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
237
|
+
if not len(aprs_data["entries"]):
|
238
|
+
LOG.error("Found no entries from aprs.fi!")
|
239
|
+
return "Failed to fetch location"
|
240
|
+
|
241
|
+
lat = aprs_data["entries"][0]["lat"]
|
242
|
+
lon = aprs_data["entries"][0]["lng"]
|
243
|
+
|
244
|
+
units = CONF.units
|
245
|
+
api_key = CONF.owm_weather_plugin.apiKey
|
246
|
+
try:
|
247
|
+
wx_data = plugin_utils.fetch_openweathermap(
|
248
|
+
api_key,
|
249
|
+
lat,
|
250
|
+
lon,
|
251
|
+
units=units,
|
252
|
+
exclude="minutely,hourly",
|
253
|
+
)
|
254
|
+
except Exception as ex:
|
255
|
+
LOG.error(f"Couldn't fetch openweathermap api '{ex}'")
|
256
|
+
# default to UTC
|
257
|
+
return "Unable to get weather"
|
258
|
+
|
259
|
+
if units == "metric":
|
260
|
+
degree = "C"
|
261
|
+
else:
|
262
|
+
degree = "F"
|
263
|
+
|
264
|
+
if "wind_gust" in wx_data["current"]:
|
265
|
+
wind = "{:.0f}@{}G{:.0f}".format(
|
266
|
+
wx_data["current"]["wind_speed"],
|
267
|
+
wx_data["current"]["wind_deg"],
|
268
|
+
wx_data["current"]["wind_gust"],
|
269
|
+
)
|
270
|
+
else:
|
271
|
+
wind = "{:.0f}@{}".format(
|
272
|
+
wx_data["current"]["wind_speed"],
|
273
|
+
wx_data["current"]["wind_deg"],
|
274
|
+
)
|
275
|
+
|
276
|
+
# LOG.debug(wx_data["current"])
|
277
|
+
# LOG.debug(wx_data["daily"])
|
278
|
+
reply = "{} {:.1f}{}/{:.1f}{} Wind {} {}%".format(
|
279
|
+
wx_data["current"]["weather"][0]["description"],
|
280
|
+
wx_data["current"]["temp"],
|
281
|
+
degree,
|
282
|
+
wx_data["current"]["dew_point"],
|
283
|
+
degree,
|
284
|
+
wind,
|
285
|
+
wx_data["current"]["humidity"],
|
286
|
+
)
|
287
|
+
|
288
|
+
return reply
|
289
|
+
|
290
|
+
|
291
|
+
class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
292
|
+
"""AVWXWeatherMap Weather Command
|
293
|
+
|
294
|
+
Fetches a METAR weather report for the nearest
|
295
|
+
weather station from the callsign
|
296
|
+
Can be called with:
|
297
|
+
metar - fetches metar for caller
|
298
|
+
metar <CALLSIGN> - fetches metar for <CALLSIGN>
|
299
|
+
|
300
|
+
This plugin requires the avwx-api service
|
301
|
+
to provide the metar for a station near
|
302
|
+
the callsign.
|
303
|
+
|
304
|
+
avwx-api is an opensource project that has
|
305
|
+
a hosted service here: https://avwx.rest/
|
306
|
+
|
307
|
+
You can launch your own avwx-api in a container
|
308
|
+
by cloning the githug repo here: https://github.com/avwx-rest/AVWX-API
|
309
|
+
|
310
|
+
Then build the docker container with:
|
311
|
+
docker build -f Dockerfile -t avwx-api:master .
|
312
|
+
"""
|
313
|
+
|
314
|
+
command_regex = r"^([m]|[m]|[m]\s|metar)"
|
315
|
+
command_name = "AVWXWeather"
|
316
|
+
short_description = "AVWX weather of GPS Beacon location"
|
317
|
+
|
318
|
+
def setup(self):
|
319
|
+
if not CONF.avwx_plugin.base_url:
|
320
|
+
LOG.error("Config avwx_plugin.base_url not specified. Disabling")
|
321
|
+
return False
|
322
|
+
elif not CONF.avwx_plugin.apiKey:
|
323
|
+
LOG.error("Config avwx_plugin.apiKey not specified. Disabling")
|
324
|
+
return False
|
325
|
+
else:
|
326
|
+
return True
|
327
|
+
|
328
|
+
def help(self):
|
329
|
+
_help = [
|
330
|
+
"avwxweather: Send {} to get weather "
|
331
|
+
"from your location".format(self.command_regex),
|
332
|
+
"avwxweather: Send {} <callsign> to get "
|
333
|
+
"weather from <callsign>".format(self.command_regex),
|
334
|
+
]
|
335
|
+
return _help
|
336
|
+
|
337
|
+
@trace.trace
|
338
|
+
def process(self, packet):
|
339
|
+
fromcall = packet.get("from")
|
340
|
+
message = packet.get("message_text", None)
|
341
|
+
# ack = packet.get("msgNo", "0")
|
342
|
+
LOG.info(f"AVWXWeather Plugin '{message}'")
|
343
|
+
a = re.search(r"^.*\s+(.*)", message)
|
344
|
+
if a is not None:
|
345
|
+
searchcall = a.group(1)
|
346
|
+
searchcall = searchcall.upper()
|
347
|
+
else:
|
348
|
+
searchcall = fromcall
|
349
|
+
|
350
|
+
api_key = CONF.aprs_fi.apiKey
|
351
|
+
try:
|
352
|
+
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
353
|
+
except Exception as ex:
|
354
|
+
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
355
|
+
return "Failed to fetch location"
|
356
|
+
|
357
|
+
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
358
|
+
if not len(aprs_data["entries"]):
|
359
|
+
LOG.error("Found no entries from aprs.fi!")
|
360
|
+
return "Failed to fetch location"
|
361
|
+
|
362
|
+
lat = aprs_data["entries"][0]["lat"]
|
363
|
+
lon = aprs_data["entries"][0]["lng"]
|
364
|
+
|
365
|
+
api_key = CONF.avwx_plugin.apiKey
|
366
|
+
base_url = CONF.avwx_plugin.base_url
|
367
|
+
token = f"TOKEN {api_key}"
|
368
|
+
headers = {"Authorization": token}
|
369
|
+
try:
|
370
|
+
coord = f"{lat},{lon}"
|
371
|
+
url = (
|
372
|
+
"{}/api/station/near/{}?"
|
373
|
+
"n=1&airport=false&reporting=true&format=json".format(base_url, coord)
|
374
|
+
)
|
375
|
+
|
376
|
+
LOG.debug(f"Get stations near me '{url}'")
|
377
|
+
response = requests.get(url, headers=headers)
|
378
|
+
except Exception as ex:
|
379
|
+
LOG.error(ex)
|
380
|
+
raise Exception(f"Failed to get the weather '{ex}'")
|
381
|
+
else:
|
382
|
+
wx_data = json.loads(response.text)
|
383
|
+
|
384
|
+
# LOG.debug(wx_data)
|
385
|
+
station = wx_data[0]["station"]["icao"]
|
386
|
+
|
387
|
+
try:
|
388
|
+
url = (
|
389
|
+
"{}/api/metar/{}?options=info,translate,summary"
|
390
|
+
"&airport=true&reporting=true&format=json&onfail=cache".format(
|
391
|
+
base_url,
|
392
|
+
station,
|
393
|
+
)
|
394
|
+
)
|
395
|
+
|
396
|
+
LOG.debug(f"Get METAR '{url}'")
|
397
|
+
response = requests.get(url, headers=headers)
|
398
|
+
except Exception as ex:
|
399
|
+
LOG.error(ex)
|
400
|
+
raise Exception(f"Failed to get metar {ex}")
|
401
|
+
else:
|
402
|
+
metar_data = json.loads(response.text)
|
403
|
+
|
404
|
+
# LOG.debug(metar_data)
|
405
|
+
return metar_data["raw"]
|
aprsd/stats/__init__.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
from aprsd import plugin
|
2
|
+
from aprsd.client import stats as client_stats
|
3
|
+
from aprsd.packets import packet_list, seen_list, tracker, watch_list
|
4
|
+
from aprsd.plugins import email
|
5
|
+
from aprsd.stats import app, collector
|
6
|
+
from aprsd.threads import aprsd
|
7
|
+
|
8
|
+
|
9
|
+
# Create the collector and register all the objects
|
10
|
+
# that APRSD has that implement the stats protocol
|
11
|
+
stats_collector = collector.Collector()
|
12
|
+
stats_collector.register_producer(app.APRSDStats)
|
13
|
+
stats_collector.register_producer(packet_list.PacketList)
|
14
|
+
stats_collector.register_producer(watch_list.WatchList)
|
15
|
+
stats_collector.register_producer(tracker.PacketTrack)
|
16
|
+
stats_collector.register_producer(plugin.PluginManager)
|
17
|
+
stats_collector.register_producer(aprsd.APRSDThreadList)
|
18
|
+
stats_collector.register_producer(email.EmailStats)
|
19
|
+
stats_collector.register_producer(client_stats.APRSClientStats)
|
20
|
+
stats_collector.register_producer(seen_list.SeenList)
|
aprsd/stats/app.py
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
import datetime
|
2
|
+
import tracemalloc
|
3
|
+
|
4
|
+
from oslo_config import cfg
|
5
|
+
|
6
|
+
import aprsd
|
7
|
+
from aprsd import utils
|
8
|
+
from aprsd.log import log as aprsd_log
|
9
|
+
|
10
|
+
|
11
|
+
CONF = cfg.CONF
|
12
|
+
|
13
|
+
|
14
|
+
class APRSDStats:
|
15
|
+
"""The AppStats class is used to collect stats from the application."""
|
16
|
+
|
17
|
+
_instance = None
|
18
|
+
start_time = None
|
19
|
+
|
20
|
+
def __new__(cls, *args, **kwargs):
|
21
|
+
"""Have to override the new method to make this a singleton
|
22
|
+
|
23
|
+
instead of using @singletone decorator so the unit tests work.
|
24
|
+
"""
|
25
|
+
if not cls._instance:
|
26
|
+
cls._instance = super().__new__(cls)
|
27
|
+
cls._instance.start_time = datetime.datetime.now()
|
28
|
+
return cls._instance
|
29
|
+
|
30
|
+
def uptime(self):
|
31
|
+
return datetime.datetime.now() - self.start_time
|
32
|
+
|
33
|
+
def stats(self, serializable=False) -> dict:
|
34
|
+
current, peak = tracemalloc.get_traced_memory()
|
35
|
+
uptime = self.uptime()
|
36
|
+
qsize = aprsd_log.logging_queue.qsize()
|
37
|
+
if serializable:
|
38
|
+
uptime = str(uptime)
|
39
|
+
stats = {
|
40
|
+
"version": aprsd.__version__,
|
41
|
+
"uptime": uptime,
|
42
|
+
"callsign": CONF.callsign,
|
43
|
+
"memory_current": int(current),
|
44
|
+
"memory_current_str": utils.human_size(current),
|
45
|
+
"memory_peak": int(peak),
|
46
|
+
"memory_peak_str": utils.human_size(peak),
|
47
|
+
"loging_queue": qsize,
|
48
|
+
}
|
49
|
+
return stats
|
aprsd/stats/collector.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Callable, Protocol, runtime_checkable
|
3
|
+
|
4
|
+
from aprsd.utils import singleton
|
5
|
+
|
6
|
+
|
7
|
+
LOG = logging.getLogger("APRSD")
|
8
|
+
|
9
|
+
|
10
|
+
@runtime_checkable
|
11
|
+
class StatsProducer(Protocol):
|
12
|
+
"""The StatsProducer protocol is used to define the interface for collecting stats."""
|
13
|
+
def stats(self, serializeable=False) -> dict:
|
14
|
+
"""provide stats in a dictionary format."""
|
15
|
+
...
|
16
|
+
|
17
|
+
|
18
|
+
@singleton
|
19
|
+
class Collector:
|
20
|
+
"""The Collector class is used to collect stats from multiple StatsProducer instances."""
|
21
|
+
def __init__(self):
|
22
|
+
self.producers: list[Callable] = []
|
23
|
+
|
24
|
+
def collect(self, serializable=False) -> dict:
|
25
|
+
stats = {}
|
26
|
+
for name in self.producers:
|
27
|
+
cls = name()
|
28
|
+
if isinstance(cls, StatsProducer):
|
29
|
+
try:
|
30
|
+
stats[cls.__class__.__name__] = cls.stats(serializable=serializable).copy()
|
31
|
+
except Exception as e:
|
32
|
+
LOG.error(f"Error in producer {name} (stats): {e}")
|
33
|
+
else:
|
34
|
+
raise TypeError(f"{cls} is not an instance of StatsProducer")
|
35
|
+
return stats
|
36
|
+
|
37
|
+
def register_producer(self, producer_name: Callable):
|
38
|
+
self.producers.append(producer_name)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import queue
|
2
|
+
|
3
|
+
# Make these available to anyone importing
|
4
|
+
# aprsd.threads
|
5
|
+
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
|
6
|
+
from .rx import ( # noqa: F401
|
7
|
+
APRSDDupeRXThread, APRSDProcessPacketThread, APRSDRXThread,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
packet_queue = queue.Queue(maxsize=20)
|
aprsd/threads/aprsd.py
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
import abc
|
2
|
+
import datetime
|
3
|
+
import logging
|
4
|
+
import threading
|
5
|
+
from typing import List
|
6
|
+
|
7
|
+
import wrapt
|
8
|
+
|
9
|
+
|
10
|
+
LOG = logging.getLogger("APRSD")
|
11
|
+
|
12
|
+
|
13
|
+
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
14
|
+
"""Base class for all threads in APRSD."""
|
15
|
+
|
16
|
+
loop_count = 1
|
17
|
+
|
18
|
+
def __init__(self, name):
|
19
|
+
super().__init__(name=name)
|
20
|
+
self.thread_stop = False
|
21
|
+
APRSDThreadList().add(self)
|
22
|
+
self._last_loop = datetime.datetime.now()
|
23
|
+
|
24
|
+
def _should_quit(self):
|
25
|
+
""" see if we have a quit message from the global queue."""
|
26
|
+
if self.thread_stop:
|
27
|
+
return True
|
28
|
+
|
29
|
+
def stop(self):
|
30
|
+
self.thread_stop = True
|
31
|
+
|
32
|
+
@abc.abstractmethod
|
33
|
+
def loop(self):
|
34
|
+
pass
|
35
|
+
|
36
|
+
def _cleanup(self):
|
37
|
+
"""Add code to subclass to do any cleanup"""
|
38
|
+
|
39
|
+
def __str__(self):
|
40
|
+
out = f"Thread <{self.__class__.__name__}({self.name}) Alive? {self.is_alive()}>"
|
41
|
+
return out
|
42
|
+
|
43
|
+
def loop_age(self):
|
44
|
+
"""How old is the last loop call?"""
|
45
|
+
return datetime.datetime.now() - self._last_loop
|
46
|
+
|
47
|
+
def run(self):
|
48
|
+
LOG.debug("Starting")
|
49
|
+
while not self._should_quit():
|
50
|
+
self.loop_count += 1
|
51
|
+
can_loop = self.loop()
|
52
|
+
self._last_loop = datetime.datetime.now()
|
53
|
+
if not can_loop:
|
54
|
+
self.stop()
|
55
|
+
self._cleanup()
|
56
|
+
APRSDThreadList().remove(self)
|
57
|
+
LOG.debug("Exiting")
|
58
|
+
|
59
|
+
|
60
|
+
class APRSDThreadList:
|
61
|
+
"""Singleton class that keeps track of application wide threads."""
|
62
|
+
|
63
|
+
_instance = None
|
64
|
+
|
65
|
+
threads_list: List[APRSDThread] = []
|
66
|
+
lock = threading.Lock()
|
67
|
+
|
68
|
+
def __new__(cls, *args, **kwargs):
|
69
|
+
if cls._instance is None:
|
70
|
+
cls._instance = super().__new__(cls)
|
71
|
+
cls.threads_list = []
|
72
|
+
return cls._instance
|
73
|
+
|
74
|
+
def stats(self, serializable=False) -> dict:
|
75
|
+
stats = {}
|
76
|
+
for th in self.threads_list:
|
77
|
+
age = th.loop_age()
|
78
|
+
if serializable:
|
79
|
+
age = str(age)
|
80
|
+
stats[th.name] = {
|
81
|
+
"name": th.name,
|
82
|
+
"class": th.__class__.__name__,
|
83
|
+
"alive": th.is_alive(),
|
84
|
+
"age": th.loop_age(),
|
85
|
+
"loop_count": th.loop_count,
|
86
|
+
}
|
87
|
+
return stats
|
88
|
+
|
89
|
+
@wrapt.synchronized(lock)
|
90
|
+
def add(self, thread_obj):
|
91
|
+
self.threads_list.append(thread_obj)
|
92
|
+
|
93
|
+
@wrapt.synchronized(lock)
|
94
|
+
def remove(self, thread_obj):
|
95
|
+
self.threads_list.remove(thread_obj)
|
96
|
+
|
97
|
+
@wrapt.synchronized(lock)
|
98
|
+
def stop_all(self):
|
99
|
+
"""Iterate over all threads and call stop on them."""
|
100
|
+
for th in self.threads_list:
|
101
|
+
LOG.info(f"Stopping Thread {th.name}")
|
102
|
+
if hasattr(th, "packet"):
|
103
|
+
LOG.info(F"{th.name} packet {th.packet}")
|
104
|
+
th.stop()
|
105
|
+
|
106
|
+
@wrapt.synchronized(lock)
|
107
|
+
def info(self):
|
108
|
+
"""Go through all the threads and collect info about each."""
|
109
|
+
info = {}
|
110
|
+
for thread in self.threads_list:
|
111
|
+
alive = thread.is_alive()
|
112
|
+
age = thread.loop_age()
|
113
|
+
key = thread.__class__.__name__
|
114
|
+
info[key] = {"alive": True if alive else False, "age": age, "name": thread.name}
|
115
|
+
return info
|
116
|
+
|
117
|
+
@wrapt.synchronized(lock)
|
118
|
+
def __len__(self):
|
119
|
+
return len(self.threads_list)
|