prop/lib/microwaveprop/beacon_measurements.ex

110 lines
3.3 KiB
Elixir

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,
preload: [:monitor]
)
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