Files
meshing-around/modules/space.py
SpudGunMan fa76a76203 BIG OLD PATCH 🍠
pz days ... haha. I hope this works.
fancy potato
2025-10-24 19:54:46 -07:00

255 lines
11 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# helper functions to get HF band conditions, DRAP X-ray flux, and sunrise/sunset times
# HF code from https://github.com/Murturtle/MeshLink
# K7MHI Kelly Keeton 2024
import requests # pip install requests
import xml.dom.minidom
from datetime import datetime
import ephem # pip install pyephem
from datetime import timezone
from modules.log import logger, getPrettyTime
from modules.settings import (latitudeValue, longitudeValue, zuluTime,
n2yoAPIKey, urlTimeoutSeconds, use_metric,
ERROR_FETCHING_DATA, NO_DATA_NOGPS, NO_ALERTS)
import math
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass", "howtall")
def hf_band_conditions():
# ham radio HF band conditions
hf_cond = ""
signalnoise = ""
band_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
if(band_cond.ok):
solarxml = xml.dom.minidom.parseString(band_cond.text)
for i in solarxml.getElementsByTagName("band"):
hf_cond += i.getAttribute("time")[0]+i.getAttribute("name") +"="+str(i.childNodes[0].data)+"\n"
hf_cond = hf_cond[:-1] # remove the last newline
for i in solarxml.getElementsByTagName("solardata"):
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
hf_cond += "\nQRN:" + signalnoise
else:
logger.error("Solar: Error fetching HF band conditions")
hf_cond = ERROR_FETCHING_DATA
return hf_cond
def solar_conditions():
# radio related solar conditions from hamsql.com
solar_cond = ""
solar_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
if(solar_cond.ok):
solar_xml = xml.dom.minidom.parseString(solar_cond.text)
for i in solar_xml.getElementsByTagName("solardata"):
solar_a_index = i.getElementsByTagName("aindex")[0].childNodes[0].data
solar_k_index = i.getElementsByTagName("kindex")[0].childNodes[0].data
solar_xray = i.getElementsByTagName("xray")[0].childNodes[0].data
solar_flux = i.getElementsByTagName("solarflux")[0].childNodes[0].data
sunspots = i.getElementsByTagName("sunspots")[0].childNodes[0].data
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
solar_cond = "A-Index: " + solar_a_index + "\nK-Index: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nSignal Noise: " + signalnoise
else:
logger.error("Solar: Error fetching solar conditions")
solar_cond = ERROR_FETCHING_DATA
return solar_cond
def drap_xray_conditions():
# DRAP X-ray flux conditions, from NOAA direct
drap_cond = ""
drap_cond = requests.get("https://services.swpc.noaa.gov/text/drap_global_frequencies.txt", timeout=urlTimeoutSeconds)
if(drap_cond.ok):
drap_list = drap_cond.text.split('\n')
x_filter = '# X-RAY Message :'
for line in drap_list:
if x_filter in line:
xray_flux = line.split(": ")[1]
else:
logger.error("Error fetching DRAP X-ray flux")
xray_flux = ERROR_FETCHING_DATA
return xray_flux
def get_sun(lat=0, lon=0):
# get sunrise and sunset times using callers location or default
obs = ephem.Observer()
obs.date = datetime.now(timezone.utc)
sun = ephem.Sun()
if lat != 0 and lon != 0:
obs.lat = str(lat)
obs.lon = str(lon)
else:
obs.lat = str(latitudeValue)
obs.lon = str(longitudeValue)
sun.compute(obs)
sun_table = {}
# get the sun azimuth and altitude
sun_table['azimuth'] = sun.az
sun_table['altitude'] = sun.alt
# sun is up include altitude
if sun_table['altitude'] > 0:
sun_table['altitude'] = sun.alt
else:
sun_table['altitude'] = 0
# get the next rise and set times
local_sunrise = ephem.localtime(obs.next_rising(sun))
local_sunset = ephem.localtime(obs.next_setting(sun))
if zuluTime:
sun_table['rise_time'] = local_sunrise.strftime('%a %d %H:%M')
sun_table['set_time'] = local_sunset.strftime('%a %d %H:%M')
else:
sun_table['rise_time'] = local_sunrise.strftime('%a %d %I:%M%p')
sun_table['set_time'] = local_sunset.strftime('%a %d %I:%M%p')
# if sunset is before sunrise, then data will be for tomorrow format sunset first and sunrise second
if local_sunset < local_sunrise:
sun_data = "SunSet: " + sun_table['set_time'] + "\nRise: " + sun_table['rise_time']
else:
sun_data = "SunRise: " + sun_table['rise_time'] + "\nSet: " + sun_table['set_time']
sun_data += "\nDaylight: " + str((local_sunset - local_sunrise).seconds // 3600) + "h " + str(((local_sunset - local_sunrise).seconds // 60) % 60) + "m"
if sun_table['altitude'] > 0:
sun_data += "\nRemaining: " + str((local_sunset - datetime.now()).seconds // 3600) + "h " + str(((local_sunset - datetime.now()).seconds // 60) % 60) + "m"
sun_data += "\nAzimuth: " + str('{0:.2f}'.format(sun_table['azimuth'] * 180 / ephem.pi)) + "°"
if sun_table['altitude'] > 0:
sun_data += "\nAltitude: " + str('{0:.2f}'.format(sun_table['altitude'] * 180 / ephem.pi)) + "°"
return sun_data
def get_moon(lat=0, lon=0):
# get moon phase and rise/set times using callers location or default
obs = ephem.Observer()
moon = ephem.Moon()
if lat != 0 and lon != 0:
obs.lat = str(lat)
obs.lon = str(lon)
else:
obs.lat = str(latitudeValue)
obs.lon = str(longitudeValue)
obs.date = datetime.now(timezone.utc)
moon.compute(obs)
moon_table = {}
illum = moon.phase # 0 = new, 50 = first/last quarter, 100 = full
if illum < 1.0:
moon_phase = 'New Moon🌑'
elif illum < 49:
moon_phase = 'Waxing Crescent 🌒'
elif 49 <= illum < 51:
moon_phase = 'First Quarter 🌓'
elif illum < 99:
moon_phase = 'Waxing Gibbous 🌔'
elif illum >= 99:
moon_phase = 'Full Moon🌕'
elif illum > 51:
moon_phase = 'Waning Gibbous 🌖'
elif 51 >= illum > 49:
moon_phase = 'Last Quarter 🌗'
else:
moon_phase = 'Waning Crescent 🌘'
moon_table['phase'] = moon_phase
moon_table['illumination'] = moon.phase
moon_table['azimuth'] = moon.az
moon_table['altitude'] = moon.alt
local_moonrise = ephem.localtime(obs.next_rising(moon))
local_moonset = ephem.localtime(obs.next_setting(moon))
if zuluTime:
moon_table['rise_time'] = local_moonrise.strftime('%a %d %H:%M')
moon_table['set_time'] = local_moonset.strftime('%a %d %H:%M')
else:
moon_table['rise_time'] = local_moonrise.strftime('%a %d %I:%M%p')
moon_table['set_time'] = local_moonset.strftime('%a %d %I:%M%p')
local_next_full_moon = ephem.localtime(ephem.next_full_moon((obs.date)))
local_next_new_moon = ephem.localtime(ephem.next_new_moon((obs.date)))
if zuluTime:
moon_table['next_full_moon'] = local_next_full_moon.strftime('%a %b %d %H:%M')
moon_table['next_new_moon'] = local_next_new_moon.strftime('%a %b %d %H:%M')
else:
moon_table['next_full_moon'] = local_next_full_moon.strftime('%a %b %d %I:%M%p')
moon_table['next_new_moon'] = local_next_new_moon.strftime('%a %b %d %I:%M%p')
moon_data = "MoonRise: " + moon_table['rise_time'] + "\nSet: " + moon_table['set_time'] + \
"\nPhase: " + moon_table['phase'] + " @: " + str('{0:.2f}'.format(moon_table['illumination'])) + "%" \
+ "\nFullMoon: " + moon_table['next_full_moon'] + "\nNewMoon: " + moon_table['next_new_moon']
# if moon is in the sky, add azimuth and altitude
if moon_table['altitude'] > 0:
moon_data += "\nAz: " + str('{0:.2f}'.format(moon_table['azimuth'] * 180 / ephem.pi)) + "°" + \
"\nAlt: " + str('{0:.2f}'.format(moon_table['altitude'] * 180 / ephem.pi)) + "°"
return moon_data
def getNextSatellitePass(satellite, lat=0, lon=0):
pass_data = ''
# get the next satellite pass for a given satellite
visualPassAPI = "https://api.n2yo.com/rest/v1/satellite/visualpasses/"
if lat == 0 and lon == 0:
lat = latitudeValue
lon = longitudeValue
# API URL
if n2yoAPIKey == '':
logger.error("System: Missing API key free at https://www.n2yo.com/login/")
return "not configured, bug your sysop"
url = visualPassAPI + str(satellite) + "/" + str(lat) + "/" + str(lon) + "/0/2/300/" + "&apiKey=" + n2yoAPIKey
# get the next pass data
try:
if not int(satellite):
raise Exception("Invalid satellite number")
next_pass_data = requests.get(url, timeout=urlTimeoutSeconds)
if(next_pass_data.ok):
pass_json = next_pass_data.json()
if 'info' in pass_json and 'passescount' in pass_json['info'] and pass_json['info']['passescount'] > 0:
satname = pass_json['info']['satname']
pass_time = pass_json['passes'][0]['startUTC']
pass_duration = pass_json['passes'][0]['duration']
pass_maxEl = pass_json['passes'][0]['maxEl']
pass_rise_time = datetime.fromtimestamp(pass_time).strftime('%a %d %I:%M%p')
pass_startAzCompass = pass_json['passes'][0]['startAzCompass']
pass_set_time = datetime.fromtimestamp(pass_time + pass_duration).strftime('%a %d %I:%M%p')
pass__endAzCompass = pass_json['passes'][0]['endAzCompass']
pass_data = f"{satname} @{pass_rise_time} Az: {pass_startAzCompass} for{getPrettyTime(pass_duration)}, MaxEl: {pass_maxEl}° Set @{pass_set_time} Az: {pass__endAzCompass}"
elif pass_json['info']['passescount'] == 0:
satname = pass_json['info']['satname']
pass_data = f"{satname} has no upcoming passes"
else:
logger.error(f"System: Error fetching satellite pass data {satellite}")
pass_data = ERROR_FETCHING_DATA
except Exception as e:
logger.warning(f"System: User supplied value {satellite} unknown or invalid")
pass_data = "Provide NORAD# example use: 🛰satpass 25544,33591"
return pass_data
def measureHeight(lat=0, lon=0, shadow=0):
# measure height of a given location using sun angle and shadow length
if lat == 0 and lon == 0:
return NO_DATA_NOGPS
if shadow == 0:
return NO_ALERTS
obs = ephem.Observer()
obs.lat = str(lat)
obs.lon = str(lon)
obs.date = datetime.now(timezone.utc)
sun = ephem.Sun()
sun.compute(obs)
sun_altitude = sun.alt * 180 / ephem.pi
if sun_altitude <= 0:
return "Sun is below horizon, I dont belive your shadow measurement"
try:
if use_metric:
height = float(shadow) * math.tan(sun.alt)
return f"📏Object Height: {height:.2f} m (Shadow: {shadow} m, 📐Sun Alt: {sun_altitude:.2f}°)"
else:
# Assume shadow is in feet if imperial, otherwise convert from meters to feet
shadow_ft = float(shadow)
height_ft = shadow_ft * math.tan(sun.alt)
return f"📏Object Height: {height_ft:.2f} ft (Shadow: {shadow_ft} ft, 📐Sun Alt: {sun_altitude:.2f}°)"
except Exception as e:
logger.error(f"Space: Error calculating height: {e}")
return NO_ALERTS