110 lines
3.3 KiB
Elixir
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
|