294 lines
9.6 KiB
Elixir
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
|