init: inital commit

This commit is contained in:
2026-04-16 15:46:00 +02:00
commit 34ddbe669e
40 changed files with 1556 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
defmodule Localiser.Domain.Floors do
import Ecto.Query
alias Localiser.Repo
alias Localiser.Domain.Schema.Floor
def list_floors do
Repo.all(Floor)
end
def get_floor!(id), do: Repo.get!(Floor, id)
def create_floor(attrs) do
%Floor{}
|> Floor.changeset(attrs)
|> Repo.insert()
end
def update_floor(%Floor{} = floor, attrs) do
floor
|> Floor.changeset(attrs)
|> Repo.update()
end
def delete_floor(%Floor{} = floor) do
Repo.delete(floor)
end
def list_floors_with_rooms do
Floor
|> preload(:rooms)
|> Repo.all()
end
end
+34
View File
@@ -0,0 +1,34 @@
defmodule Localiser.Domain.Rooms do
import Ecto.Query
alias Localiser.Repo
alias Localiser.Domain.Schema.Room
def list_rooms do
Repo.all(Room)
end
def list_rooms_for_floor(floor_id) do
Room
|> where([r], r.floor_id == ^floor_id)
|> Repo.all()
end
def get_room!(id), do: Repo.get!(Room, id)
def create_room(attrs) do
%Room{}
|> Room.changeset(attrs)
|> Repo.insert()
end
def update_room(%Room{} = room, attrs) do
room
|> Room.changeset(attrs)
|> Repo.update()
end
def delete_room(%Room{} = room) do
Repo.delete(room)
end
end
+21
View File
@@ -0,0 +1,21 @@
defmodule Localiser.Domain.Schema.Floor do
use Ecto.Schema
import Ecto.Changeset
alias Localiser.Domain.Schema.Room
schema "floors" do
field :name, :string
has_many :rooms, Room
timestamps(type: :utc_datetime)
end
@doc false
def changeset(floor, attrs) do
floor
|> cast(attrs, [:name])
|> validate_required([:name])
end
end
+28
View File
@@ -0,0 +1,28 @@
defmodule Localiser.Domain.Schema.Room do
use Ecto.Schema
import Ecto.Changeset
alias Localiser.Domain.Schema.Floor
alias Localiser.Domain.Schema.Sensor
schema "rooms" do
field :name, :string
field :width, :float
field :height, :float
field :offset_x, :float
field :offset_y, :float
belongs_to :floor, Floor
has_many :sensors, Sensor
timestamps(type: :utc_datetime)
end
@doc false
def changeset(room, attrs) do
room
|> cast(attrs, [:name, :floor_id, :width, :height, :offset_x, :offset_y])
|> validate_required([:name, :floor_id])
|> assoc_constraint(:floor)
end
end
+27
View File
@@ -0,0 +1,27 @@
defmodule Localiser.Domain.Schema.Sensor do
use Ecto.Schema
import Ecto.Changeset
alias Localiser.Domain.Schema.Room
alias Localiser.Domain.Schema.SensorCalibration
schema "sensors" do
field :sensor_id, :string
field :x, :float
field :y, :float
belongs_to :room, Room
has_many :calibrations, SensorCalibration
timestamps(type: :utc_datetime)
end
@doc false
def changeset(sensor, attrs) do
sensor
|> cast(attrs, [:sensor_id, :room_id, :x, :y])
|> validate_required([:sensor_id])
|> unique_constraint(:sensor_id)
|> assoc_constraint(:room)
end
end
@@ -0,0 +1,26 @@
defmodule Localiser.Domain.Schema.SensorCalibration do
use Ecto.Schema
import Ecto.Changeset
alias Localiser.Domain.Schema.Sensor
@timestamps_opts [inserted_at: :inserted_at, updated_at: false, type: :utc_datetime]
schema "sensor_calibrations" do
field :rssi_ref, :integer
field :path_loss_exp, :float
field :calibrated_at, :utc_datetime
belongs_to :sensor, Sensor
timestamps(@timestamps_opts)
end
@doc false
def changeset(calibration, attrs) do
calibration
|> cast(attrs, [:sensor_id, :rssi_ref, :path_loss_exp, :calibrated_at])
|> validate_required([:sensor_id, :rssi_ref, :path_loss_exp, :calibrated_at])
|> assoc_constraint(:sensor)
end
end
+21
View File
@@ -0,0 +1,21 @@
defmodule Localiser.Domain.Schema.Tag do
use Ecto.Schema
import Ecto.Changeset
schema "tags" do
field :tag_id, :string
field :name, :string
field :color, :string
field :metadata, :map
timestamps(type: :utc_datetime)
end
@doc false
def changeset(tag, attrs) do
tag
|> cast(attrs, [:tag_id, :name, :color, :metadata])
|> validate_required([:tag_id])
|> unique_constraint(:tag_id)
end
end
+28
View File
@@ -0,0 +1,28 @@
defmodule Localiser.Domain.Schema.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :username, :string
field :password_hash, :string, redact: true
field :password, :string, virtual: true, redact: true
timestamps(type: :utc_datetime)
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :password])
|> validate_required([:username, :password])
|> validate_length(:password, min: 8)
|> unique_constraint(:username)
|> hash_password()
end
defp hash_password(%Ecto.Changeset{valid?: true, changes: %{password: pw}} = changeset) do
put_change(changeset, :password_hash, Argon2.hash_pwd_salt(pw))
end
defp hash_password(changeset), do: changeset
end
+118
View File
@@ -0,0 +1,118 @@
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
Repo.all(Sensor)
end
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: Repo.get!(Sensor, id)
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 delete_sensor(%Sensor{} = sensor) do
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
# Called when an ESP board self-announces on MQTT. Inserts a new unplaced sensor
# record, or bumps updated_at if the sensor_id already exists.
def upsert_announced(sensor_id) do
result =
%Sensor{}
|> Sensor.changeset(%{sensor_id: sensor_id})
|> Repo.insert(
on_conflict: [set: [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
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
end
+30
View File
@@ -0,0 +1,30 @@
defmodule Localiser.Domain.Tags do
alias Localiser.Repo
alias Localiser.Domain.Schema.Tag
def list_tags do
Repo.all(Tag)
end
def get_tag!(id), do: Repo.get!(Tag, id)
def get_tag_by_tag_id(tag_id) do
Repo.get_by(Tag, tag_id: tag_id)
end
def create_tag(attrs) do
%Tag{}
|> Tag.changeset(attrs)
|> Repo.insert()
end
def update_tag(%Tag{} = tag, attrs) do
tag
|> Tag.changeset(attrs)
|> Repo.update()
end
def delete_tag(%Tag{} = tag) do
Repo.delete(tag)
end
end
+32
View File
@@ -0,0 +1,32 @@
defmodule Localiser.Domain.Users do
alias Localiser.Repo
alias Localiser.Domain.Schema.User
def get_user!(id), do: Repo.get!(User, id)
def get_user_by_username(username) do
Repo.get_by(User, username: username)
end
def create_user(attrs) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def authenticate_user(username, password) do
user = get_user_by_username(username)
cond do
user && Argon2.verify_pass(password, user.password_hash) ->
{:ok, user}
user ->
{:error, :invalid_credentials}
true ->
Argon2.no_user_verify()
{:error, :invalid_credentials}
end
end
end