defmodule ToweropsWeb.GraphLive.Show do @moduledoc false use ToweropsWeb, :live_view alias Towerops.Equipment alias Towerops.Snmp @impl true def mount(_params, _session, socket) do {:ok, socket} end @impl true def handle_params(%{"id" => equipment_id, "sensor_type" => sensor_type} = params, _, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{equipment_id}") end range = Map.get(params, "range", "24h") socket = socket |> assign(:equipment_id, equipment_id) |> assign(:sensor_type, sensor_type) |> assign(:range, range) |> load_graph_data() {:noreply, socket} end @impl true def handle_event("change_range", %{"range" => range}, socket) do {:noreply, push_patch(socket, to: ~p"/orgs/#{socket.assigns.current_organization.slug}/equipment/#{socket.assigns.equipment_id}/graph/#{socket.assigns.sensor_type}?range=#{range}" )} end # Private functions defp load_graph_data(socket) do equipment_id = socket.assigns.equipment_id sensor_type = socket.assigns.sensor_type range = socket.assigns.range equipment = Equipment.get_equipment!(equipment_id) chart_data = if sensor_type == "traffic" do load_traffic_chart_data(equipment_id, range) else sensor_types = get_sensor_types_for_chart(sensor_type) load_sensor_chart_data(equipment_id, sensor_types, range) end {title, unit, auto_scale} = get_chart_config(sensor_type) socket |> assign(:equipment, equipment) |> assign(:page_title, "#{equipment.name} - #{title}") |> assign(:chart_title, title) |> assign(:chart_data, chart_data) |> assign(:unit, unit) |> assign(:auto_scale, auto_scale) end defp get_sensor_types_for_chart("processors"), do: ["cpu_load"] defp get_sensor_types_for_chart("memory"), do: ["memory_usage"] defp get_sensor_types_for_chart("storage"), do: ["disk_usage"] defp get_sensor_types_for_chart("temperature"), do: ["temperature", "cpu_temperature", "celsius"] defp get_sensor_types_for_chart("voltage"), do: ["voltage", "volts"] defp get_sensor_types_for_chart("traffic"), do: nil defp get_sensor_types_for_chart(_), do: [] defp get_chart_config("processors"), do: {"Processor Usage", "%", false} defp get_chart_config("memory"), do: {"Memory Usage", "%", false} defp get_chart_config("storage"), do: {"Storage Usage", "%", false} defp get_chart_config("temperature"), do: {"Temperature", "°C", true} defp get_chart_config("voltage"), do: {"Voltage", "V", true} defp get_chart_config("traffic"), do: {"Overall Traffic", "bps", true} defp get_chart_config(_), do: {"Sensor Data", "", false} defp load_sensor_chart_data(equipment_id, sensor_types, range) do case Snmp.get_device_with_associations(equipment_id) do nil -> nil device -> # Find all sensors of the specified types sensors = device.sensors |> Enum.filter(&(&1.sensor_type in sensor_types)) |> Enum.sort_by(& &1.sensor_index) if Enum.empty?(sensors) do nil else # Get readings based on range since = get_datetime_from_range(range) datasets = Enum.map(sensors, fn sensor -> readings = sensor.id |> Snmp.get_sensor_readings( since: since, limit: get_limit_for_range(range) ) |> Enum.reverse() %{ label: sensor.sensor_descr, data: Enum.map(readings, fn reading -> %{ x: DateTime.to_unix(reading.checked_at, :millisecond), y: if(reading.value, do: Float.round(reading.value, 1)) } end) } end) Jason.encode!(%{datasets: datasets}) end end end defp get_datetime_from_range("1h"), do: DateTime.add(DateTime.utc_now(), -1, :hour) defp get_datetime_from_range("6h"), do: DateTime.add(DateTime.utc_now(), -6, :hour) defp get_datetime_from_range("12h"), do: DateTime.add(DateTime.utc_now(), -12, :hour) defp get_datetime_from_range("24h"), do: DateTime.add(DateTime.utc_now(), -24, :hour) defp get_datetime_from_range("7d"), do: DateTime.add(DateTime.utc_now(), -7, :day) defp get_datetime_from_range("30d"), do: DateTime.add(DateTime.utc_now(), -30, :day) defp get_datetime_from_range(_), do: DateTime.add(DateTime.utc_now(), -24, :hour) defp get_limit_for_range("1h"), do: 360 defp get_limit_for_range("6h"), do: 720 defp get_limit_for_range("12h"), do: 1440 defp get_limit_for_range("24h"), do: 2880 defp get_limit_for_range("7d"), do: 10_080 defp get_limit_for_range("30d"), do: 43_200 defp get_limit_for_range(_), do: 2880 defp load_traffic_chart_data(equipment_id, range) do case Snmp.get_device_with_associations(equipment_id) do nil -> nil device -> if Enum.empty?(device.interfaces) do nil else since = get_datetime_from_range(range) limit = get_limit_for_range(range) # Get all stats for all interfaces all_stats = device.interfaces |> Enum.flat_map(fn interface -> stats = interface.id |> Snmp.get_interface_stats(since: since, limit: limit) |> Enum.reverse() Enum.map(stats, fn stat -> {stat.checked_at, stat.if_in_octets, stat.if_out_octets} end) end) |> Enum.group_by(&elem(&1, 0)) |> Enum.map(fn {timestamp, stats} -> total_in = Enum.reduce(stats, 0, fn {_, in_octets, _}, acc -> acc + (in_octets || 0) end) total_out = Enum.reduce(stats, 0, fn {_, _, out_octets}, acc -> acc + (out_octets || 0) end) {timestamp, total_in, total_out} end) |> Enum.sort_by(&elem(&1, 0), DateTime) if Enum.empty?(all_stats) do nil else # Calculate bits per second from counter differences in_data = all_stats |> Enum.chunk_every(2, 1, :discard) |> Enum.map(fn [{t1, in1, _out1}, {t2, in2, _out2}] -> time_diff = t2 |> DateTime.diff(t1, :second) |> max(1) bps = ((in2 - in1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2) %{ x: DateTime.to_unix(t2, :millisecond), y: bps } end) out_data = all_stats |> Enum.chunk_every(2, 1, :discard) |> Enum.map(fn [{t1, _in1, out1}, {t2, _in2, out2}] -> time_diff = t2 |> DateTime.diff(t1, :second) |> max(1) bps = ((out2 - out1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2) %{ x: DateTime.to_unix(t2, :millisecond), y: bps } end) datasets = [ %{label: "Inbound", data: in_data}, %{label: "Outbound", data: out_data} ] Jason.encode!(%{datasets: datasets}) end end end end end