From cdce8445e5fd23b9762c288052c6255429c89617 Mon Sep 17 00:00:00 2001 From: graham Date: Mon, 23 Mar 2026 10:19:43 -0500 Subject: [PATCH] fix/overall-traffic-graph-data (#125) Reviewed-on: https://git.mcintire.me/graham/towerops-web/pulls/125 --- lib/towerops_web/live/device_live/show.ex | 79 +++++++++++++++++++---- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/lib/towerops_web/live/device_live/show.ex b/lib/towerops_web/live/device_live/show.ex index 28776991..eca1abae 100644 --- a/lib/towerops_web/live/device_live/show.ex +++ b/lib/towerops_web/live/device_live/show.ex @@ -1204,8 +1204,10 @@ defmodule ToweropsWeb.DeviceLive.Show do # Calculate BPS per interface first, then aggregate by timestamp # This avoids issues with misaligned timestamps between interfaces + # Only processes monitored interfaces to match actual traffic being tracked defp aggregate_interface_traffic_stats(interfaces, since) do interfaces + |> Enum.filter(& &1.monitored) |> Enum.flat_map(fn interface -> interface.id |> Snmp.get_interface_stats(since: since, limit: 1000) @@ -1329,16 +1331,78 @@ defmodule ToweropsWeb.DeviceLive.Show do # Calculate capacity data using sensor values (for radios with Rx/Tx Capacity sensors) # Sensor values represent total device capacity, not per-interface + # Loads time-series sensor readings to create a dynamic capacity line defp calculate_sensor_capacity_data(all_stats, rx_capacity_sensor, tx_capacity_sensor) do - rx_cap_bps = get_capacity_bps(rx_capacity_sensor) || 0 - tx_cap_bps = get_capacity_bps(tx_capacity_sensor) || 0 + twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour) + + # Load time-series readings for both sensors + rx_readings = load_capacity_sensor_readings(rx_capacity_sensor, twenty_four_hours_ago) + tx_readings = load_capacity_sensor_readings(tx_capacity_sensor, twenty_four_hours_ago) + + # If we have sensor readings, use them directly (they have their own timestamps) + # Otherwise fall back to static values at traffic timestamps + if map_size(rx_readings) > 0 || map_size(tx_readings) > 0 do + build_capacity_data_from_sensors(rx_readings, tx_readings, rx_capacity_sensor, tx_capacity_sensor) + else + build_static_capacity_data(all_stats, rx_capacity_sensor, tx_capacity_sensor) + end + end + + # Load sensor readings and convert to timestamp -> bps map + defp load_capacity_sensor_readings(nil, _since), do: %{} + + defp load_capacity_sensor_readings(sensor, since) do + sensor.id + |> Snmp.get_sensor_readings(since: since, limit: 1000) + |> Map.new(&sensor_reading_to_capacity_point/1) + end + + # Convert a sensor reading to {timestamp_ms, bps} tuple + # Sensor values are in Mbps, convert to bps + defp sensor_reading_to_capacity_point(reading) do + bps = capacity_value_to_bps(reading.value) + ts_ms = DateTime.to_unix(reading.checked_at, :millisecond) + {ts_ms, bps} + end + + # Convert capacity sensor value (in Mbps) to bps + defp capacity_value_to_bps(nil), do: 0 + defp capacity_value_to_bps(value) when is_number(value), do: round(value * 1_000_000) + defp capacity_value_to_bps(_), do: 0 + + # Build capacity data points from sensor readings + defp build_capacity_data_from_sensors(rx_readings, tx_readings, rx_sensor, tx_sensor) do + # Combine all unique timestamps from both sensors + all_timestamps = + (Map.keys(rx_readings) ++ Map.keys(tx_readings)) + |> MapSet.new() + |> Enum.sort() + + # Build capacity data points using sensor readings + Enum.map(all_timestamps, fn ts_ms -> + rx_bps = Map.get(rx_readings, ts_ms, get_fallback_capacity_bps(rx_sensor)) + tx_bps = Map.get(tx_readings, ts_ms, get_fallback_capacity_bps(tx_sensor)) + {ts_ms, rx_bps, tx_bps} + end) + end + + # Build static capacity data at traffic timestamps + defp build_static_capacity_data(all_stats, rx_sensor, tx_sensor) do + rx_cap_bps = get_fallback_capacity_bps(rx_sensor) + tx_cap_bps = get_fallback_capacity_bps(tx_sensor) - # Use the same capacity for all timestamps Enum.map(all_stats, fn stat -> {stat.timestamp_ms, rx_cap_bps, tx_cap_bps} end) end + # Get fallback capacity in bps from sensor's last_value (already in Mbps) + defp get_fallback_capacity_bps(nil), do: 0 + + defp get_fallback_capacity_bps(sensor) do + capacity_value_to_bps(sensor.last_value) + end + # Calculate capacity data from interface configured_capacity_bps or if_speed # Sums capacity of active interfaces at each timestamp defp calculate_interface_capacity_data(all_stats, device) do @@ -1384,15 +1448,6 @@ defmodule ToweropsWeb.DeviceLive.Show do end) end - # Get capacity in bps from a sensor (sensor values are in Mbps, convert to bps) - defp get_capacity_bps(nil), do: nil - - defp get_capacity_bps(%{last_value: value}) when is_number(value) do - # Sensor last_value is in Mbps (already divided by sensor_divisor), convert to bps - round(value * 1_000_000) - end - - defp get_capacity_bps(_sensor), do: nil # Group interfaces by type for organized display defp group_interfaces_by_type(interfaces) do interfaces