From 3addafb8b9f885cb9b710d2bf30a1329381ba179 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Thu, 2 Oct 2025 10:13:52 +0100 Subject: [PATCH] Load providers by class & module name. Closes #6 --- README.md | 16 ++++++++++++++++ config-example.yml | 26 +++++++++++++------------- main.py | 28 +++++----------------------- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c81d383..6362800 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,19 @@ Currently supports: * Parks n Peaks * RBN * APRS + +### Writing your own Providers + +(S)pothole is designed to be easily extensible. If you want to write your own provider, simply add a module to the `providers` package containing your class. (Currently, in order to be loaded correctly, the module (file) name should be the same as the class name, but lower case.) + +Your class should extend "Provider"; if it operates by polling an HTTP Server on a timer, it can instead extend "HTTPProvider" where some of the work is done for you. + +The class will need to implement a constructor that takes in the `provider_config` and provides it to the superclass constructor, while also taking any other config parameters it needs. + +If you're extending the base `Provider` class, you will need to implement `start()` and `stop()` methods that start and stop a separate thread which handles the provider's processing needs. The thread should call `submit()` or `submit_batch()` when it has one or more spots to report. + +If you're extending the `HTTPProvider` class, you will need to provide a URI to query and an interval to the superclass constructor. You'll then need to implement the `http_response_to_spots()` method which is called when new data is retrieved. Your implementation should then call `submit()` or `submit_batch()` when it has one or more spots to report. + +When constructing spots, use the comments in the Spot class and the existing implementations as an example. All parameters are optional, but you will at least want to provide a `time` (which must be timezone-aware) and a `dx_call`. + +Finally, simply add the appropriate config to the `providers` section of `config.yml`, and your provider should be instantiated on startup. diff --git a/config-example.yml b/config-example.yml index 3eb01a4..f3eaa05 100644 --- a/config-example.yml +++ b/config-example.yml @@ -8,56 +8,56 @@ server-owner-callsign: "N0CALL" # Data providers to use. This is an example set, tailor it to your liking by commenting and uncommenting. # RBN and APRS-IS are supported but have such a high data rate, you probably don't want them enabled. -# Each provider needs a type, a name, and an enabled/disabled state. Some require more config such as hostnames/IP +# Each provider needs a class, a name, and an enabled/disabled state. Some require more config such as hostnames/IP # addresses and ports. You can duplicate them if you like, e.g. to support several DX clusters. RBN uses two ports, 7000 # for CW/RTTY and 7001 for FT8, so if you want both, you need two entries, as shown below. -# Feel free to write your own provider classes! +# Feel free to write your own provider classes! There are details in the README. providers: - - type: "POTA" + class: "POTA" name: "POTA" enabled: true - - type: "SOTA" + class: "SOTA" name: "SOTA" enabled: true - - type: "WWFF" + class: "WWFF" name: "WWFF" enabled: true - - type: "WWBOTA" + class: "WWBOTA" name: "WWBOTA" enabled: true - - type: "GMA" + class: "GMA" name: "GMA" enabled: true - - type: "HEMA" + class: "HEMA" name: "HEMA" enabled: true - - type: "ParksNPeaks" + class: "ParksNPeaks" name: "ParksNPeaks" enabled: true - - type: "APRS-IS" + class: "APRSIS" name: "APRS-IS" enabled: false - - type: "DXCluster" + class: "DXCluster" name: "HRD Dx Cluster" enabled: true host: "hrd.wa9pie.net" port: 8000 - - type: "RBN" + class: "RBN" name: "RBN CW/RTTY" enabled: false port: 7000 - - type: "RBN" + class: "RBN" name: "RBN FT8" enabled: false port: 7001 diff --git a/main.py b/main.py index df45cf8..3b08150 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ import sys from datetime import datetime from time import sleep +import importlib import psutil import pytz @@ -41,30 +42,11 @@ def shutdown(sig, frame): p.stop() cleanup_timer.stop() -# Utility method to get a data provider based on its config entry. +# Utility method to get a data provider based on the class specified in its config entry. def get_provider_from_config(config_providers_entry): - match config_providers_entry["type"]: - case "POTA": - return POTA(config_providers_entry) - case "SOTA": - return SOTA(config_providers_entry) - case "WWFF": - return WWFF(config_providers_entry) - case "GMA": - return GMA(config_providers_entry) - case "WWBOTA": - return WWBOTA(config_providers_entry) - case "HEMA": - return HEMA(config_providers_entry) - case "ParksNPeaks": - return ParksNPeaks(config_providers_entry) - case "DXCluster": - return DXCluster(config_providers_entry) - case "RBN": - return RBN(config_providers_entry) - case "APRS-IS": - return APRSIS(config_providers_entry) - return None + module = importlib.import_module('providers.' + config_providers_entry["class"].lower()) + provider_class = getattr(module, config_providers_entry["class"]) + return provider_class(config_providers_entry) # Main function