Python pskr_mqtt_listen.py: - Guard against malformed CONNACK/SUBACK/PUBLISH packets - Fix _s() truthiness: is not None instead of if v (zero SNR was lost) - EINTR-safe select loop, OOB data handling, VBInt overflow check - Proper socket cleanup with try/finally + DISCONNECT on shutdown Elixir: - Log dropped spot errors in aggregator (was silently discarded) - Wire retain_scores_window into NotifyListener chain completion - Recurse sweep_tmp_dir into band/weather_scalars subdirectories - Rescue recalibrator.run/0 to write failed status row on crash - Handle nil in fmt_snr/fmt_callsigns (no more nil dB crashes) - Replace Stream.run with Enum.reduce logging exits in poll_worker - Handle File.stat race in ms_footprints prune, grid_center nil log - Fix unused variables, stale comments, missing get_path!/1 Rust: - Graceful JoinError handling in fetcher/hrdps_fetcher (no more panics) - round_to_5min returns Option (leap-second safe) - Acquire/Release ordering on shutdown flags (was Relaxed) - Parameterized NOTIFY in db.rs, defensive scalar sweep, NaN guard - OsString::push for tmp naming, clippy fixes
110 lines
3 KiB
Elixir
110 lines
3 KiB
Elixir
defmodule Microwaveprop.Commercial.PollWorker do
|
|
@moduledoc false
|
|
use Oban.Pro.Worker, queue: :commercial, max_attempts: 1
|
|
|
|
alias Microwaveprop.Commercial
|
|
alias Microwaveprop.Commercial.SnmpClient
|
|
alias Microwaveprop.Weather
|
|
alias Microwaveprop.Weather.IemClient
|
|
|
|
require Logger
|
|
|
|
@impl Oban.Pro.Worker
|
|
def process(%Oban.Job{}) do
|
|
links = Commercial.enabled_links()
|
|
|
|
poll_and_record(links, &SnmpClient.poll/3)
|
|
fetch_weather(links)
|
|
|
|
:ok
|
|
end
|
|
|
|
@spec poll_and_record([map()], function()) :: :ok
|
|
def poll_and_record(links, poll_fn) do
|
|
now = DateTime.utc_now()
|
|
Enum.each(links, &poll_link(&1, poll_fn, now))
|
|
end
|
|
|
|
defp poll_link(link, poll_fn, now) do
|
|
case poll_fn.(link.host, link.community, link.radio_type) do
|
|
{:ok, data} ->
|
|
save_sample(link, data, now)
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("SNMP poll failed for #{link.label} (#{link.host}): #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
defp save_sample(link, data, now) do
|
|
attrs =
|
|
data
|
|
|> Map.put(:link_id, link.id)
|
|
|> Map.put(:sampled_at, now)
|
|
|
|
case Commercial.create_sample(attrs) do
|
|
{:ok, _sample} ->
|
|
Logger.info("Recorded sample for #{link.label}")
|
|
|
|
{:error, changeset} ->
|
|
Logger.warning("Failed to save sample for #{link.label}: #{inspect(changeset.errors)}")
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Fetch the last hour of ASOS data for every unique weather station
|
|
referenced by the given links. Exposed (not private) so tests can
|
|
exercise the IEM fetch + upsert path without having to drive
|
|
`perform/1` through the real SNMP layer.
|
|
"""
|
|
@spec fetch_weather([Commercial.Link.t()]) :: :ok
|
|
def fetch_weather(links) do
|
|
stations =
|
|
links
|
|
|> Enum.map(& &1.weather_station)
|
|
|> Enum.reject(&is_nil/1)
|
|
|> Enum.uniq()
|
|
|
|
now = DateTime.utc_now()
|
|
start_dt = DateTime.add(now, -3600, :second)
|
|
|
|
stations
|
|
|> Task.async_stream(&fetch_station_weather(&1, start_dt, now),
|
|
timeout: :infinity,
|
|
max_concurrency: System.schedulers_online()
|
|
)
|
|
|> Enum.reduce(:ok, fn
|
|
{:ok, :ok}, acc ->
|
|
acc
|
|
|
|
{:exit, reason}, acc ->
|
|
Logger.error("PollWorker weather fetch task crashed: #{inspect(reason)}")
|
|
acc
|
|
end)
|
|
end
|
|
|
|
defp fetch_station_weather(station_code, start_dt, now) do
|
|
{:ok, station} =
|
|
Weather.find_or_create_station(%{
|
|
station_code: station_code,
|
|
station_type: "asos",
|
|
name: station_code,
|
|
lat: 0.0,
|
|
lon: 0.0
|
|
})
|
|
|
|
case IemClient.fetch_asos(station_code, start_dt, now) do
|
|
{:ok, rows} ->
|
|
ingest_asos(station, station_code, rows)
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("ASOS fetch failed for #{station_code}: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
defp ingest_asos(station, station_code, rows) do
|
|
filtered = Enum.filter(rows, & &1.observed_at)
|
|
Weather.upsert_surface_observations(station, filtered)
|
|
|
|
Logger.info("Fetched #{length(rows)} ASOS observations for #{station_code}")
|
|
end
|
|
end
|