defmodule Microwaveprop.BeaconMeasurements do @moduledoc """ The BeaconMeasurements context: ingests beacon-signal-level measurements uploaded by propmonitor clients. See `Microwaveprop.BeaconMeasurements.BeaconMeasurement` for the schema and field semantics. """ import Ecto.Query alias Microwaveprop.BeaconMeasurements.BeaconMeasurement alias Microwaveprop.BeaconMonitors.BeaconMonitor alias Microwaveprop.Repo @doc """ Inserts a measurement for the given monitor. `attrs` is expected to carry the wire fields from `POST /api/v1/beacon-monitor/measurements` (including `beacon_id` as a UUID string). Returns `{:error, :beacon_not_found}` if the beacon UUID does not resolve to an approved on-the-air beacon — the propmonitor client drops the measurement on this, so we keep the differentiation from generic 422s. """ @spec create_measurement(BeaconMonitor.t(), map()) :: {:ok, BeaconMeasurement.t()} | {:error, :beacon_not_found} | {:error, Ecto.Changeset.t()} def create_measurement(%BeaconMonitor{id: monitor_id}, attrs) when is_map(attrs) do case lookup_beacon(attrs) do :not_found -> {:error, :beacon_not_found} {:ok, beacon_id} -> attrs = attrs |> normalize_attrs() |> Map.put("monitor_id", monitor_id) |> Map.put("beacon_id", beacon_id) %BeaconMeasurement{} |> BeaconMeasurement.changeset(attrs) |> Repo.insert() end end defp lookup_beacon(attrs) do case Map.get(attrs, "beacon_id") || Map.get(attrs, :beacon_id) do id when is_binary(id) and byte_size(id) > 0 -> # Only let monitors report for approved on-the-air beacons. The # client drops 404s without retry, so an admin un-approving a # beacon cleanly stops the monitor pointing at it. query = from b in Microwaveprop.Beacons.Beacon, where: b.id == ^id and b.approved == true and b.on_the_air == true, select: b.id case safe_one(query) do nil -> :not_found uuid -> {:ok, uuid} end _ -> :not_found end end defp safe_one(query) do Repo.one(query) rescue Ecto.Query.CastError -> nil end # The wire payload uses string keys (Phoenix JSON params). Make sure # we don't drop a value because some intermediate handed us atom keys. defp normalize_attrs(attrs) do Map.new(attrs, fn {k, v} when is_atom(k) -> {Atom.to_string(k), v} {k, v} -> {k, v} end) end @doc """ Returns the most recent measurements for the given beacon, newest first. """ @spec list_recent_for_beacon(Ecto.UUID.t(), pos_integer()) :: [BeaconMeasurement.t()] def list_recent_for_beacon(beacon_id, limit \\ 100) do Repo.all( from m in BeaconMeasurement, where: m.beacon_id == ^beacon_id, order_by: [desc: m.measured_at], limit: ^limit ) end @doc """ Returns the most recent measurements for the given monitor, newest first. """ @spec list_recent_for_monitor(Ecto.UUID.t(), pos_integer()) :: [BeaconMeasurement.t()] def list_recent_for_monitor(monitor_id, limit \\ 100) do Repo.all( from m in BeaconMeasurement, where: m.monitor_id == ^monitor_id, order_by: [desc: m.measured_at], limit: ^limit ) end end