Store surface observations (ASOS) and upper-air soundings (RAOB) alongside QSOs for atmospheric propagation correlation. Three new tables: weather_stations, surface_observations, and soundings with JSONB profiles and pre-computed derived parameters (refractivity, gradients, duct detection, stability indices). Includes IEM API client for historical data import and import script seeded with 95 ASOS + 9 sounding stations from PropCast coverage area.
119 lines
3.4 KiB
Elixir
119 lines
3.4 KiB
Elixir
defmodule Microwaveprop.Weather.IemClientTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Weather.IemClient
|
|
|
|
describe "parse_asos_csv/1" do
|
|
test "parses valid CSV rows" do
|
|
csv = """
|
|
station,valid,tmpf,dwpf,relh,sknt,drct,mslp,alti,skyc1
|
|
KDFW,2026-03-28 18:53,75.0,55.0,49.12,12,180,1013.2,29.92,SCT
|
|
KDFW,2026-03-28 19:53,76.0,54.0,46.00,10,190,1013.0,29.91,FEW
|
|
"""
|
|
|
|
rows = IemClient.parse_asos_csv(csv)
|
|
assert length(rows) == 2
|
|
|
|
first = hd(rows)
|
|
assert first.temp_f == 75.0
|
|
assert first.dewpoint_f == 55.0
|
|
assert first.relative_humidity == 49.12
|
|
assert first.wind_speed_kts == 12.0
|
|
assert first.wind_direction_deg == 180
|
|
assert first.sea_level_pressure_mb == 1013.2
|
|
assert first.altimeter_setting == 29.92
|
|
assert first.sky_condition == "SCT"
|
|
assert first.observed_at == ~U[2026-03-28 18:53:00Z]
|
|
end
|
|
|
|
test "handles null values in CSV" do
|
|
csv = """
|
|
station,valid,tmpf,dwpf,relh,sknt,drct,mslp,alti,skyc1
|
|
KDFW,2026-03-28 18:53,null,null,null,null,null,null,null,null
|
|
"""
|
|
|
|
rows = IemClient.parse_asos_csv(csv)
|
|
assert length(rows) == 1
|
|
|
|
first = hd(rows)
|
|
assert first.temp_f == nil
|
|
assert first.dewpoint_f == nil
|
|
assert first.sky_condition == nil
|
|
end
|
|
|
|
test "skips comment lines and empty lines" do
|
|
csv = """
|
|
#DEBUG something
|
|
station,valid,tmpf,dwpf,relh,sknt,drct,mslp,alti,skyc1
|
|
KDFW,2026-03-28 18:53,75.0,55.0,49.0,12,180,1013.2,29.92,SCT
|
|
|
|
"""
|
|
|
|
rows = IemClient.parse_asos_csv(csv)
|
|
assert length(rows) == 1
|
|
end
|
|
end
|
|
|
|
describe "parse_raob_json/1" do
|
|
test "parses valid RAOB JSON response" do
|
|
json = %{
|
|
"profiles" => [
|
|
%{
|
|
"station" => "KFWD",
|
|
"valid" => "2026-03-28 12:00:00+00:00",
|
|
"profile" => [
|
|
%{"pres" => 1013.0, "hght" => 171.0, "tmpc" => 25.0, "dwpc" => 15.0, "drct" => 180.0, "sknt" => 10.0},
|
|
%{"pres" => 925.0, "hght" => 800.0, "tmpc" => 20.0, "dwpc" => 12.0, "drct" => 200.0, "sknt" => 15.0}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
result = IemClient.parse_raob_json(json)
|
|
assert length(result) == 1
|
|
|
|
sounding = hd(result)
|
|
assert sounding.observed_at == ~U[2026-03-28 12:00:00Z]
|
|
assert length(sounding.profile) == 2
|
|
assert hd(sounding.profile)["pres"] == 1013.0
|
|
end
|
|
|
|
test "returns empty list for empty profiles" do
|
|
json = %{"profiles" => []}
|
|
assert IemClient.parse_raob_json(json) == []
|
|
end
|
|
|
|
test "handles missing profile key gracefully" do
|
|
json = %{}
|
|
assert IemClient.parse_raob_json(json) == []
|
|
end
|
|
end
|
|
|
|
describe "asos_url/3" do
|
|
test "builds correct URL for date range" do
|
|
url =
|
|
IemClient.asos_url(
|
|
"KDFW",
|
|
~U[2026-03-28 12:00:00Z],
|
|
~U[2026-03-28 18:00:00Z]
|
|
)
|
|
|
|
assert url =~ "station=KDFW"
|
|
assert url =~ "year1=2026"
|
|
assert url =~ "month1=3"
|
|
assert url =~ "day1=28"
|
|
assert url =~ "hour1=12"
|
|
assert url =~ "year2=2026"
|
|
assert url =~ "hour2=18"
|
|
assert url =~ "format=onlycomma"
|
|
end
|
|
end
|
|
|
|
describe "raob_url/2" do
|
|
test "builds correct URL for sounding fetch" do
|
|
url = IemClient.raob_url("FWD", ~U[2026-03-28 12:00:00Z])
|
|
assert url =~ "station=FWD"
|
|
assert url =~ "ts=202603281200"
|
|
end
|
|
end
|
|
end
|