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/fortune.py
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
import logging
|
2
|
+
import shutil
|
3
|
+
import subprocess
|
4
|
+
|
5
|
+
from aprsd import packets, plugin
|
6
|
+
from aprsd.utils import trace
|
7
|
+
|
8
|
+
|
9
|
+
LOG = logging.getLogger("APRSD")
|
10
|
+
|
11
|
+
DEFAULT_FORTUNE_PATH = '/usr/games/fortune'
|
12
|
+
|
13
|
+
|
14
|
+
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
15
|
+
"""Fortune."""
|
16
|
+
|
17
|
+
command_regex = r"^([f]|[f]\s|fortune)"
|
18
|
+
command_name = "fortune"
|
19
|
+
short_description = "Give me a fortune"
|
20
|
+
|
21
|
+
fortune_path = None
|
22
|
+
|
23
|
+
def setup(self):
|
24
|
+
self.fortune_path = shutil.which(DEFAULT_FORTUNE_PATH)
|
25
|
+
LOG.info(f"Fortune path {self.fortune_path}")
|
26
|
+
if not self.fortune_path:
|
27
|
+
self.enabled = False
|
28
|
+
else:
|
29
|
+
self.enabled = True
|
30
|
+
|
31
|
+
@trace.trace
|
32
|
+
def process(self, packet: packets.MessagePacket):
|
33
|
+
LOG.info("FortunePlugin")
|
34
|
+
|
35
|
+
# fromcall = packet.get("from")
|
36
|
+
# message = packet.get("message_text", None)
|
37
|
+
# ack = packet.get("msgNo", "0")
|
38
|
+
|
39
|
+
reply = None
|
40
|
+
|
41
|
+
try:
|
42
|
+
cmnd = [self.fortune_path, "-s", "-n 60"]
|
43
|
+
command = " ".join(cmnd)
|
44
|
+
output = subprocess.check_output(
|
45
|
+
command,
|
46
|
+
shell=True,
|
47
|
+
timeout=3,
|
48
|
+
universal_newlines=True,
|
49
|
+
)
|
50
|
+
output = (
|
51
|
+
output.replace("\r", "")
|
52
|
+
.replace("\n", "")
|
53
|
+
.replace(" ", "")
|
54
|
+
.replace("\t", " ")
|
55
|
+
)
|
56
|
+
except subprocess.CalledProcessError as ex:
|
57
|
+
reply = f"Fortune command failed '{ex.output}'"
|
58
|
+
else:
|
59
|
+
reply = output
|
60
|
+
|
61
|
+
return reply
|
@@ -0,0 +1,179 @@
|
|
1
|
+
import logging
|
2
|
+
import re
|
3
|
+
import time
|
4
|
+
|
5
|
+
from geopy.geocoders import ArcGIS, AzureMaps, Baidu, Bing, GoogleV3
|
6
|
+
from geopy.geocoders import HereV7, Nominatim, OpenCage, TomTom, What3WordsV3, Woosmap
|
7
|
+
from oslo_config import cfg
|
8
|
+
|
9
|
+
from aprsd import packets, plugin, plugin_utils
|
10
|
+
from aprsd.utils import trace
|
11
|
+
|
12
|
+
|
13
|
+
CONF = cfg.CONF
|
14
|
+
LOG = logging.getLogger("APRSD")
|
15
|
+
|
16
|
+
|
17
|
+
class UsLocation:
|
18
|
+
raw = {}
|
19
|
+
|
20
|
+
def __init__(self, info):
|
21
|
+
self.info = info
|
22
|
+
|
23
|
+
def __str__(self):
|
24
|
+
return self.info
|
25
|
+
|
26
|
+
|
27
|
+
class USGov:
|
28
|
+
"""US Government geocoder that uses the geopy API.
|
29
|
+
|
30
|
+
This is a dummy class the implements the geopy reverse API,
|
31
|
+
so the factory can return an object that conforms to the API.
|
32
|
+
"""
|
33
|
+
def reverse(self, coordinates):
|
34
|
+
"""Reverse geocode a coordinate."""
|
35
|
+
LOG.info(f"USGov reverse geocode {coordinates}")
|
36
|
+
coords = coordinates.split(",")
|
37
|
+
lat = float(coords[0])
|
38
|
+
lon = float(coords[1])
|
39
|
+
result = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
40
|
+
# LOG.info(f"WEATHER: {result}")
|
41
|
+
# LOG.info(f"area description {result['location']['areaDescription']}")
|
42
|
+
if 'location' in result:
|
43
|
+
loc = UsLocation(result['location']['areaDescription'])
|
44
|
+
else:
|
45
|
+
loc = UsLocation("Unknown Location")
|
46
|
+
|
47
|
+
LOG.info(f"USGov reverse geocode LOC {loc}")
|
48
|
+
return loc
|
49
|
+
|
50
|
+
|
51
|
+
def geopy_factory():
|
52
|
+
"""Factory function for geopy geocoders."""
|
53
|
+
geocoder = CONF.location_plugin.geopy_geocoder
|
54
|
+
LOG.info(f"Using geocoder: {geocoder}")
|
55
|
+
user_agent = CONF.location_plugin.user_agent
|
56
|
+
LOG.info(f"Using user_agent: {user_agent}")
|
57
|
+
|
58
|
+
if geocoder == "Nominatim":
|
59
|
+
return Nominatim(user_agent=user_agent)
|
60
|
+
elif geocoder == "USGov":
|
61
|
+
return USGov()
|
62
|
+
elif geocoder == "ArcGIS":
|
63
|
+
return ArcGIS(
|
64
|
+
username=CONF.location_plugin.arcgis_username,
|
65
|
+
password=CONF.location_plugin.arcgis_password,
|
66
|
+
user_agent=user_agent,
|
67
|
+
)
|
68
|
+
elif geocoder == "AzureMaps":
|
69
|
+
return AzureMaps(
|
70
|
+
user_agent=user_agent,
|
71
|
+
subscription_key=CONF.location_plugin.azuremaps_subscription_key,
|
72
|
+
)
|
73
|
+
elif geocoder == "Baidu":
|
74
|
+
return Baidu(user_agent=user_agent, api_key=CONF.location_plugin.baidu_api_key)
|
75
|
+
elif geocoder == "Bing":
|
76
|
+
return Bing(user_agent=user_agent, api_key=CONF.location_plugin.bing_api_key)
|
77
|
+
elif geocoder == "GoogleV3":
|
78
|
+
return GoogleV3(user_agent=user_agent, api_key=CONF.location_plugin.google_api_key)
|
79
|
+
elif geocoder == "HERE":
|
80
|
+
return HereV7(user_agent=user_agent, api_key=CONF.location_plugin.here_api_key)
|
81
|
+
elif geocoder == "OpenCage":
|
82
|
+
return OpenCage(user_agent=user_agent, api_key=CONF.location_plugin.opencage_api_key)
|
83
|
+
elif geocoder == "TomTom":
|
84
|
+
return TomTom(user_agent=user_agent, api_key=CONF.location_plugin.tomtom_api_key)
|
85
|
+
elif geocoder == "What3Words":
|
86
|
+
return What3WordsV3(user_agent=user_agent, api_key=CONF.location_plugin.what3words_api_key)
|
87
|
+
elif geocoder == "Woosmap":
|
88
|
+
return Woosmap(user_agent=user_agent, api_key=CONF.location_plugin.woosmap_api_key)
|
89
|
+
else:
|
90
|
+
raise ValueError(f"Unknown geocoder: {geocoder}")
|
91
|
+
|
92
|
+
|
93
|
+
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
94
|
+
"""Location!"""
|
95
|
+
|
96
|
+
command_regex = r"^([l]|[l]\s|location)"
|
97
|
+
command_name = "location"
|
98
|
+
short_description = "Where in the world is a CALLSIGN's last GPS beacon?"
|
99
|
+
|
100
|
+
def setup(self):
|
101
|
+
self.ensure_aprs_fi_key()
|
102
|
+
|
103
|
+
@trace.trace
|
104
|
+
def process(self, packet: packets.MessagePacket):
|
105
|
+
LOG.info("Location Plugin")
|
106
|
+
fromcall = packet.from_call
|
107
|
+
message = packet.get("message_text", None)
|
108
|
+
|
109
|
+
api_key = CONF.aprs_fi.apiKey
|
110
|
+
|
111
|
+
# optional second argument is a callsign to search
|
112
|
+
a = re.search(r"^.*\s+(.*)", message)
|
113
|
+
if a is not None:
|
114
|
+
searchcall = a.group(1)
|
115
|
+
searchcall = searchcall.upper()
|
116
|
+
else:
|
117
|
+
# if no second argument, search for calling station
|
118
|
+
searchcall = fromcall
|
119
|
+
|
120
|
+
try:
|
121
|
+
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
122
|
+
except Exception as ex:
|
123
|
+
LOG.error(f"Failed to fetch aprs.fi '{ex}'")
|
124
|
+
return "Failed to fetch aprs.fi location"
|
125
|
+
|
126
|
+
LOG.debug(f"LocationPlugin: aprs_data = {aprs_data}")
|
127
|
+
if not len(aprs_data["entries"]):
|
128
|
+
LOG.error("Didn't get any entries from aprs.fi")
|
129
|
+
return "Failed to fetch aprs.fi location"
|
130
|
+
|
131
|
+
lat = float(aprs_data["entries"][0]["lat"])
|
132
|
+
lon = float(aprs_data["entries"][0]["lng"])
|
133
|
+
|
134
|
+
# Get some information about their location
|
135
|
+
try:
|
136
|
+
tic = time.perf_counter()
|
137
|
+
geolocator = geopy_factory()
|
138
|
+
LOG.info(f"Using GEOLOCATOR: {geolocator}")
|
139
|
+
coordinates = f"{lat:0.6f}, {lon:0.6f}"
|
140
|
+
location = geolocator.reverse(coordinates)
|
141
|
+
address = location.raw.get("address")
|
142
|
+
LOG.debug(f"GEOLOCATOR address: {address}")
|
143
|
+
toc = time.perf_counter()
|
144
|
+
if address:
|
145
|
+
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
|
146
|
+
if address.get("country_code") == "us":
|
147
|
+
area_info = f"{address.get('county')}, {address.get('state')}"
|
148
|
+
else:
|
149
|
+
# what to do for address for non US?
|
150
|
+
area_info = f"{address.get('country'), 'Unknown'}"
|
151
|
+
else:
|
152
|
+
area_info = str(location)
|
153
|
+
except Exception as ex:
|
154
|
+
LOG.error(ex)
|
155
|
+
LOG.error(f"Failed to fetch Geopy address {ex}")
|
156
|
+
area_info = "Unknown Location"
|
157
|
+
|
158
|
+
try: # altitude not always provided
|
159
|
+
alt = float(aprs_data["entries"][0]["altitude"])
|
160
|
+
except Exception:
|
161
|
+
alt = 0
|
162
|
+
altfeet = int(alt * 3.28084)
|
163
|
+
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
164
|
+
# aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
165
|
+
# "ascii", errors="ignore"
|
166
|
+
# ) # unicode to ascii
|
167
|
+
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
168
|
+
delta_hours = delta_seconds / 60 / 60
|
169
|
+
|
170
|
+
reply = "{}: {} {}' {},{} {}h ago".format(
|
171
|
+
searchcall,
|
172
|
+
area_info,
|
173
|
+
str(altfeet),
|
174
|
+
f"{lat:0.2f}",
|
175
|
+
f"{lon:0.2f}",
|
176
|
+
str("%.1f" % round(delta_hours, 1)),
|
177
|
+
).rstrip()
|
178
|
+
|
179
|
+
return reply
|
aprsd/plugins/notify.py
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from oslo_config import cfg
|
4
|
+
|
5
|
+
from aprsd import packets, plugin
|
6
|
+
|
7
|
+
|
8
|
+
CONF = cfg.CONF
|
9
|
+
LOG = logging.getLogger("APRSD")
|
10
|
+
|
11
|
+
|
12
|
+
class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
13
|
+
"""Notification plugin to send seen message for callsign.
|
14
|
+
|
15
|
+
|
16
|
+
This plugin will track callsigns in the watch list and report
|
17
|
+
when a callsign has been seen when the last time they were
|
18
|
+
seen was older than the configured age limit.
|
19
|
+
"""
|
20
|
+
|
21
|
+
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
|
22
|
+
|
23
|
+
def process(self, packet: packets.MessagePacket):
|
24
|
+
LOG.info("NotifySeenPlugin")
|
25
|
+
|
26
|
+
notify_callsign = CONF.watch_list.alert_callsign
|
27
|
+
fromcall = packet.from_call
|
28
|
+
|
29
|
+
wl = packets.WatchList()
|
30
|
+
age = wl.age(fromcall)
|
31
|
+
|
32
|
+
if fromcall != notify_callsign:
|
33
|
+
if wl.is_old(fromcall):
|
34
|
+
LOG.info(
|
35
|
+
"NOTIFY {} last seen {} max age={}".format(
|
36
|
+
fromcall,
|
37
|
+
age,
|
38
|
+
wl.max_delta(),
|
39
|
+
),
|
40
|
+
)
|
41
|
+
packet_type = packet.__class__.__name__
|
42
|
+
# we shouldn't notify the alert user that they are online.
|
43
|
+
pkt = packets.MessagePacket(
|
44
|
+
from_call=CONF.callsign,
|
45
|
+
to_call=notify_callsign,
|
46
|
+
message_text=(
|
47
|
+
f"{fromcall} was just seen by type:'{packet_type}'"
|
48
|
+
),
|
49
|
+
allow_delay=False,
|
50
|
+
)
|
51
|
+
pkt.allow_delay = False
|
52
|
+
return pkt
|
53
|
+
else:
|
54
|
+
LOG.debug(
|
55
|
+
"Not old enough to notify on callsign "
|
56
|
+
f"'{fromcall}' : {age} < {wl.max_delta()}",
|
57
|
+
)
|
58
|
+
return packets.NULL_MESSAGE
|
59
|
+
else:
|
60
|
+
LOG.debug("fromcall and notify_callsign are the same, ignoring")
|
61
|
+
return packets.NULL_MESSAGE
|
aprsd/plugins/ping.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
import logging
|
2
|
+
import time
|
3
|
+
|
4
|
+
from aprsd import plugin
|
5
|
+
from aprsd.utils import trace
|
6
|
+
|
7
|
+
|
8
|
+
LOG = logging.getLogger("APRSD")
|
9
|
+
|
10
|
+
|
11
|
+
class PingPlugin(plugin.APRSDRegexCommandPluginBase):
|
12
|
+
"""Ping."""
|
13
|
+
|
14
|
+
command_regex = r"^([p]|[p]\s|ping)"
|
15
|
+
command_name = "ping"
|
16
|
+
short_description = "reply with a Pong!"
|
17
|
+
|
18
|
+
@trace.trace
|
19
|
+
def process(self, packet):
|
20
|
+
LOG.info("PingPlugin")
|
21
|
+
# fromcall = packet.get("from")
|
22
|
+
# message = packet.get("message_text", None)
|
23
|
+
# ack = packet.get("msgNo", "0")
|
24
|
+
stm = time.localtime()
|
25
|
+
h = stm.tm_hour
|
26
|
+
m = stm.tm_min
|
27
|
+
s = stm.tm_sec
|
28
|
+
reply = (
|
29
|
+
"Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
|
30
|
+
)
|
31
|
+
return reply.rstrip()
|
aprsd/plugins/time.py
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
import logging
|
2
|
+
import re
|
3
|
+
|
4
|
+
from oslo_config import cfg
|
5
|
+
import pytz
|
6
|
+
from tzlocal import get_localzone
|
7
|
+
|
8
|
+
from aprsd import packets, plugin, plugin_utils
|
9
|
+
from aprsd.utils import fuzzy, trace
|
10
|
+
|
11
|
+
|
12
|
+
CONF = cfg.CONF
|
13
|
+
LOG = logging.getLogger("APRSD")
|
14
|
+
|
15
|
+
|
16
|
+
class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
17
|
+
"""Time command."""
|
18
|
+
|
19
|
+
# Look for t or t<space> or T<space> or time
|
20
|
+
command_regex = r"^([t]|[t]\s|time)"
|
21
|
+
command_name = "time"
|
22
|
+
short_description = "What is the current local time."
|
23
|
+
|
24
|
+
def _get_local_tz(self):
|
25
|
+
lz = get_localzone()
|
26
|
+
return pytz.timezone(str(lz))
|
27
|
+
|
28
|
+
def _get_utcnow(self):
|
29
|
+
return pytz.datetime.datetime.utcnow()
|
30
|
+
|
31
|
+
def build_date_str(self, localzone):
|
32
|
+
utcnow = self._get_utcnow()
|
33
|
+
gmt_t = pytz.utc.localize(utcnow)
|
34
|
+
local_t = gmt_t.astimezone(localzone)
|
35
|
+
|
36
|
+
local_short_str = local_t.strftime("%H:%M %Z")
|
37
|
+
local_hour = local_t.strftime("%H")
|
38
|
+
local_min = local_t.strftime("%M")
|
39
|
+
cur_time = fuzzy(int(local_hour), int(local_min), 1)
|
40
|
+
|
41
|
+
reply = "{} ({})".format(
|
42
|
+
cur_time,
|
43
|
+
local_short_str,
|
44
|
+
)
|
45
|
+
|
46
|
+
return reply
|
47
|
+
|
48
|
+
@trace.trace
|
49
|
+
def process(self, packet: packets.Packet):
|
50
|
+
LOG.info("TIME COMMAND")
|
51
|
+
# So we can mock this in unit tests
|
52
|
+
localzone = self._get_local_tz()
|
53
|
+
return self.build_date_str(localzone)
|
54
|
+
|
55
|
+
|
56
|
+
class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
|
57
|
+
"""OpenWeatherMap based timezone fetching."""
|
58
|
+
|
59
|
+
command_regex = r"^([t]|[t]\s|time)"
|
60
|
+
command_name = "time"
|
61
|
+
short_description = "Current time of GPS beacon's timezone. Uses OpenWeatherMap"
|
62
|
+
|
63
|
+
def setup(self):
|
64
|
+
self.ensure_aprs_fi_key()
|
65
|
+
|
66
|
+
@trace.trace
|
67
|
+
def process(self, packet: packets.MessagePacket):
|
68
|
+
fromcall = packet.from_call
|
69
|
+
message = packet.message_text
|
70
|
+
# ack = packet.get("msgNo", "0")
|
71
|
+
|
72
|
+
# optional second argument is a callsign to search
|
73
|
+
a = re.search(r"^.*\s+(.*)", message)
|
74
|
+
if a is not None:
|
75
|
+
searchcall = a.group(1)
|
76
|
+
searchcall = searchcall.upper()
|
77
|
+
else:
|
78
|
+
# if no second argument, search for calling station
|
79
|
+
searchcall = fromcall
|
80
|
+
|
81
|
+
api_key = CONF.aprs_fi.apiKey
|
82
|
+
try:
|
83
|
+
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
84
|
+
except Exception as ex:
|
85
|
+
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
86
|
+
return "Failed to fetch location"
|
87
|
+
|
88
|
+
LOG.debug(f"LocationPlugin: aprs_data = {aprs_data}")
|
89
|
+
if not len(aprs_data["entries"]):
|
90
|
+
LOG.error("Didn't get any entries from aprs.fi")
|
91
|
+
return "Failed to fetch aprs.fi location"
|
92
|
+
|
93
|
+
lat = aprs_data["entries"][0]["lat"]
|
94
|
+
lon = aprs_data["entries"][0]["lng"]
|
95
|
+
|
96
|
+
try:
|
97
|
+
self.config.exists(
|
98
|
+
["services", "openweathermap", "apiKey"],
|
99
|
+
)
|
100
|
+
except Exception as ex:
|
101
|
+
LOG.error(f"Failed to find config openweathermap:apiKey {ex}")
|
102
|
+
return "No openweathermap apiKey found"
|
103
|
+
|
104
|
+
api_key = self.config["services"]["openweathermap"]["apiKey"]
|
105
|
+
try:
|
106
|
+
results = plugin_utils.fetch_openweathermap(api_key, lat, lon)
|
107
|
+
except Exception as ex:
|
108
|
+
LOG.error(f"Couldn't fetch openweathermap api '{ex}'")
|
109
|
+
# default to UTC
|
110
|
+
localzone = pytz.timezone("UTC")
|
111
|
+
else:
|
112
|
+
tzone = results["timezone"]
|
113
|
+
localzone = pytz.timezone(tzone)
|
114
|
+
|
115
|
+
return self.build_date_str(localzone)
|
aprsd/plugins/version.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import aprsd
|
4
|
+
from aprsd import plugin
|
5
|
+
from aprsd.stats import collector
|
6
|
+
|
7
|
+
|
8
|
+
LOG = logging.getLogger("APRSD")
|
9
|
+
|
10
|
+
|
11
|
+
class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
|
12
|
+
"""Version of APRSD Plugin."""
|
13
|
+
|
14
|
+
command_regex = r"^([v]|[v]\s|version)"
|
15
|
+
command_name = "version"
|
16
|
+
short_description = "What is the APRSD Version"
|
17
|
+
|
18
|
+
# message_number:time combos so we don't resend the same email in
|
19
|
+
# five mins {int:int}
|
20
|
+
email_sent_dict = {}
|
21
|
+
|
22
|
+
def process(self, packet):
|
23
|
+
LOG.info("Version COMMAND")
|
24
|
+
# fromcall = packet.get("from")
|
25
|
+
# message = packet.get("message_text", None)
|
26
|
+
# ack = packet.get("msgNo", "0")
|
27
|
+
s = collector.Collector().collect()
|
28
|
+
return "APRSD ver:{} uptime:{}".format(
|
29
|
+
aprsd.__version__,
|
30
|
+
s["APRSDStats"]["uptime"],
|
31
|
+
)
|