aprsd 3.2.2__py2.py3-none-any.whl → 3.3.1__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.
aprsd/cmds/webchat.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import datetime
2
2
  import json
3
3
  import logging
4
- from logging.handlers import RotatingFileHandler
4
+ import math
5
5
  import signal
6
6
  import sys
7
7
  import threading
@@ -11,17 +11,20 @@ from aprslib import util as aprslib_util
11
11
  import click
12
12
  import flask
13
13
  from flask import request
14
- from flask.logging import default_handler
15
14
  from flask_httpauth import HTTPBasicAuth
16
15
  from flask_socketio import Namespace, SocketIO
16
+ from geopy.distance import geodesic
17
17
  from oslo_config import cfg
18
18
  from werkzeug.security import check_password_hash, generate_password_hash
19
19
  import wrapt
20
20
 
21
21
  import aprsd
22
- from aprsd import cli_helper, client, conf, packets, stats, threads, utils
23
- from aprsd.log import rich as aprsd_logging
22
+ from aprsd import (
23
+ cli_helper, client, packets, plugin_utils, stats, threads, utils,
24
+ )
25
+ from aprsd.log import log
24
26
  from aprsd.main import cli
27
+ from aprsd.threads import aprsd as aprsd_threads
25
28
  from aprsd.threads import rx, tx
26
29
  from aprsd.utils import trace
27
30
 
@@ -32,6 +35,16 @@ auth = HTTPBasicAuth()
32
35
  users = {}
33
36
  socketio = None
34
37
 
