Files
localiserd/lib/localiser/web/controllers/sensor_controller.ex
T

294 lines
9.6 KiB
Elixir

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, version} = Sensors.get_version(sensor)
json(conn, %{status: "ok", version: version})
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