add files
This commit is contained in:
parent
de9bb1bdc2
commit
e56b291ee6
|
@ -1,3 +1,5 @@
|
||||||
|
.idea
|
||||||
|
|
||||||
# ---> Python
|
# ---> Python
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
@ -15,7 +17,6 @@ dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
@ -159,4 +160,3 @@ cython_debug/
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import datetime
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import chromedriver_autoinstaller
|
||||||
|
import requests
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver import Keys
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
|
||||||
|
IONEX_BASE_URL = 'https://cddis.nasa.gov/archive/gnss/products/ionex/'
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_latest_ionex(username: str, password: str):
|
||||||
|
now = datetime.date.today()
|
||||||
|
url = IONEX_BASE_URL + str(now.year)
|
||||||
|
|
||||||
|
chromedriver_autoinstaller.install()
|
||||||
|
options = Options()
|
||||||
|
options.add_argument('--headless=new')
|
||||||
|
driver = webdriver.Chrome(options=options)
|
||||||
|
driver.get(url)
|
||||||
|
|
||||||
|
# Login
|
||||||
|
username_field = WebDriverWait(driver, 30).until(EC.presence_of_element_located((By.ID, "username")))
|
||||||
|
username_field.clear()
|
||||||
|
username_field.send_keys(username)
|
||||||
|
password_field = WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "password")))
|
||||||
|
password_field.clear()
|
||||||
|
password_field.send_keys(password)
|
||||||
|
password_field.send_keys(Keys.RETURN)
|
||||||
|
|
||||||
|
# Wait until we're redirected to the right page.
|
||||||
|
WebDriverWait(driver, 30).until(EC.visibility_of_element_located((By.ID, "parDirTextContainer")))
|
||||||
|
|
||||||
|
# Get the days in the year.
|
||||||
|
day_elements = driver.find_elements(By.XPATH, '//div[@class="archiveDir"]/div[@class="archiveDirTextContainer"]/a[@class="archiveDirText"]')
|
||||||
|
day_urls = [element.get_attribute('href') for element in day_elements]
|
||||||
|
|
||||||
|
# Load the latest day.
|
||||||
|
driver.get(day_urls[-1])
|
||||||
|
|
||||||
|
# Find our file.
|
||||||
|
file_elements = driver.find_elements(By.XPATH, '//a[@class="archiveItemText"]')
|
||||||
|
file_urls = [element.get_attribute('href') for element in file_elements]
|
||||||
|
found_url = None
|
||||||
|
for u in file_urls:
|
||||||
|
parts = u.split('/')
|
||||||
|
if parts[-1].startswith('c2pg'):
|
||||||
|
found_url = u
|
||||||
|
break
|
||||||
|
if found_url is None:
|
||||||
|
print('Did not find c2pg')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Download our file.
|
||||||
|
auth_cookie = None
|
||||||
|
for cookie in driver.get_cookies():
|
||||||
|
if cookie['name'] == 'ProxyAuth':
|
||||||
|
auth_cookie = cookie['value']
|
||||||
|
break
|
||||||
|
if auth_cookie is None:
|
||||||
|
print('Did not find ProxyAuth cookie')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
driver.close()
|
||||||
|
del driver
|
||||||
|
|
||||||
|
# Download data.
|
||||||
|
zip_data_r = requests.get(found_url, cookies={'ProxyAuth': auth_cookie})
|
||||||
|
zip_data_r.raise_for_status()
|
||||||
|
|
||||||
|
# Read data.
|
||||||
|
tmp_file = tempfile.NamedTemporaryFile()
|
||||||
|
tmp_file.write(zip_data_r.content)
|
||||||
|
tmp_dir = tempfile.TemporaryDirectory()
|
||||||
|
subprocess.run(["7z", "e", tmp_file.name, f"-o{tmp_dir.name}"], check=True, stdout=subprocess.PIPE)
|
||||||
|
p = Path(tmp_dir.name)
|
||||||
|
target_file = list(p.iterdir())[-1]
|
||||||
|
data = target_file.read_text()
|
||||||
|
return data
|
|
@ -0,0 +1,68 @@
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import cartopy.crs as ccrs
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
"""
|
||||||
|
https://github.com/daniestevez/jupyter_notebooks/blob/master/IONEX.ipynb
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ionex_datetime(s: str):
|
||||||
|
match = re.match(r'\s*(\d{4})\s*(\d{1,2})\s*(\d{1,2})\s*(\d{1,2})\s*(\d{1,2})\s*(\d{1,2})', s)
|
||||||
|
if match:
|
||||||
|
year, month, day, hour, minute, second = map(int, match.groups())
|
||||||
|
return datetime(year, month, day, hour, minute, second)
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid date format")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_map(tecmap, exponent=-1):
|
||||||
|
tecmap = re.split('.*END OF TEC MAP', tecmap)[0]
|
||||||
|
return np.stack([np.fromstring(l, sep=' ') for l in re.split('.*LAT/LON1/LON2/DLON/H\\n', tecmap)[1:]]) * 10 ** exponent
|
||||||
|
|
||||||
|
|
||||||
|
def get_tecmaps(ionex: str):
|
||||||
|
for tecmap in ionex.split('START OF TEC MAP')[1:]:
|
||||||
|
lines = tecmap.split('\n')
|
||||||
|
epoch = lines[1].strip() if len(lines) > 1 else None
|
||||||
|
yield parse_map(tecmap), epoch
|
||||||
|
|
||||||
|
|
||||||
|
def plot_tec_map(tecmap, lon_range: list, lat_range: list):
|
||||||
|
proj = ccrs.PlateCarree()
|
||||||
|
f, ax = plt.subplots(1, 1, subplot_kw=dict(projection=proj))
|
||||||
|
|
||||||
|
# Create arrays of latitudes and longitudes to match the geographical grid of the TEC map data.
|
||||||
|
# This is hard coded and should never change.
|
||||||
|
lat = np.arange(-87.5, 87.5, 2.5)
|
||||||
|
lon = np.arange(-180, 180, 2.5)
|
||||||
|
|
||||||
|
# Create a mask for the data in the lat/lon range
|
||||||
|
lon_mask = (lon >= lon_range[0]) & (lon <= lon_range[1])
|
||||||
|
lat_mask = (lat >= lat_range[0]) & (lat <= lat_range[1])
|
||||||
|
mask = np.ix_(lat_mask, lon_mask)
|
||||||
|
|
||||||
|
# Select only the data in the lat/lon range
|
||||||
|
tecmap_ranged = tecmap[mask]
|
||||||
|
|
||||||
|
# Plot the TEC map
|
||||||
|
h = plt.imshow(tecmap_ranged, cmap='viridis', vmin=0, vmax=100, extent=(lon_range[0], lon_range[1], lat_range[0], lat_range[1]), transform=proj)
|
||||||
|
|
||||||
|
# Make graph pretty
|
||||||
|
# ax.coastlines()
|
||||||
|
# plt.title('VTEC map')
|
||||||
|
# divider = make_axes_locatable(ax)
|
||||||
|
# ax_cb = divider.new_horizontal(size='5%', pad=0.1, axes_class=plt.Axes)
|
||||||
|
# f.add_axes(ax_cb)
|
||||||
|
# cb = plt.colorbar(h, cax=ax_cb)
|
||||||
|
# plt.rc('text', usetex=True)
|
||||||
|
# cb.set_label('TECU ($10^{16} \\mathrm{el}/\\mathrm{m}^2$)')
|
||||||
|
# plt.show()
|
||||||
|
|
||||||
|
# Deallocate
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
return tecmap_ranged
|
|
@ -0,0 +1,68 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
from lib.cddis_fetch import fetch_latest_ionex
|
||||||
|
from lib.tecmap import get_tecmaps, plot_tec_map
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
MQTT_BROKER_HOST = os.getenv('MQTT_BROKER_HOST', "")
|
||||||
|
MQTT_BROKER_PORT = int(os.getenv('MQTT_BROKER_PORT', 1883))
|
||||||
|
MQTT_CLIENT_ID = os.getenv('MQTT_CLIENT_ID', "space_weather")
|
||||||
|
MQTT_USERNAME = os.getenv('MQTT_USERNAME', "")
|
||||||
|
MQTT_PASSWORD = os.getenv('MQTT_PASSWORD', "")
|
||||||
|
MQTT_TOPIC_PREFIX = os.getenv('MQTT_TOPIC_PREFIX', "space-weather")
|
||||||
|
|
||||||
|
LAT_RANGE_MIN = os.getenv('LAT_RANGE_MIN')
|
||||||
|
LAT_RANGE_MAX = os.getenv('LAT_RANGE_MAX')
|
||||||
|
LON_RANGE_MIN = os.getenv('LON_RANGE_MIN')
|
||||||
|
LON_RANGE_MAX = os.getenv('LON_RANGE_MAX')
|
||||||
|
if not LAT_RANGE_MIN or not LAT_RANGE_MAX or not LON_RANGE_MIN or not LON_RANGE_MAX:
|
||||||
|
logging.critical('Must set LAT_RANGE_MIN, LAT_RANGE_MAX, LON_RANGE_MIN, and LON_RANGE_MAX environment variables')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
CDDIS_USERNAME = os.getenv('CDDIS_USERNAME')
|
||||||
|
CDDIS_PASSWORD = os.getenv('CDDIS_PASSWORD')
|
||||||
|
if not CDDIS_USERNAME or not CDDIS_PASSWORD:
|
||||||
|
logging.critical('Must set CDDIS_USERNAME and CDDIS_PASSWORD environment variables')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
client = mqtt.Client(client_id=MQTT_CLIENT_ID)
|
||||||
|
if MQTT_USERNAME and MQTT_PASSWORD:
|
||||||
|
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
|
||||||
|
logging.info("Username and password set.")
|
||||||
|
client.will_set(MQTT_TOPIC_PREFIX + "/status", payload="Offline", qos=1, retain=True) # set LWT
|
||||||
|
client.connect(MQTT_BROKER_HOST, port=MQTT_BROKER_PORT)
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
|
|
||||||
|
def publish(topic: str, msg):
|
||||||
|
topic_expanded = MQTT_TOPIC_PREFIX + '/' + topic
|
||||||
|
result = client.publish(topic_expanded, msg)
|
||||||
|
status = result[0]
|
||||||
|
if status == 0:
|
||||||
|
logging.info(f"Sent {msg} to topic {topic_expanded}")
|
||||||
|
else:
|
||||||
|
logging.error(f"Failed to send message to topic {topic_expanded}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
while True:
|
||||||
|
logging.info('Fetching latest IONEX data')
|
||||||
|
ionex_data = fetch_latest_ionex(CDDIS_USERNAME, CDDIS_PASSWORD)
|
||||||
|
tec_data = []
|
||||||
|
for tecmap, epoch in get_tecmaps(ionex_data):
|
||||||
|
avg_tec = np.mean(plot_tec_map(tecmap, [float(LON_RANGE_MIN), float(LON_RANGE_MAX)], [float(LAT_RANGE_MIN), float(LAT_RANGE_MAX)]))
|
||||||
|
tec_data.append(avg_tec)
|
||||||
|
daily_avg = round(np.mean(tec_data), 1)
|
||||||
|
publish('vtec', daily_avg)
|
||||||
|
time.sleep(1800)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,7 @@
|
||||||
|
paho-mqtt==1.5.0
|
||||||
|
chromedriver-autoinstaller==0.6.4
|
||||||
|
selenium==4.23.1
|
||||||
|
requests==2.32.3
|
||||||
|
matplotlib==3.9.2
|
||||||
|
cartopy==0.23.0
|
||||||
|
numpy==2.0.1
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Space Weather VTEC
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=homeassistant
|
||||||
|
EnvironmentFile=/etc/secrets/space-weather
|
||||||
|
ExecStart=/srv/space-weather/venv/bin/python /srv/space-weather/ha-noaa-space-weather-sensor/main.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
Loading…
Reference in New Issue