38
+ # List of callsigns that we don't want to track/fetch their location
39
+ callsign_no_track = [
40
+ "REPEAT", "WB4BOR-11", "APDW16", "WXNOW", "WXBOT", "BLN0", "BLN1", "BLN2",
41
+ "BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
42
+ ]
43
+
44
+ # Callsign location information
45
+ # callsign: {lat: 0.0, long: 0.0, last_update: datetime}
46
+ callsign_locations = {}
47
+
35
48
  flask_app = flask.Flask(
36
49
  "aprsd",
37
50
  static_url_path="/static",
@@ -121,8 +134,188 @@ def verify_password(username, password):
121
134
  return username
122
135
 
123
136
 
137
+ def calculate_initial_compass_bearing(point_a, point_b):
138
+ """
139
+ Calculates the bearing between two points.
140
+ The formulae used is the following:
141
+ θ = atan2(sin(Δlong).cos(lat2),
142
+ cos(lat1).sin(lat2) − sin(lat1).cos(lat2).cos(Δlong))
143
+ :Parameters:
144
+ - `pointA: The tuple representing the latitude/longitude for the
145
+ first point. Latitude and longitude must be in decimal degrees
146
+ - `pointB: The tuple representing the latitude/longitude for the
147
+ second point. Latitude and longitude must be in decimal degrees
148
+ :Returns:
149
+ The bearing in degrees
150
+ :Returns Type:
151
+ float
152
+ """
153
+ if (type(point_a) is not tuple) or (type(point_b) is not tuple):
154
+ raise TypeError("Only tuples are supported as arguments")
155
+
156
+ lat1 = math.radians(point_a[0])
157
+ lat2 = math.radians(point_b[0])
158
+
159
+ diff_long = math.radians(point_b[1] - point_a[1])
160
+
161
+ x = math.sin(diff_long) * math.cos(lat2)
162
+ y = math.cos(lat1) * math.sin(lat2) - (
163
+ math.sin(lat1)
164
+ * math.cos(lat2) * math.cos(diff_long)
165
+ )
166
+
167
+ initial_bearing = math.atan2(x, y)
168
+
169
+ # Now we have the initial bearing but math.atan2 return values
170
+ # from -180° to + 180° which is not what we want for a compass bearing
171
+ # The solution is to normalize the initial bearing as shown below
172
+ initial_bearing = math.degrees(initial_bearing)
173
+ compass_bearing = (initial_bearing + 360) % 360
174
+
175
+ return compass_bearing
176
+
177
+
178
+ def _build_location_from_repeat(message):
179
+ # This is a location message Format is
180
+ # ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
181
+ a = message.split(":")
182
+ LOG.warning(a)
183
+ if len(a) == 2:
184
+ callsign = a[0].replace("^ld^", "")
185
+ b = a[1].split(",")
186
+ LOG.warning(b)
187
+ if len(b) == 6:
188
+ lat = float(b[0])
189
+ lon = float(b[1])
190
+ alt = float(b[2])
191
+ course = float(b[3])
192
+ speed = float(b[4])
193
+ time = int(b[5])
194
+ data = {
195
+ "callsign": callsign,
196
+ "lat": lat,
197
+ "lon": lon,
198
+ "altitude": alt,
199
+ "course": course,
200
+ "speed": speed,
201
+ "lasttime": time,
202
+ }
203
+ LOG.warning(f"Location data from REPEAT {data}")
204
+ return data
205
+
206
+
207
+ def _calculate_location_data(location_data):
208
+ """Calculate all of the location data from data from aprs.fi or REPEAT."""
209
+ lat = location_data["lat"]
210
+ lon = location_data["lon"]
211
+ alt = location_data["altitude"]
212
+ speed = location_data["speed"]
213
+ lasttime = location_data["lasttime"]
214
+ # now calculate distance from our own location
215
+ distance = 0
216
+ if CONF.webchat.latitude and CONF.webchat.longitude:
217
+ our_lat = float(CONF.webchat.latitude)
218
+ our_lon = float(CONF.webchat.longitude)
219
+ distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
220
+ bearing = calculate_initial_compass_bearing(
221
+ (our_lat, our_lon),
222
+ (lat, lon),
223
+ )
224
+ return {
225
+ "callsign": location_data["callsign"],
226
+ "lat": lat,
227
+ "lon": lon,
228
+ "altitude": alt,
229
+ "course": f"{bearing:0.1f}",
230
+ "speed": speed,
231
+ "lasttime": lasttime,
232
+ "distance": f"{distance:0.3f}",
233
+ }
234
+
235
+
236
+ def send_location_data_to_browser(location_data):
237
+ global socketio
238
+ callsign = location_data["callsign"]
239
+ LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
240
+ socketio.emit(
241
+ "callsign_location", callsign_locations[callsign],
242
+ namespace="/sendmsg",
243
+ )
244
+
245
+
246
+ def populate_callsign_location(callsign, data=None):
247
+ """Populate the location for the callsign.
248
+
249
+ if data is passed in, then we have the location already from
250
+ an APRS packet. If data is None, then we need to fetch the
251
+ location from aprs.fi or REPEAT.
252
+ """
253
+ global socketio
254
+ """Fetch the location for the callsign."""
255
+ LOG.debug(f"populate_callsign_location {callsign}")
256
+ if data:
257
+ location_data = _calculate_location_data(data)
258
+ callsign_locations[callsign] = location_data
259
+ send_location_data_to_browser(location_data)
260
+ return
261
+
262
+ # First we are going to try to get the location from aprs.fi
263
+ # if there is no internets, then this will fail and we will
264
+ # fallback to calling REPEAT for the location for the callsign.
265
+ fallback = False
266
+ if not CONF.aprs_fi.apiKey:
267
+ LOG.warning(
268
+ "Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
269
+ " falling back to sending REPEAT to get location.",
270
+ )
271
+ fallback = True
272
+ else:
273
+ try:
274
+ aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
275
+ if not len(aprs_data["entries"]):
276
+ LOG.error("Didn't get any entries from aprs.fi")
277
+ return
278
+ lat = float(aprs_data["entries"][0]["lat"])
279
+ lon = float(aprs_data["entries"][0]["lng"])
280
+ try: # altitude not always provided
281
+ alt = float(aprs_data["entries"][0]["altitude"])
282
+ except Exception:
283
+ alt = 0
284
+ location_data = {
285
+ "callsign": callsign,
286
+ "lat": lat,
287
+ "lon": lon,
288
+ "altitude": alt,
289
+ "lasttime": int(aprs_data["entries"][0]["lasttime"]),
290
+ "course": float(aprs_data["entries"][0].get("course", 0)),
291
+ "speed": float(aprs_data["entries"][0].get("speed", 0)),
292
+ }
293
+ location_data = _calculate_location_data(location_data)
294
+ callsign_locations[callsign] = location_data
295
+ send_location_data_to_browser(location_data)
296
+ return
297
+ except Exception as ex:
298
+ LOG.error(f"Failed to fetch aprs.fi '{ex}'")
299
+ LOG.error(ex)
300
+ fallback = True
301
+
302
+ if fallback:
303
+ # We don't have the location data
304
+ # and we can't get it from aprs.fi
305
+ # Send a special message to REPEAT to get the location data
306
+ LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
307
+ tx.send(
308
+ packets.MessagePacket(
309
+ from_call=CONF.callsign,
310
+ to_call="REPEAT",
311
+ message_text=f"ld {callsign}",
312
+ ),
313
+ )
314
+
315
+
124
316
  class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
125
317
  """Class that handles packets being sent to us."""
318
+
126
319
  def __init__(self, packet_queue, socketio):
127
320
  self.socketio = socketio
128
321
  self.connected = False
@@ -132,20 +325,53 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
132
325
  super().process_ack_packet(packet)
133
326
  ack_num = packet.get("msgNo")
134
327
  SentMessages().ack(ack_num)
135
- self.socketio.emit(
136
- "ack", SentMessages().get(ack_num),
137
- namespace="/sendmsg",
138
- )
328
+ msg = SentMessages().get(ack_num)
329
+ if msg:
330
+ self.socketio.emit(
331
+ "ack", msg,
332
+ namespace="/sendmsg",
333
+ )
139
334
  self.got_ack = True
140
335
 
141
336
  def process_our_message_packet(self, packet: packets.MessagePacket):
337
+ global callsign_locations
142
338
  LOG.info(f"process MessagePacket {repr(packet)}")
339
+ # ok lets see if we have the location for the
340
+ # person we just sent a message to.
341
+ from_call = packet.get("from_call").upper()
342
+ if from_call == "REPEAT":
343
+ # We got a message from REPEAT. Is this a location message?
344
+ message = packet.get("message_text")
345
+ if message.startswith("^ld^"):
346
+ location_data = _build_location_from_repeat(message)
347
+ callsign = location_data["callsign"]
348
+ location_data = _calculate_location_data(location_data)
349
+ callsign_locations[callsign] = location_data
350
+ send_location_data_to_browser(location_data)
351
+ return
352
+ elif (
353
+ from_call not in callsign_locations
354
+ and from_call not in callsign_no_track
355
+ ):
356
+ # We have to ask aprs for the location for the callsign
357
+ # We send a message packet to wb4bor-11 asking for location.
358
+ populate_callsign_location(from_call)
359
+ # Send the packet to the browser.
143
360
  self.socketio.emit(
144
361
  "new", packet.__dict__,
145
362
  namespace="/sendmsg",
146
363
  )
147
364
 
148
365
 
366
+ class LocationProcessingThread(aprsd_threads.APRSDThread):
367
+ """Class to handle the location processing."""
368
+ def __init__(self):
369
+ super().__init__("LocationProcessingThread")
370
+
371
+ def loop(self):
372
+ pass
373
+
374
+
149
375
  def set_config():
150
376
  global users
151
377
 
@@ -181,6 +407,12 @@ def _get_transport(stats):
181
407
  return transport, aprs_connection
182
408
 
183
409
 
410
+ @flask_app.route("/location/<callsign>", methods=["POST"])
411
+ def location(callsign):
412
+ LOG.debug(f"Fetch location for callsign {callsign}")
413
+ populate_callsign_location(callsign)
414
+
415
+
184
416
  @auth.login_required
185
417
  @flask_app.route("/")
186
418
  def index():
@@ -216,7 +448,7 @@ def index():
216
448
 
217
449
 
218
450
  @auth.login_required
219
- @flask_app.route("//send-message-status")
451
+ @flask_app.route("/send-message-status")
220
452
  def send_message_status():
221
453
  LOG.debug(request)
222
454
  msgs = SentMessages()
@@ -331,53 +563,21 @@ class SendMessageNamespace(Namespace):
331
563
  def handle_json(self, data):
332
564
  LOG.debug(f"WS json {data}")
333
565
 
334
-
335
- def setup_logging(flask_app, loglevel, quiet):
336
- flask_log = logging.getLogger("werkzeug")
337
- flask_app.logger.removeHandler(default_handler)
338
- flask_log.removeHandler(default_handler)
339
-
340
- log_level = conf.log.LOG_LEVELS[loglevel]
341
- flask_log.setLevel(log_level)
342
- date_format = CONF.logging.date_format
343
-
344
- if CONF.logging.rich_logging and not quiet:
345
- log_format = "%(message)s"
346
- log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
347
- rh = aprsd_logging.APRSDRichHandler(
348
- show_thread=True, thread_width=15,
349
- rich_tracebacks=True, omit_repeated_times=False,
350
- )
351
- rh.setFormatter(log_formatter)
352
- flask_log.addHandler(rh)
353
-
354
- log_file = CONF.logging.logfile
355
-
356
- if log_file:
357
- log_format = CONF.logging.logformat
358
- log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
359
- fh = RotatingFileHandler(
360
- log_file, maxBytes=(10248576 * 5),
361
- backupCount=4,
362
- )
363
- fh.setFormatter(log_formatter)
364
- flask_log.addHandler(fh)
566
+ def on_get_callsign_location(self, data):
567
+ LOG.debug(f"on_callsign_location {data}")
568
+ populate_callsign_location(data["callsign"])
365
569
 
366
570
 
367
571
  @trace.trace
368
572
  def init_flask(loglevel, quiet):
369
573
  global socketio, flask_app
370
574
 
371
- setup_logging(flask_app, loglevel, quiet)
575
+ log.setup_logging(loglevel, quiet)
372
576
 
373
577
  socketio = SocketIO(
374
578
  flask_app, logger=False, engineio_logger=False,
375
579
  async_mode="threading",
376
580
  )
377
- # async_mode="gevent",
378
- # async_mode="eventlet",
379
- # import eventlet
380
- # eventlet.monkey_patch()
381
581
 
382
582
  socketio.on_namespace(
383
583
  SendMessageNamespace(
aprsd/conf/common.py CHANGED
@@ -24,6 +24,11 @@ webchat_group = cfg.OptGroup(
24
24
  title="Settings specific to the webchat command",
25
25
  )
26
26
 
27
+ registry_group = cfg.OptGroup(
28
+ name="aprs_registry",
29
+ title="APRS Registry settings",
30
+ )
31
+
27
32
 
28
33
  aprsd_opts = [
29
34
  cfg.StrOpt(
@@ -67,9 +72,35 @@ aprsd_opts = [
67
72
  ),
68
73
  cfg.IntOpt(
69
74
  "packet_dupe_timeout",
70
- default=60,
75
+ default=300,
71
76
  help="The number of seconds before a packet is not considered a duplicate.",
72
77
  ),
78
+ cfg.BoolOpt(
79
+ "enable_beacon",
80
+ default=False,
81
+ help="Enable sending of a GPS Beacon packet to locate this service. "
82
+ "Requires latitude and longitude to be set.",
83
+ ),
84
+ cfg.IntOpt(
85
+ "beacon_interval",
86
+ default=1800,
87
+ help="The number of seconds between beacon packets.",
88
+ ),
89
+ cfg.StrOpt(
90
+ "beacon_symbol",
91
+ default="/",
92
+ help="The symbol to use for the GPS Beacon packet. See: http://www.aprs.net/vm/DOS/SYMBOLS.HTM",
93
+ ),
94
+ cfg.StrOpt(
95
+ "latitude",
96
+ default=None,
97
+ help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
98
+ ),
99
+ cfg.StrOpt(
100
+ "longitude",
101
+ default=None,
102
+ help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
103
+ ),
73
104
  ]
74
105
 
75
106
  watch_list_opts = [
@@ -196,6 +227,39 @@ webchat_opts = [
196
227
  ),
197
228
  ]
198
229
 
230
+ registry_opts = [
231
+ cfg.StrOpt(
232
+ "enabled",
233
+ default=False,
234
+ help="Enable sending aprs registry information. This will let the "
235
+ "APRS registry know about your service and it's uptime. "
236
+ "No personal information is sent, just the callsign, uptime and description. "
237
+ "The service callsign is the callsign set in [DEFAULT] section.",
238
+ ),
239
+ cfg.StrOpt(
240
+ "description",
241
+ default=None,
242
+ help="Description of the service to send to the APRS registry. "
243
+ "This is what will show up in the APRS registry."
244
+ "If not set, the description will be the same as the callsign.",
245
+ ),
246
+ cfg.StrOpt(
247
+ "registry_url",
248
+ default="https://aprs.hemna.com/api/v1/registry",
249
+ help="The APRS registry domain name to send the information to.",
250
+ ),
251
+ cfg.StrOpt(
252
+ "service_website",
253
+ default=None,
254
+ help="The website for your APRS service to send to the APRS registry.",
255
+ ),
256
+ cfg.IntOpt(
257
+ "frequency_seconds",
258
+ default=3600,
259
+ help="The frequency in seconds to send the APRS registry information.",
260
+ ),
261
+ ]
262
+
199
263
 
200
264
  def register_opts(config):
201
265
  config.register_opts(aprsd_opts)
@@ -208,6 +272,8 @@ def register_opts(config):
208
272
  config.register_opts(rpc_opts, group=rpc_group)
209
273
  config.register_group(webchat_group)
210
274
  config.register_opts(webchat_opts, group=webchat_group)
275
+ config.register_group(registry_group)
276
+ config.register_opts(registry_opts, group=registry_group)
211
277
 
212
278
 
213
279
  def list_opts():
@@ -217,4 +283,5 @@ def list_opts():
217
283
  watch_list_group.name: watch_list_opts,
218
284
  rpc_group.name: rpc_opts,
219
285
  webchat_group.name: webchat_opts,
286
+ registry_group.name: registry_opts,
220
287
  }
aprsd/conf/log.py CHANGED
@@ -20,21 +20,19 @@ DEFAULT_LOG_FORMAT = (
20
20
  " %(message)s - [%(pathname)s:%(lineno)d]"
21
21
  )
22
22
 
23
+ DEFAULT_LOG_FORMAT = (
24
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
25
+ "<yellow>{thread.name: <18}</yellow> | "
26
+ "<level>{level: <8}</level> | "
27
+ "<level>{message}</level> | "
28
+ "<cyan>{name}</cyan>:<cyan>{function:}</cyan>:<magenta>{line:}</magenta>"
29
+ )
30
+
23
31
  logging_group = cfg.OptGroup(
24
32
  name="logging",
25
33
  title="Logging options",
26
34
  )
27
35
  logging_opts = [
28
- cfg.StrOpt(
29
- "date_format",
30
- default=DEFAULT_DATE_FORMAT,
31
- help="Date format for log entries",
32
- ),
33
- cfg.BoolOpt(
34
- "rich_logging",
35
- default=True,
36
- help="Enable Rich log",
37
- ),
38
36
  cfg.StrOpt(
39
37
  "logfile",
40
38
  default=None,
@@ -18,6 +18,11 @@ owm_wx_group = cfg.OptGroup(
18
18
  title="Options for the OWMWeatherPlugin",
19
19
  )
20
20
 
21
+ location_group = cfg.OptGroup(
22
+ name="location_plugin",
23
+ title="Options for the LocationPlugin",
24
+ )
25
+
21
26
  aprsfi_opts = [
22
27
  cfg.StrOpt(
23
28
  "apiKey",
@@ -62,6 +67,106 @@ avwx_opts = [
62
67
  ),
63
68
  ]
64
69
 
70
+ location_opts = [
71
+ cfg.StrOpt(
72
+ "geopy_geocoder",
73
+ choices=[
74
+ "ArcGIS", "AzureMaps", "Baidu", "Bing", "GoogleV3", "HERE",
75
+ "Nominatim", "OpenCage", "TomTom", "USGov", "What3Words", "Woosmap",
76
+ ],
77
+ default="Nominatim",
78
+ help="The geopy geocoder to use. Default is Nominatim."
79
+ "See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
80
+ "for more information.",
81
+ ),
82
+ cfg.StrOpt(
83
+ "user_agent",
84
+ default="APRSD",
85
+ help="The user agent to use for the Nominatim geocoder."
86
+ "See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
87
+ "for more information.",
88
+ ),
89
+ cfg.StrOpt(
90
+ "arcgis_username",
91
+ default=None,
92
+ help="The username to use for the ArcGIS geocoder."
93
+ "See https://geopy.readthedocs.io/en/latest/#arcgis"
94
+ "for more information."
95
+ "Only used for the ArcGIS geocoder.",
96
+ ),
97
+ cfg.StrOpt(
98
+ "arcgis_password",
99
+ default=None,
100
+ help="The password to use for the ArcGIS geocoder."
101
+ "See https://geopy.readthedocs.io/en/latest/#arcgis"
102
+ "for more information."
103
+ "Only used for the ArcGIS geocoder.",
104
+ ),
105
+ cfg.StrOpt(
106
+ "azuremaps_subscription_key",
107
+ help="The subscription key to use for the AzureMaps geocoder."
108
+ "See https://geopy.readthedocs.io/en/latest/#azuremaps"
109
+ "for more information."
110
+ "Only used for the AzureMaps geocoder.",
111
+ ),
112
+ cfg.StrOpt(
113
+ "baidu_api_key",
114
+ help="The API key to use for the Baidu geocoder."
115
+ "See https://geopy.readthedocs.io/en/latest/#baidu"
116
+ "for more information."
117
+ "Only used for the Baidu geocoder.",
118
+ ),
119
+ cfg.StrOpt(
120
+ "bing_api_key",
121
+ help="The API key to use for the Bing geocoder."
122
+ "See https://geopy.readthedocs.io/en/latest/#bing"
123
+ "for more information."
124
+ "Only used for the Bing geocoder.",
125
+ ),
126
+ cfg.StrOpt(
127
+ "google_api_key",
128
+ help="The API key to use for the Google geocoder."
129
+ "See https://geopy.readthedocs.io/en/latest/#googlev3"
130
+ "for more information."
131
+ "Only used for the Google geocoder.",
132
+ ),
133
+ cfg.StrOpt(
134
+ "here_api_key",
135
+ help="The API key to use for the HERE geocoder."
136
+ "See https://geopy.readthedocs.io/en/latest/#here"
137
+ "for more information."
138
+ "Only used for the HERE geocoder.",
139
+ ),
140
+ cfg.StrOpt(
141
+ "opencage_api_key",
142
+ help="The API key to use for the OpenCage geocoder."
143
+ "See https://geopy.readthedocs.io/en/latest/#opencage"
144
+ "for more information."
145
+ "Only used for the OpenCage geocoder.",
146
+ ),
147
+ cfg.StrOpt(
148
+ "tomtom_api_key",
149
+ help="The API key to use for the TomTom geocoder."
150
+ "See https://geopy.readthedocs.io/en/latest/#tomtom"
151
+ "for more information."
152
+ "Only used for the TomTom geocoder.",
153
+ ),
154
+ cfg.StrOpt(
155
+ "what3words_api_key",
156
+ help="The API key to use for the What3Words geocoder."
157
+ "See https://geopy.readthedocs.io/en/latest/#what3words"
158
+ "for more information."
159
+ "Only used for the What3Words geocoder.",
160
+ ),
161
+ cfg.StrOpt(
162
+ "woosmap_api_key",
163
+ help="The API key to use for the Woosmap geocoder."
164
+ "See https://geopy.readthedocs.io/en/latest/#woosmap"
165
+ "for more information."
166
+ "Only used for the Woosmap geocoder.",
167
+ ),
168
+ ]
169
+
65
170
 
66
171
  def register_opts(config):
67
172
  config.register_group(aprsfi_group)
@@ -72,6 +177,8 @@ def register_opts(config):
72
177
  config.register_opts(owm_wx_opts, group=owm_wx_group)
73
178
  config.register_group(avwx_group)
74
179
  config.register_opts(avwx_opts, group=avwx_group)
180
+ config.register_group(location_group)
181
+ config.register_opts(location_opts, group=location_group)
75
182
 
76
183
 
77
184
  def list_opts():
@@ -80,4 +187,5 @@ def list_opts():
80
187
  query_group.name: query_plugin_opts,
81
188
  owm_wx_group.name: owm_wx_opts,
82
189
  avwx_group.name: avwx_opts,
190
+ location_group.name: location_opts,
83
191
  }