defmodule Localiser.Web.Controllers.SensorController do use Phoenix.Controller, formats: [:json] use OpenApiSpex.ControllerSpecs alias Localiser.Domain.{Sensors, Firmware} 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 :enroll, summary: "Notify server a sensor is about to be enrolled", request_body: {"Enroll params", "application/json", Schemas.SensorEnrollParams, required: true}, responses: [ ok: {"Sensor (unconfirmed)", "application/json", Schemas.Sensor}, bad_request: {"Missing sensor_id", "application/json", Schemas.Error}, unauthorized: {"Unauthorized", "application/json", Schemas.Error} ] operation :ota, summary: "Send OTA update command to a single sensor", parameters: [id: [in: :path, type: :integer, required: true]], request_body: {"OTA params", "application/json", %OpenApiSpex.Schema{ type: :object, required: [:version], properties: %{version: %OpenApiSpex.Schema{type: :string}} }, required: true}, responses: [ ok: {"Command sent", "application/json", Schemas.CommandSent}, not_found: {"Firmware version not found", "application/json", Schemas.Error}, unauthorized: {"Unauthorized", "application/json", Schemas.Error} ] operation :factory_reset, summary: "Send factory reset command to sensor", parameters: [id: [in: :path, type: :integer, required: true]], responses: [ ok: {"Command sent", "application/json", Schemas.CommandSent}, unauthorized: {"Unauthorized", "application/json", Schemas.Error} ] operation :reconfigure, summary: "Push new network settings to sensor", parameters: [id: [in: :path, type: :integer, required: true]], request_body: {"Reconfigure params", "application/json", Schemas.SensorReconfigureParams, required: true}, responses: [ ok: {"Command sent", "application/json", Schemas.CommandSent}, 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 enroll(conn, %{"sensor_id" => sensor_id}) do case Sensors.expect_sensor(sensor_id) do {:ok, sensor} -> json(conn, render_sensor(sensor)) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{errors: format_errors(changeset)}) end end def enroll(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "sensor_id is required"}) end 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 ota(conn, %{"id" => id, "version" => version}) do sensor = Sensors.get_sensor!(id) case Firmware.get(version) do {:ok, _path} -> url = "#{conn.scheme}://#{conn.host}:#{conn.port}/api/firmware/#{version}" {:ok, _} = Sensors.send_ota_update(sensor, url, version) json(conn, %{status: "ok", url: url, version: version}) :not_found -> conn |> put_status(:not_found) |> json(%{error: "Firmware version not found"}) end end def ota(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "version is required"}) end def factory_reset(conn, %{"id" => id}) do sensor = Sensors.get_sensor!(id) {:ok, _} = Sensors.factory_reset(sensor) json(conn, %{status: "ok"}) end def get_version(conn, %{"id" => id}) do sensor = Sensors.get_sensor!(id) {:ok, _} = Sensors.get_version(sensor) json(conn, %{status: "ok"}) end def reconfigure(conn, %{"id" => id} = params) do sensor = Sensors.get_sensor!(id) config = Map.take(params, ["ssid", "password", "mqtt_broker", "mqtt_port"]) {:ok, _} = Sensors.reconfigure_settings(sensor, config) json(conn, %{status: "ok"}) 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, confirmed: sensor.confirmed, room_id: sensor.room_id, floor_x: sensor.floor_x, floor_y: sensor.floor_y, x: sensor.x, y: sensor.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