Extract some common code into telnet_spot_provier as a base class for DX cluster and RBN

This commit is contained in:
Ian Renton
2026-09-22 18:35:05 +01:00
parent 9067f545af
commit f8343dd036
11 changed files with 190 additions and 230 deletions
+117
View File
@@ -0,0 +1,117 @@
import logging
import socket
from datetime import datetime
from threading import Event, Lock, Thread
import pytz
import telnetlib3
from core.utils import decode_telnet_bytes
from providers.spot.spot_provider import SpotProvider
logger = logging.getLogger(__name__)
class TelnetSpotProvider(SpotProvider):
"""Base class for spot providers that connect to a telnet server to receive spots."""
def __init__(self, name, provider_config, host, port, login_prompt, login_response):
"""Constructor. host/port are the telnet server to connect to. login_prompt is the text to wait for before
logging in, and login_response is the callsign to send in response."""
super().__init__(name, provider_config)
self._host = host
self._port = port
self._login_prompt = login_prompt
self._login_response = login_response
self._telnet = None
self._telnet_lock = Lock()
self._thread = None
self._stop_event = Event()
def _parse_line(self, line):
"""Parse a line of telnet output and return a Spot, or None if the line did not contain a spot. Subclasses
must implement this method."""
raise NotImplementedError("Subclasses must implement this method")
def start(self):
self._thread = Thread(target=self._handle, name=f"{self.name}SpotProvider", daemon=True)
self._thread.start()
def stop(self):
self._stop_event.set()
with self._telnet_lock:
if self._telnet:
try:
self._telnet.sock.shutdown(socket.SHUT_RDWR)
except (AttributeError, OSError):
pass
self._telnet.close()
if self._thread:
self._thread.join(timeout=5)
if self._thread.is_alive():
logger.warning(f"{self.name} worker thread did not exit on time and will be killed.")
def _handle(self):
while not self._stop_event.is_set():
connected = False
while not connected and not self._stop_event.is_set():
try:
self.status = "Connecting"
logger.info(f"{self.name} connecting to {self._host}:{self._port!s}...")
new_telnet = telnetlib3.Telnet(self._host, self._port)
with self._telnet_lock:
self._telnet = new_telnet
if self._stop_event.is_set():
# stop() was called while we were connecting, close the connection rather than trying to
# read when we know it won't work
new_telnet.close()
break
self._telnet.read_until(self._login_prompt.encode("latin-1"))
self._telnet.write(f"{self._login_response}\n".encode("latin-1"))
connected = True
logger.info(f"{self.name} connected.")
except ConnectionRefusedError:
self.status = "Error"
logger.warning(f"Connection refused to {self.name} ({self._host}:{self._port!s}).")
self._stop_event.wait(timeout=300)
except Exception:
self.status = "Error"
logger.exception(f"Exception while connecting to {self.name} ({self._host}:{self._port!s}).")
self._stop_event.wait(timeout=5)
self.status = "Waiting for Data"
while connected and not self._stop_event.is_set():
try:
# Check new telnet info against regular expression
telnet_output = self._telnet.read_until("\n".encode("latin-1"))
spot = self._parse_line(decode_telnet_bytes(telnet_output))
if spot:
# Add to our list
self._submit(spot)
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logger.debug(f"Data received from {self.name}.")
except EOFError:
connected = False
if not self._stop_event.is_set():
self.status = "Restarting"
logger.warning(f"Disconnected from {self.name}. Reconnecting...")
self._stop_event.wait(timeout=5)
else:
logger.info(f"{self.name} shutting down...")
self.status = "Shutting down"
except Exception:
connected = False
if not self._stop_event.is_set():
self.status = "Error"
logger.exception(f"Exception in {self.name}")
self._stop_event.wait(timeout=5)
else:
logger.info(f"{self.name} shutting down...")
self.status = "Shutting down"
self.status = "Disconnected"