towerops/lib/towerops/preseem.ex

191 lines
5.2 KiB
Elixir

defmodule Towerops.Preseem do
@moduledoc """
Context for querying Preseem integration data.
"""
import Ecto.Query
alias Towerops.Preseem.AccessPoint
alias Towerops.Preseem.Insights
alias Towerops.Preseem.SubscriberMetric
alias Towerops.Repo
@doc "Get the Preseem AP linked to a specific device, or nil."
def get_access_point_for_device(device_id) do
AccessPoint
|> where(device_id: ^device_id)
|> limit(1)
|> Repo.one()
end
@doc "List all Preseem APs for an organization."
def list_access_points(organization_id) do
AccessPoint
|> where(organization_id: ^organization_id)
|> order_by(:name)
|> preload(:device)
|> Repo.all()
end
@doc "List unmatched or ambiguous Preseem APs for an organization."
def list_unmatched_access_points(organization_id) do
AccessPoint
|> where(organization_id: ^organization_id)
|> where([ap], ap.match_confidence in ["unmatched", "ambiguous"])
|> order_by(:name)
|> preload(:device)
|> Repo.all()
end
@doc "List matched Preseem APs for an organization (everything except unmatched/ambiguous)."
def list_matched_access_points(organization_id) do
AccessPoint
|> where(organization_id: ^organization_id)
|> where([ap], ap.match_confidence not in ["unmatched", "ambiguous"])
|> order_by(:name)
|> preload(:device)
|> Repo.all()
end
@doc "List recent subscriber metrics for a Preseem AP. Default limit 100."
def list_subscriber_metrics(access_point_id, opts \\ []) do
limit = Keyword.get(opts, :limit, 100)
SubscriberMetric
|> where(preseem_access_point_id: ^access_point_id)
|> order_by(desc: :recorded_at)
|> limit(^limit)
|> Repo.all()
end
@doc """
Get QoE data for a device's access points since a given time.
Returns a list of metrics with latency, throughput, and loss data.
"""
def get_device_qoe_data(device_id, since) do
ap_ids =
AccessPoint
|> where(device_id: ^device_id)
|> select([ap], ap.id)
|> Repo.all()
if Enum.empty?(ap_ids) do
[]
else
SubscriberMetric
|> where([m], m.preseem_access_point_id in ^ap_ids)
|> where([m], m.recorded_at >= ^since)
|> order_by(asc: :recorded_at)
|> select([m], %{
t: m.recorded_at,
latency: m.avg_latency,
throughput: m.avg_throughput,
loss: m.avg_loss
})
|> Repo.all()
|> Enum.map(fn m ->
%{
t: DateTime.to_iso8601(m.t),
latency: m.latency,
throughput: m.throughput,
loss: m.loss
}
end)
end
end
@doc "Link a Preseem AP to a device manually."
def link_access_point(access_point_id, device_id) do
case Repo.get(AccessPoint, access_point_id) do
nil ->
{:error, :not_found}
ap ->
ap
|> AccessPoint.changeset(%{device_id: device_id, match_confidence: "manual"})
|> Repo.update()
end
end
@doc "Unlink a Preseem AP from its device."
def unlink_access_point(access_point_id) do
case Repo.get(AccessPoint, access_point_id) do
nil ->
{:error, :not_found}
ap ->
ap
|> AccessPoint.changeset(%{device_id: nil, match_confidence: "unmatched"})
|> Repo.update()
end
end
@doc """
Returns an aggregate QoE summary for all APs at a site.
Returns a map with average QoE, latency, capacity info, and per-AP breakdowns.
"""
def get_site_qoe_summary(site_id) do
alias Towerops.Devices
device_ids =
site_id
|> Devices.list_site_devices()
|> Enum.map(& &1.id)
if device_ids == [] do
%{
avg_qoe: nil,
avg_capacity: nil,
total_subscribers: 0,
aps: [],
aps_with_issues: 0,
total_aps: 0
}
else
aps =
AccessPoint
|> where([ap], ap.device_id in ^device_ids)
|> Repo.all()
qoe_scores = aps |> Enum.map(& &1.qoe_score) |> Enum.reject(&is_nil/1)
capacity_scores = aps |> Enum.map(& &1.capacity_score) |> Enum.reject(&is_nil/1)
avg_qoe =
if qoe_scores == [],
do: nil,
else: Float.round(Enum.sum(qoe_scores) / length(qoe_scores), 1)
avg_capacity =
if capacity_scores == [],
do: nil,
else: Float.round(Enum.sum(capacity_scores) / length(capacity_scores), 1)
total_subscribers =
aps |> Enum.map(& &1.subscriber_count) |> Enum.reject(&is_nil/1) |> Enum.sum()
# APs with issues: QoE < 6.0 (on 1-10 scale) or capacity < 50
aps_with_issues =
Enum.count(aps, fn ap ->
(ap.qoe_score && ap.qoe_score < 6.0) ||
(ap.capacity_score && ap.capacity_score < 50.0)
end)
%{
avg_qoe: avg_qoe,
avg_capacity: avg_capacity,
total_subscribers: total_subscribers,
aps: aps,
aps_with_issues: aps_with_issues,
total_aps: length(aps)
}
end
end
# --- Insight delegations ---
defdelegate list_insights(organization_id, opts \\ []), to: Insights
defdelegate list_device_insights(device_id), to: Insights
defdelegate dismiss_insight(insight_id), to: Insights
defdelegate dismiss_insights(insight_ids), to: Insights
end