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 } 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