feat: expose CRUD, onboarding, pubsub via web

This commit is contained in:
2026-04-22 16:32:41 +02:00
parent 9807331da4
commit 9389c32244
33 changed files with 1536 additions and 7 deletions
@@ -0,0 +1,193 @@
defmodule Localiser.Web.Controllers.SensorController do
use Phoenix.Controller, formats: [:json]
use OpenApiSpex.ControllerSpecs
alias Localiser.Domain.Sensors
alias Localiser.Localisation.Sensor.Server, as: SensorServer
alias Localiser.Web.Schemas
tags ["Sensors"]
security [%{"bearerAuth" => []}]
operation :index,
summary: "List all sensors",
responses: [
ok: {"Sensor list", "application/json", %OpenApiSpex.Schema{type: :array, items: Schemas.Sensor}},
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
]
operation :unplaced,
summary: "List sensors not yet assigned to a room",
responses: [
ok: {"Sensor list", "application/json", %OpenApiSpex.Schema{type: :array, items: Schemas.Sensor}},
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
]
operation :show,
summary: "Get a sensor",
parameters: [id: [in: :path, type: :integer, required: true]],
responses: [
ok: {"Sensor", "application/json", Schemas.Sensor},
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
]
operation :update,
summary: "Update sensor metadata",
parameters: [id: [in: :path, type: :integer, required: true]],
request_body: {"Sensor params", "application/json", Schemas.SensorUpdateParams},
responses: [
ok: {"Updated sensor", "application/json", Schemas.Sensor},
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
]
operation :delete,
summary: "Delete / unenrol a sensor",
parameters: [id: [in: :path, type: :integer, required: true]],
responses: [
no_content: "Deleted",
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
]
operation :place,
summary: "Place sensor at a position in a room",
parameters: [id: [in: :path, type: :integer, required: true]],
request_body: {"Place params", "application/json", Schemas.SensorPlaceParams, required: true},
responses: [
ok: {"Updated sensor", "application/json", Schemas.Sensor},
bad_request: {"Missing params", "application/json", Schemas.Error},
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
]
operation :unplace,
summary: "Remove sensor from floor layout",
parameters: [id: [in: :path, type: :integer, required: true]],
responses: [
ok: {"Updated sensor", "application/json", Schemas.Sensor},
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
]
operation :calibration_start,
summary: "Begin RSSI calibration",
parameters: [id: [in: :path, type: :integer, required: true]],
request_body: {"Calibration params", "application/json", Schemas.CalibrationStartParams, required: true},
responses: [
ok: {"Calibration started", "application/json", Schemas.CalibrationStatus},
bad_request: {"Missing reference_distance", "application/json", Schemas.Error},
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
]
operation :calibration_stop,
summary: "Abort active calibration",
parameters: [id: [in: :path, type: :integer, required: true]],
responses: [
ok: {"Calibration aborted", "application/json", Schemas.CalibrationStatus},
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
]
def index(conn, _params) do
json(conn, Enum.map(Sensors.list_sensors(), &render_sensor/1))
end
def unplaced(conn, _params) do
json(conn, Enum.map(Sensors.list_unplaced(), &render_sensor/1))
end
def show(conn, %{"id" => id}) do
sensor = Sensors.get_sensor!(id)
json(conn, render_sensor(sensor))
end
def update(conn, %{"id" => id} = params) do
sensor = Sensors.get_sensor!(id)
attrs = Map.drop(params, ["id"])
case Sensors.update_sensor(sensor, attrs) do
{:ok, updated} ->
json(conn, render_sensor(updated))
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: format_errors(changeset)})
end
end
def delete(conn, %{"id" => id}) do
sensor = Sensors.get_sensor!(id)
{:ok, _} = Sensors.delete_sensor(sensor)
send_resp(conn, :no_content, "")
end
def place(conn, %{"id" => id, "room_id" => room_id, "x" => x, "y" => y}) do
sensor = Sensors.get_sensor!(id)
case Sensors.place_sensor(sensor, room_id, {x, y}) do
{:ok, updated} ->
json(conn, render_sensor(updated))
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: format_errors(changeset)})
end
end
def place(conn, _params) do
conn
|> put_status(:bad_request)
|> json(%{error: "room_id, x, and y are required"})
end
def unplace(conn, %{"id" => id}) do
sensor = Sensors.get_sensor!(id)
case Sensors.remove_from_layout(sensor) do
{:ok, updated} ->
json(conn, render_sensor(updated))
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: format_errors(changeset)})
end
end
def calibration_start(conn, %{"id" => id, "reference_distance" => ref_dist}) do
sensor = Sensors.get_sensor!(id)
:ok = SensorServer.begin_calibration(sensor.sensor_id, ref_dist)
json(conn, %{status: "calibrating"})
end
def calibration_start(conn, _params) do
conn
|> put_status(:bad_request)
|> json(%{error: "reference_distance is required"})
end
def calibration_stop(conn, %{"id" => id}) do
sensor = Sensors.get_sensor!(id)
:ok = SensorServer.abort_calibration(sensor.sensor_id)
json(conn, %{status: "idle"})
end
defp render_sensor(sensor) do
%{
id: sensor.id,
sensor_id: sensor.sensor_id,
room_id: sensor.room_id,
floor_x: sensor.floor_x,
floor_y: sensor.floor_y,
rssi_ref: sensor.rssi_ref
}
end
defp format_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, val}, acc ->
String.replace(acc, "%{#{key}}", to_string(val))
end)
end)
end
end