prop/test/microwaveprop/beacons/range_estimate_test.exs
Graham McIntire a5b3f1f3da Beacon detail map with range estimate and on_the_air flag
- Add on_the_air boolean to beacons (default true); surfaced as a
  checkbox on the form, a badge on the index, and in the detail list.
- Render a Leaflet map on the beacon show page with a marker at the
  beacon's lat/lon tooltipped with callsign + frequency. Marker is
  green when on air, gray when off.
- Compute and draw a reception-range estimate as concentric signal-
  strength rings. New Microwaveprop.Beacons.RangeEstimate solves a
  link budget (FSPL + O2/H2O absorption from BandConfig) at five RX
  thresholds (-100 to -145 dBm), then scales by 0.5 + score/100 from
  the latest Propagation.point_detail at the beacon's grid square, so
  current HRRR conditions shift the rings in or out.
- Re-enable the hourly PropagationGridWorker cron and freshness
  monitor in dev.exs so dev actually has HRRR-backed scores to feed
  the new estimator.
2026-04-08 13:29:58 -05:00

96 lines
2.8 KiB
Elixir

defmodule Microwaveprop.Beacons.RangeEstimateTest do
use Microwaveprop.DataCase
alias Microwaveprop.Beacons.Beacon
alias Microwaveprop.Beacons.RangeEstimate
defp beacon(overrides \\ %{}) do
base = %Beacon{
id: "00000000-0000-0000-0000-000000000000",
frequency_mhz: 10_368.1,
callsign: "W5HN",
grid: "EM12",
lat: 32.897,
lon: -97.038,
power_mw: 10_000.0,
height_ft: 100.0,
on_the_air: true
}
struct!(base, overrides)
end
describe "mw_to_dbm/1" do
test "1 mW is 0 dBm" do
assert_in_delta RangeEstimate.mw_to_dbm(1.0), 0.0, 0.001
end
test "1 W (1000 mW) is 30 dBm" do
assert_in_delta RangeEstimate.mw_to_dbm(1000.0), 30.0, 0.001
end
test "10 W (10_000 mW) is 40 dBm" do
assert_in_delta RangeEstimate.mw_to_dbm(10_000.0), 40.0, 0.001
end
end
describe "nearest_band_mhz/1" do
test "10368.1 MHz maps to 10000 (10 GHz band)" do
assert RangeEstimate.nearest_band_mhz(10_368.1) == 10_000
end
test "24192.1 MHz maps to 24000" do
assert RangeEstimate.nearest_band_mhz(24_192.1) == 24_000
end
test "47088 MHz maps to 47000" do
assert RangeEstimate.nearest_band_mhz(47_088.0) == 47_000
end
end
describe "estimate/1" do
test "returns a map with band info, score, and a list of rings" do
result = RangeEstimate.estimate(beacon())
assert result.band_mhz == 10_000
assert is_integer(result.score) or is_float(result.score)
assert is_float(result.eirp_dbm)
assert is_list(result.rings)
assert length(result.rings) > 0
end
test "rings are ordered strongest-to-weakest with increasing radius" do
result = RangeEstimate.estimate(beacon())
radii = Enum.map(result.rings, & &1.radius_km)
assert radii == Enum.sort(radii)
end
test "each ring carries a label, color, threshold, and radius_km" do
result = RangeEstimate.estimate(beacon())
for ring <- result.rings do
assert is_binary(ring.label)
assert is_binary(ring.color)
assert is_number(ring.rx_dbm)
assert is_float(ring.radius_km)
assert ring.radius_km >= 0.0
end
end
test "a more powerful beacon produces larger rings" do
weak = RangeEstimate.estimate(beacon(power_mw: 10.0))
strong = RangeEstimate.estimate(beacon(power_mw: 10_000.0))
weakest_weak = List.last(weak.rings).radius_km
weakest_strong = List.last(strong.rings).radius_km
assert weakest_strong > weakest_weak
end
test "uses a default score of 50 when no propagation data exists" do
result = RangeEstimate.estimate(beacon())
# No scores in test DB → score defaults
assert result.score == 50
assert result.valid_time == nil
end
end
end