From c2a7f060491b386528f1af22412d165389135729 Mon Sep 17 00:00:00 2001 From: Joerg Schultze-Lutter Date: Sat, 29 May 2021 00:12:11 +0200 Subject: [PATCH] Initial release based on DB4BIN's latest requirements. Includes: - bug fix to predict.habhub.org's parser (negative lat/lon was not detected) - adds support for parsing radiosondy.info data - improved output formatting --- .gitignore | 2 + README.md | 33 ++++++- radiobot.py | 209 +++++++++++++++++++++++++++++++++++++----- radiosonde_modules.py | 66 +++++++------ 4 files changed, 250 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 90dd0ce..9bee031 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ radiosonde.cfg +.idea +venv diff --git a/README.md b/README.md index 5e4bb7b..da8d42a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,35 @@ # radiosonde-telegram-bot Radiosonde landing prediction Telegram bot -Scavenges multiple MPAD functions in order to provide an easier testing access for DB4BIN. "radiosonde_modules.py" is 100% identical with MPAD's source -TEST PURPOSES ONLY :-) +## Usage +Use the command ```/sonde [radiosonde]``` for running your prediction on a specific radiosonde. The bot will use the given radiosonde ID and run queries on the following web sites: + +- predict.habhub.org +- radiosondy.info + +Each site gets queried individually. + +## Dependencies + +### Python packages + +- [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) +- [activesoup](https://github.com/jelford/activesoup) required: version 0.2.3 or greater +- [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/) +- [geopy](https://github.com/geopy/geopy) +- [requests](https://github.com/psf/requests) +- [xmltodict](https://github.com/martinblech/xmltodict) + +### Web sites + +- predict.habhub.org +- radiosondy.info +- aprs.fi + +### API access keys + +- aprs.fi API access key +- your own telegram API access key + +(both need to be stored in the ```radiosonde.cfg``` file) \ No newline at end of file diff --git a/radiobot.py b/radiobot.py index 66c2608..9856d1a 100644 --- a/radiobot.py +++ b/radiobot.py @@ -1,8 +1,12 @@ -# Telegram Bot zum Testen der Radiosonde Landing Prediction für DB4BIN -# (jede Menge recycelter Code aus MPAD) -# "radiosonde_modules" ist dabei 1:1 austauschbar -# -# Ziel: besseres Testen für Ingo :-) +#!/opt/local/bin/python +# +# Telegram Bot "Radiosonde Landing Prediction" +# Uses "radiosonde_modules" from the MPAD project +# (file is 100% identical). +# +# This is mainly a web site scraper which uses predict.habhub.org +# and radiosondy.info as data sources +# # Author: Joerg Schultze-Lutter, 2020 # # This program is free software; you can redistribute it and/or modify @@ -20,21 +24,23 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # -import requests -import json import logging from telegram.ext import Updater from telegram.ext import CommandHandler +from telegram import ParseMode from telegram.ext import MessageHandler, Filters from utility_modules import read_program_config -from radiosonde_modules import get_radiosonde_landing_prediction +from radiosonde_modules import get_radiosonde_landing_prediction, get_radiosondy_data from geopy_modules import get_reverse_geopy_data import sys import signal -logging.basicConfig(level=logging.INFO, format="%(asctime)s %(module)s -%(levelname)s- %(message)s") +logging.basicConfig( + level=logging.INFO, format="%(asctime)s %(module)s -%(levelname)s- %(message)s" +) logger = logging.getLogger(__name__) + def signal_term_handler(signal_number, frame): """ Signal handler for SIGTERM signals. Ensures that the program @@ -55,25 +61,184 @@ def signal_term_handler(signal_number, frame): logger.info(msg="Received SIGTERM; forcing clean program exit") sys.exit(0) + def start(update, context): - context.bot.send_message(chat_id=update.effective_chat.id, text="Ich bin ein Testbot für DB4BIN") + context.bot.send_message( + chat_id=update.effective_chat.id, + text="73 de DF1JSL's/DB4BIN's Telegram radiosonde landing prediction bot", + ) + context.bot.send_message( + chat_id=update.effective_chat.id, + text="Use command
/sonde [radiosonde-id]
for requesting the landing prediction information", + parse_mode=ParseMode.HTML, + ) + context.bot.send_message( + chat_id=update.effective_chat.id, + text="Source code & further info: https://www.github.com/joergschultzelutter/radiosonde-telegram-bot", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + ) + def sonde(update, context): for sonde in context.args: sonde = sonde.upper() if len(sonde) > 0: - success, lat, lon, timestamp = get_radiosonde_landing_prediction(aprsfi_callsign=sonde,aprsdotfi_api_key=aprsdotfi_api_key) + found_something = False + # Run the query on habhub.org + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Querying position data for '{sonde}' on
habhub.org
", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + ) + ( + success, + lat, + lon, + timestamp, + landing_url, + ) = get_radiosonde_landing_prediction( + aprsfi_callsign=sonde, aprsdotfi_api_key=aprsdotfi_api_key + ) if success: - context.bot.send_message(chat_id=update.effective_chat.id, text=f"Landevorhersage: Latitude = {lat}, Longitude={lon}, Landezeit ={timestamp.strftime('%d-%b-%Y %H:%M:%S')} UTC") - context.bot.send_message(chat_id=update.effective_chat.id, text=f"https://maps.google.com/?q={lat},{lon}") - success, address = get_reverse_geopy_data(latitude=lat,longitude=lon,language="de") - if success: - context.bot.send_message(chat_id=update.effective_chat.id, text=address) + found_something = True + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Habhub information for {sonde}", + parse_mode=ParseMode.HTML, + ) + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Landing prediction: landing time ={timestamp.strftime('%d-%b-%Y %H:%M:%S')} UTC, latitude = {lat}, longitude={lon} (Google Maps link)", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + ) + success, address = get_reverse_geopy_data(latitude=lat, longitude=lon) + if success and address: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Address: {address}", + parse_mode=ParseMode.HTML, + ) else: - context.bot.send_message(chat_id=update.effective_chat.id, text=f"Sorry, nix gefunden zu '{sonde}'") + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"`Habhub did not provide any data for '{sonde}'`", + parse_mode=ParseMode.HTML, + ) + # Run the query on radiosondy.info + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Querying position data for '{sonde}' on
radiosondy.info
- this might take a while
", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + ) + success, radiosondy_response_data = get_radiosondy_data(sonde_id=sonde) + if success: + found_something = True + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Radiosondy information for '{sonde}'", + parse_mode=ParseMode.HTML, + ) + launch_site = radiosondy_response_data["launch_site"] + if launch_site: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Launch Site: {launch_site}", + parse_mode=ParseMode.HTML, + ) + probe_status = radiosondy_response_data["probe_status"] + if probe_status: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Probe Status: {probe_status}", + parse_mode=ParseMode.HTML, + ) + landing_point_latitude = radiosondy_response_data[ + "landing_point_latitude" + ] + landing_point_longitude = radiosondy_response_data[ + "landing_point_longitude" + ] + if landing_point_latitude != 0.0 and landing_point_longitude != 0.0: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f'Landing point: Lat {landing_point_latitude} / Lon {landing_point_longitude} (Google Maps link)', + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + ) + success, address = get_reverse_geopy_data( + latitude=landing_point_latitude, + longitude=landing_point_longitude, + ) + if success and address: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Landing point address data: {address}", + parse_mode=ParseMode.HTML, + ) + else: + landing_point = radiosondy_response_data["landing_point"] + if landing_point: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Landing Point raw coordinates: {landing_point}", + parse_mode=ParseMode.HTML, + ) + landing_description = radiosondy_response_data["landing_description"] + if landing_description: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Landing description: {landing_description}", + parse_mode=ParseMode.HTML, + ) + latitude = radiosondy_response_data["latitude"] + longitude = radiosondy_response_data["longitude"] + if latitude and longitude: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f'Last coordinates on
aprs.fi
: Lat {latitude} / Lon {longitude} (Google Maps link)', + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + ) + success, address = get_reverse_geopy_data( + latitude=latitude, longitude=longitude + ) + if success and address: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"
aprs.fi
address data:
{address}", + parse_mode=ParseMode.HTML, + ) + else: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"
Radiosondy.info
did not provide any data for '{sonde}'
", + parse_mode=ParseMode.HTML, + ) + if not found_something: + context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"Didn't find anything on radiosonde '{sonde}'", + ) + def unknown(update, context): - context.bot.send_message(chat_id=update.effective_chat.id, text="Nuschel nicht so. Ich verstehe Dich nicht") + context.bot.send_message(chat_id=update.effective_chat.id, text="Unknown command.") + context.bot.send_message( + chat_id=update.effective_chat.id, + text="Use command
/sonde [radiosonde-id]
for requesting the landing prediction information", + parse_mode=ParseMode.HTML, + ) + context.bot.send_message( + chat_id=update.effective_chat.id, + text="Source code & further info: https://www.github.com/joergschultzelutter/radiosonde-telegram-bot", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + ) + if __name__ == "__main__": success, aprsdotfi_api_key, telegram_token = read_program_config() @@ -88,10 +253,10 @@ def unknown(update, context): updater = Updater(token=telegram_token, use_context=True) dispatcher = updater.dispatcher - start_handler = CommandHandler('start', start) + start_handler = CommandHandler("start", start) dispatcher.add_handler(start_handler) - sonde_handler = CommandHandler('sonde', sonde) + sonde_handler = CommandHandler("sonde", sonde) dispatcher.add_handler(sonde_handler) # must be last handler prior to polling start @@ -105,6 +270,4 @@ def unknown(update, context): msg="KeyboardInterrupt or SystemExit in progress; shutting down ..." ) updater.stop() - logger.info( - msg="Have terminated the updater" - ) + logger.info(msg="Have terminated the updater") diff --git a/radiosonde_modules.py b/radiosonde_modules.py index 2a34b8a..091770a 100644 --- a/radiosonde_modules.py +++ b/radiosonde_modules.py @@ -184,6 +184,8 @@ def get_kml_data_from_habhub( if "valid" in json_content: valid = json_content["valid"] + logger.info("Have received valid initial response from Habhub") + # Everything seems to be okay so let's get the UUID (if present) if valid == "true": if "uuid" in json_content: @@ -204,6 +206,8 @@ def get_kml_data_from_habhub( except: kml_dict = {} + logger.info("Have received valid uuid response from Habhub") + # Now navigate through the structure and get our data # The stuff that we want is in the "Placemark" subsection if "kml" in kml_dict: @@ -226,7 +230,7 @@ def get_kml_data_from_habhub( ] # run some regex magic for extracting what we want - regex_string = r"^Balloon landing at (\d*[.]\d*),\s*(\d*[.]\d*)\s*at\s*(\d*[:]\d* \d{2}\/\d{2}\/\d{4}).$" + regex_string = r"^Balloon landing at (-?\d*[.]\d*),\s*(-?\d*[.]\d*)\s*at\s*(\d*[:]\d* \d{2}\/\d{2}\/\d{4}).$" matches = re.search( pattern=regex_string, string=description, @@ -316,12 +320,15 @@ def get_radiosonde_landing_prediction(aprsfi_callsign: str, aprsdotfi_api_key: s # aprs_target_type="o", ) + logger.info("Running query on aprs.fi") + # We found the entry - so let's continue if success: if comment: # logger.info(comment) clmb = get_clmb_from_comment(probe_comment=comment) if clmb: + logger.info("Getting KML data from Habhub") ( success, landing_latitude, @@ -367,20 +374,13 @@ def get_radiosondy_data(sonde_id: str): headers = {"User-Agent": "Mozilla"} # Init our target variables - this is the data that will be returned to the user - launch_site = ( - probe_type - ) = ( - probe_aux - ) = ( - probe_freq - ) = ( - probe_status - ) = probe_finder = landing_point = landing_description = changes_made = None - receiver = ( - sonde_number - ) = ( - datetime_utc - ) = latitude = longitude = course_deg = speed_kmh = altitude_m = aprs_comment = None + launch_site = probe_type = probe_aux = probe_freq = None + probe_status = probe_finder = landing_point = None + landing_point_latitude = landing_point_longitude = 0.0 + landing_description = changes_made = None + receiver = sonde_number = datetime_utc = None + latitude = longitude = course_deg = speed_kmh = None + altitude_m = aprs_comment = None climbing = temperature = pressure = humidity = aux_o3 = None # general success / failure boolean @@ -421,6 +421,19 @@ def get_radiosondy_data(sonde_id: str): landing_point = cols[6].string landing_description = cols[7].string changes_made = cols[8].string + + regex_string = r"^(-?\d*[.]\d*),\s*(-?\d*[.]\d*)$" + matches = re.search( + pattern=regex_string, + string=landing_point, + flags=re.IGNORECASE, + ) + if matches: + try: + landing_point_latitude = float(matches[1]) + landing_point_longitude = float(matches[2]) + except ValueError: + landing_point_longitude = landing_point_longitude = 0.0 else: # This branch gets executed in case the probe's status has never changed since its inception regex_string = r"images\/balloon.png\"\> Number: ([\w\s]+)\<\/h4\>" @@ -619,6 +632,8 @@ def get_radiosondy_data(sonde_id: str): "probe_status": probe_status, "probe_finder": probe_finder, "landing_point": landing_point, + "landing_point_latitude": landing_point_latitude, + "landing_point_longitude": landing_point_longitude, "landing_description": landing_description, "changes_made": changes_made, "receiver": receiver, @@ -640,23 +655,4 @@ def get_radiosondy_data(sonde_id: str): if __name__ == "__main__": - ( - success, - aprsdotfi_api_key, - openweathermapdotorg_api_key, - aprsis_callsign, - aprsis_passcode, - dapnet_callsign, - dapnet_passcode, - smtpimap_email_address, - smtpimap_email_password, - ) = read_program_config() - if success: - logger.info( - pformat( - get_radiosonde_landing_prediction( - aprsfi_callsign="D19031453", aprsdotfi_api_key=aprsdotfi_api_key - ) - ) - ) - logger.info(pformat(get_radiosondy_data(sonde_id="D19031453"))) + logger.info(pformat(get_radiosondy_data(sonde_id="R2420139")))