218 lines
6.1 KiB
Elixir
218 lines
6.1 KiB
Elixir
defmodule Localiser.Domain.Sensors do
|
|
import Ecto.Query
|
|
|
|
alias Localiser.Repo
|
|
alias Localiser.Domain.Schema.Sensor
|
|
alias Localiser.Domain.Schema.SensorCalibration
|
|
|
|
def list_sensors do
|
|
Sensor
|
|
|> Repo.all()
|
|
|> Repo.preload(:room)
|
|
|> Enum.map(&populate_floor_coords/1)
|
|
end
|
|
|
|
def list_unplaced do
|
|
Sensor |> where([s], is_nil(s.room_id)) |> Repo.all()
|
|
end
|
|
|
|
def any_placed?, do: Repo.exists?(from s in Sensor, where: not is_nil(s.room_id))
|
|
|
|
def list_sensors_for_room(room_id) do
|
|
Sensor
|
|
|> where([s], s.room_id == ^room_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
def list_sensors_for_floor(floor_id) do
|
|
Sensor
|
|
|> join(:inner, [s], r in assoc(s, :room))
|
|
|> where([_s, r], r.floor_id == ^floor_id)
|
|
|> Repo.all()
|
|
end
|
|
|
|
def get_sensor!(id) do
|
|
Sensor
|
|
|> Repo.get!(id)
|
|
|> Repo.preload(:room)
|
|
|> populate_floor_coords()
|
|
end
|
|
|
|
def get_sensor_by_sensor_id(sensor_id) do
|
|
Repo.get_by(Sensor, sensor_id: sensor_id)
|
|
end
|
|
|
|
def create_sensor(attrs) do
|
|
%Sensor{}
|
|
|> Sensor.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
def update_sensor(%Sensor{} = sensor, attrs) do
|
|
sensor
|
|
|> Sensor.changeset(attrs)
|
|
|> Repo.update()
|
|
end
|
|
|
|
def factory_reset(%Sensor{} = sensor) do
|
|
Localiser.MQTT.Connection.publish(cmd_topic(sensor), Jason.encode!(%{action: "factory_reset"}))
|
|
{:ok, sensor}
|
|
end
|
|
|
|
def send_ota_update(%Sensor{} = sensor, url, version) do
|
|
payload = %{
|
|
action: "ota",
|
|
url: url,
|
|
version: version
|
|
}
|
|
|
|
Localiser.MQTT.Connection.publish(cmd_topic(sensor), Jason.encode!(payload))
|
|
{:ok, sensor}
|
|
end
|
|
|
|
def send_ota_broadcast(url, version) do
|
|
payload = %{
|
|
action: "ota",
|
|
url: url,
|
|
version: version
|
|
}
|
|
|
|
Localiser.MQTT.Connection.publish("localiser/ota", Jason.encode!(payload))
|
|
end
|
|
|
|
# Pushes new WiFi and/or MQTT broker settings to the sensor. The sensor will
|
|
# apply the changes and restart. All keys are optional — omit any you don't
|
|
# want to change. Valid keys: "ssid", "password", "mqtt_host", "mqtt_port".
|
|
def reconfigure_settings(%Sensor{} = sensor, config) do
|
|
payload = Map.merge(
|
|
%{"action" => "reconfigure_settings"},
|
|
Map.new(config, fn
|
|
{"mqtt_broker", v} -> {"mqtt_host", v}
|
|
entry -> entry
|
|
end)
|
|
)
|
|
Localiser.MQTT.Connection.publish(cmd_topic(sensor), Jason.encode!(payload))
|
|
{:ok, sensor}
|
|
end
|
|
|
|
def delete_sensor(%Sensor{} = sensor) do
|
|
factory_reset(sensor)
|
|
Repo.delete(sensor)
|
|
end
|
|
|
|
def enroll_sensor(%Sensor{} = sensor, room_id) do
|
|
sensor
|
|
|> Sensor.changeset(%{room_id: room_id})
|
|
|> Repo.update()
|
|
end
|
|
|
|
def add_calibration(%Sensor{} = sensor, attrs) do
|
|
attrs = Map.put(attrs, :sensor_id, sensor.id)
|
|
|
|
%SensorCalibration{}
|
|
|> SensorCalibration.changeset(attrs)
|
|
|> Repo.insert()
|
|
end
|
|
|
|
def list_calibrations(%Sensor{} = sensor) do
|
|
SensorCalibration
|
|
|> where([c], c.sensor_id == ^sensor.id)
|
|
|> order_by([c], desc: c.calibrated_at)
|
|
|> Repo.all()
|
|
end
|
|
|
|
def latest_calibration(%Sensor{} = sensor) do
|
|
SensorCalibration
|
|
|> where([c], c.sensor_id == ^sensor.id)
|
|
|> order_by([c], desc: c.calibrated_at)
|
|
|> limit(1)
|
|
|> Repo.one()
|
|
end
|
|
|
|
# Sensor lifecycle / enrollment helpers
|
|
|
|
@enrollment_timeout_ms Application.compile_env(:localiserd, :enrollment_timeout_ms, 10_000)
|
|
|
|
# Called by the phone app to indicate a sensor is about to come online.
|
|
# Creates an unconfirmed sensor record and starts a timeout Task.
|
|
def expect_sensor(sensor_id) do
|
|
result =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{sensor_id: sensor_id, confirmed: false})
|
|
|> Repo.insert(
|
|
on_conflict: [set: [confirmed: false, updated_at: DateTime.utc_now()]],
|
|
conflict_target: :sensor_id,
|
|
returning: true
|
|
)
|
|
|
|
case result do
|
|
{:ok, sensor} ->
|
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "sensors", {:sensor_expected, sensor})
|
|
Task.start(fn ->
|
|
Process.sleep(@enrollment_timeout_ms)
|
|
fresh = Repo.get_by(Sensor, sensor_id: sensor_id)
|
|
if fresh && !fresh.confirmed do
|
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "sensors", {:sensor_enrollment_timeout, sensor_id})
|
|
end
|
|
end)
|
|
{:ok, sensor}
|
|
|
|
error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
# Called when an ESP board self-announces on MQTT. Inserts a new unplaced sensor
|
|
# record, or marks it confirmed if the sensor_id already exists.
|
|
def upsert_announced(sensor_id) do
|
|
result =
|
|
%Sensor{}
|
|
|> Sensor.changeset(%{sensor_id: sensor_id, confirmed: true})
|
|
|> Repo.insert(
|
|
on_conflict: [set: [confirmed: true, updated_at: DateTime.utc_now()]],
|
|
conflict_target: :sensor_id,
|
|
returning: true
|
|
)
|
|
|
|
case result do
|
|
{:ok, sensor} ->
|
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "sensors", {:sensor_announced, sensor})
|
|
{:ok, sensor}
|
|
|
|
error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
# Places (or re-places) a sensor at a specific position within a room.
|
|
# Broadcasts {:sensor_enrolled, sensor, room} for Sensor.Supervisor / Sensor.Server.
|
|
def place_sensor(%Sensor{} = sensor, room_id, {x, y}) do
|
|
with {:ok, sensor} <- update_sensor(sensor, %{room_id: room_id, x: x, y: y}) do
|
|
room = Repo.preload(sensor, :room).room
|
|
sensor = populate_floor_coords(%{sensor | room: room})
|
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "sensors", {:sensor_enrolled, sensor, room})
|
|
{:ok, sensor}
|
|
end
|
|
end
|
|
|
|
# Removes a sensor from the room layout without deleting the DB record.
|
|
# Broadcasts {:sensor_unenrolled, sensor_id} for Sensor.Supervisor.
|
|
def remove_from_layout(%Sensor{} = sensor) do
|
|
with {:ok, sensor} <- update_sensor(sensor, %{room_id: nil, x: nil, y: nil}) do
|
|
Phoenix.PubSub.broadcast(Localiser.PubSub, "sensors", {:sensor_unenrolled, sensor.sensor_id})
|
|
{:ok, sensor}
|
|
end
|
|
end
|
|
|
|
defp cmd_topic(sensor), do: "localiser/sensor/#{sensor.sensor_id}/cmd"
|
|
|
|
defp populate_floor_coords(%Sensor{room: nil} = sensor), do: sensor
|
|
|
|
defp populate_floor_coords(%Sensor{room: room} = sensor) do
|
|
%{sensor |
|
|
floor_x: (room.x || 0.0) + (sensor.x || 0.0),
|
|
floor_y: (room.y || 0.0) + (sensor.y || 0.0)
|
|
}
|
|
end
|
|
end
|