Enabled :error_handling, :unknown, :unmatched_returns, :extra_return, :missing_return in an earlier commit and landed a 129-warning baseline. Four parallel agents each fixed a directory slice: - Core contexts (29): Radio, Release, Weather, Beacons, Cache, Backtest.Features, Terrain.Srtm, Ionosphere.GiroClient, Propagation.RunTiming, Accounts.Scope, RepoListener. Fixes were (a) prefix side-effect calls (Task.start, Phoenix.PubSub, Logger, :ets.new) with _ = ; (b) tighten/widen specs that didn't match actual returns; (c) add missing @type t declarations; (d) drop dead parse_int(nil) clause. - Propagation + weather subdirs (15): FreshnessMonitor, NotifyListener, ScoreCache, ScoreCacheReconciler, Weather.FrontalAnalysis, Weather.Grib2.Extractor, Weather.Grib2.Wgrib2, GridCache, HrrrPointEnqueuer, NexradCache. Same patterns — mostly _ = on PubSub / :ets / Repo.insert_all; widened two specs (float -> number) where integer returns were reachable. - Workers (35): BackfillEnqueue, CanadianSoundingFetch, ContactImport, ContactWeatherEnqueue, GefsFetch, IemreFetch, NarrFetch, SolarIndex, TerrainProfile, WeatherFetch. Prefixed Repo.update_all / Radio.set_enrichment_status! / Weather.upsert_* side-effect calls. Fixed one :pattern_match in CanadianSoundingFetch.most_recent_sounding_time/1 where a tautological cond guard generated unreachable code. - Web + Mix tasks + lib_ml (46 of 50): controllers, LiveViews, UserAuth, and 11 mix tasks. Same prefix strategy. 4 remaining warnings originate in LiveTable.LiveResource dep macro expansion and can't be fixed without forking the dep — added .dialyzer_ignore.exs to suppress just those specific file:line pairs. Also wired ignore_warnings in mix.exs dialyzer config. mix dialyzer --format short | grep ^lib/ | wc -l -> 0 mix test: 2163 tests, 3 pre-existing flakes, 0 regressions.
127 lines
4.3 KiB
Elixir
127 lines
4.3 KiB
Elixir
defmodule Microwaveprop.Workers.CanadianSoundingFetchWorker do
|
|
@moduledoc """
|
|
Twice-daily batch fetcher for Canadian radiosonde soundings via the
|
|
University of Wyoming archive. IEM's RAOB endpoint has not been keeping
|
|
Canadian stations current (most stalled Sep 2024), so this worker fills
|
|
the gap — crucial because soundings drive the refractivity / ducting
|
|
calibration in `SoundingParams.derive/1`.
|
|
|
|
Finds every `weather_stations` row of type `sounding` whose `station_code`
|
|
starts with `C` (Canadian ICAO prefix), drops the leading `C` to get the
|
|
UWYO 3-letter code, fetches the latest available 00Z or 12Z sounding,
|
|
and upserts it into `soundings`.
|
|
"""
|
|
|
|
use Oban.Worker,
|
|
queue: :weather,
|
|
max_attempts: 3,
|
|
unique: [period: 3600, states: [:available, :scheduled, :executing, :retryable]]
|
|
|
|
import Ecto.Query
|
|
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Weather
|
|
alias Microwaveprop.Weather.SoundingParams
|
|
alias Microwaveprop.Weather.Station
|
|
alias Microwaveprop.Weather.UwyoSoundingClient
|
|
|
|
require Logger
|
|
|
|
# UWYO publishes ~90 minutes after the synoptic hour
|
|
@publish_delay_seconds 5_400
|
|
|
|
@impl Oban.Worker
|
|
def perform(%Oban.Job{args: args}) do
|
|
sounding_time =
|
|
case args do
|
|
%{"sounding_time" => iso} when is_binary(iso) ->
|
|
{:ok, dt, _} = DateTime.from_iso8601(iso)
|
|
DateTime.truncate(dt, :second)
|
|
|
|
_ ->
|
|
most_recent_sounding_time(DateTime.utc_now())
|
|
end
|
|
|
|
stations = canadian_sounding_stations()
|
|
|
|
Logger.info("CanadianSoundings: fetching #{length(stations)} stations for #{DateTime.to_iso8601(sounding_time)}")
|
|
|
|
Enum.each(stations, &fetch_station(&1, sounding_time))
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Return the most recently-publishable 00Z or 12Z sounding time, given
|
|
the current UTC instant. UWYO needs ~90 minutes after launch before the
|
|
decoded profile lands on the site, so we step back from synoptic hour
|
|
until we find one that's at least 90 minutes old.
|
|
"""
|
|
@spec most_recent_sounding_time(DateTime.t()) :: DateTime.t()
|
|
def most_recent_sounding_time(%DateTime{} = now) do
|
|
cutoff = DateTime.add(now, -@publish_delay_seconds, :second)
|
|
|
|
if cutoff.hour >= 12 do
|
|
%{cutoff | hour: 12, minute: 0, second: 0, microsecond: {0, 0}}
|
|
else
|
|
%{cutoff | hour: 0, minute: 0, second: 0, microsecond: {0, 0}}
|
|
end
|
|
end
|
|
|
|
# ---------- Internal ----------
|
|
|
|
defp canadian_sounding_stations do
|
|
Repo.all(from(s in Station, where: s.station_type == "sounding" and ilike(s.station_code, "c%")))
|
|
end
|
|
|
|
defp fetch_station(%Station{station_code: code} = station, sounding_time) do
|
|
uwyo_code = String.slice(code, 1..-1//1)
|
|
|
|
if Weather.has_sounding?(station.id, sounding_time) do
|
|
Logger.debug("CanadianSoundings: #{code} already has sounding for #{sounding_time}")
|
|
:ok
|
|
else
|
|
do_fetch(station, code, uwyo_code, sounding_time)
|
|
end
|
|
end
|
|
|
|
defp do_fetch(station, code, uwyo_code, sounding_time) do
|
|
case UwyoSoundingClient.fetch_sounding(uwyo_code, sounding_time) do
|
|
{:ok, [%{profile: profile} | _]} when profile != [] ->
|
|
attrs = build_attrs(sounding_time, profile)
|
|
_ = Weather.upsert_sounding(station, attrs)
|
|
|
|
Logger.info("CanadianSoundings: #{code} ingested sounding with #{length(profile)} levels")
|
|
|
|
{:ok, _} ->
|
|
Logger.info("CanadianSoundings: #{code} no data for #{sounding_time}")
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("CanadianSoundings: #{code} failed: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
defp build_attrs(sounding_time, profile) do
|
|
base = %{observed_at: sounding_time, profile: profile, level_count: length(profile)}
|
|
|
|
case SoundingParams.derive(profile) do
|
|
nil ->
|
|
base
|
|
|
|
params ->
|
|
Map.merge(base, %{
|
|
surface_pressure_mb: params.surface_pressure_mb,
|
|
surface_temp_c: params.surface_temp_c,
|
|
surface_dewpoint_c: params.surface_dewpoint_c,
|
|
surface_refractivity: params.surface_refractivity,
|
|
min_refractivity_gradient: params.min_refractivity_gradient,
|
|
boundary_layer_depth_m: params.boundary_layer_depth_m,
|
|
precipitable_water_mm: params.precipitable_water_mm,
|
|
k_index: params.k_index,
|
|
lifted_index: params.lifted_index,
|
|
ducting_detected: params.ducting_detected,
|
|
duct_characteristics: params.duct_characteristics
|
|
})
|
|
end
|
|
end
|
|
end
|