From 2b24626baa5e2c254d34ce3b85ca38ac704d9f46 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 22 Mar 2026 15:49:39 -0500 Subject: [PATCH] fix: correct capacity calculation and wireless sensor display (#116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Capacity: Sensor values (Rx/Tx Capacity) now represent total device capacity, not per-interface. Previous bug applied the same sensor value to all interfaces and summed them (e.g., 773.6 Mbps × 5 = 3.87 Gbps). - Wireless sensors: Changed Float.round/2 to :erlang.float_to_binary/2 to prevent scientific notation in frequency display (was showing "2.41e4 MHz" instead of "24100.0 MHz"). Files: - lib/towerops_web/live/device_live/show.ex - lib/towerops_web/live/device_live/show.html.heex - CHANGELOG.txt - priv/static/changelog.txt Reviewed-on: https://git.mcintire.me/graham/towerops-web/pulls/116 --- CHANGELOG.txt | 17 +++ lib/towerops_web/live/device_live/show.ex | 110 +++++++++++------- .../live/device_live/show.html.heex | 4 +- priv/static/changelog.txt | 4 + 4 files changed, 89 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8f64a5a9..e6da6a97 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,20 @@ +2026-03-22 +fix: correct capacity calculation for backhaul devices with sensor-based capacity + - Sensor capacity (Rx/Tx Capacity) now represents total device capacity, not per-interface + - Previous bug: applied same sensor value to all interfaces, then summed them (e.g. 773.6 Mbps * 5 interfaces = 3.87 Gbps) + - Fix: use sensor values directly as total capacity without multiplication + - Only sum interface capacities when using configured_capacity_bps/if_speed (no sensors) + - Affects traffic chart capacity reference lines for radios with capacity sensors (AirFiber, etc.) + Files: lib/towerops_web/live/device_live/show.ex + +2026-03-22 +fix: prevent scientific notation in wireless sensor display (frequency) + - Wireless sensor values (especially frequency) were showing in scientific notation (2.41e4 MHz) + - Changed Float.round/2 to :erlang.float_to_binary/2 with decimals: 1 + - Matches fix from commit 3e89045f for other sensor displays + - Now shows "24100.0 MHz" instead of "2.41e4 MHz" + Files: lib/towerops_web/live/device_live/show.html.heex + 2026-03-22 feat: simplify device type to manual-only with 6 options - Reduced device role dropdown from 10 options to 6: server, switch, router, access_point, backhaul, other diff --git a/lib/towerops_web/live/device_live/show.ex b/lib/towerops_web/live/device_live/show.ex index 636b6792..fde2cf7f 100644 --- a/lib/towerops_web/live/device_live/show.ex +++ b/lib/towerops_web/live/device_live/show.ex @@ -1273,57 +1273,20 @@ defmodule ToweropsWeb.DeviceLive.Show do # Add capacity reference lines for backhaul devices # Calculates capacity per timestamp based on which interfaces have data at that point - # For radios with Rx/Tx Capacity sensors, uses those values instead of if_speed + # For radios with Rx/Tx Capacity sensors, uses those values as total device capacity defp add_capacity_datasets(datasets, all_stats, device) do # Get capacity sensors if available (for radios like AirFiber) rx_capacity_sensor = Enum.find(device.sensors, &(&1.sensor_descr == "Rx Capacity")) tx_capacity_sensor = Enum.find(device.sensors, &(&1.sensor_descr == "Tx Capacity")) - # Build a map of interface_id -> {rx_capacity, tx_capacity} for quick lookup - interface_capacity_map = - device.interfaces - |> Enum.filter(& &1.monitored) - |> Map.new(fn interface -> - # Use sensor values if available (converted to bps), otherwise fall back to configured/if_speed - rx_cap = get_capacity_bps(rx_capacity_sensor) || interface.configured_capacity_bps || interface.if_speed || 0 - tx_cap = get_capacity_bps(tx_capacity_sensor) || interface.configured_capacity_bps || interface.if_speed || 0 - {interface.id, {rx_cap, tx_cap}} - end) - - # Get the raw interface stats to determine which interfaces were active at each timestamp - twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour) - - # Track which interfaces have data at each timestamp - interface_activity_by_timestamp = - device.interfaces - |> Enum.flat_map(fn interface -> - interface.id - |> Snmp.get_interface_stats(since: twenty_four_hours_ago, limit: 1000) - |> Enum.map(fn stat -> - ts_seconds = DateTime.to_unix(stat.checked_at) - rounded_ts_ms = div(ts_seconds, 60) * 60 * 1000 - {rounded_ts_ms, interface.id} - end) - end) - |> Enum.group_by(fn {ts, _} -> ts end, fn {_, iface_id} -> iface_id end) - - # Calculate capacity at each timestamp based on active interfaces - # For backhaul devices with Rx/Tx sensors, track separately + # If we have capacity sensors, use them as the total device capacity + # Otherwise, calculate from interface speeds capacity_data = - Enum.map(all_stats, fn stat -> - # Get interfaces that had data at this timestamp - active_interfaces = Map.get(interface_activity_by_timestamp, stat.timestamp_ms, []) - # Sum capacity of active interfaces - {total_rx, total_tx} = - active_interfaces - |> Enum.uniq() - |> Enum.reduce({0, 0}, fn iface_id, {rx_acc, tx_acc} -> - {rx_cap, tx_cap} = Map.get(interface_capacity_map, iface_id, {0, 0}) - {rx_acc + rx_cap, tx_acc + tx_cap} - end) - - {stat.timestamp_ms, total_rx, total_tx} - end) + if rx_capacity_sensor || tx_capacity_sensor do + calculate_sensor_capacity_data(all_stats, rx_capacity_sensor, tx_capacity_sensor) + else + calculate_interface_capacity_data(all_stats, device) + end # Only add capacity datasets if we have at least some capacity data max_rx = capacity_data |> Enum.map(fn {_, rx, _} -> rx end) |> Enum.max(fn -> 0 end) @@ -1365,6 +1328,63 @@ defmodule ToweropsWeb.DeviceLive.Show do end end + # Calculate capacity data using sensor values (for radios with Rx/Tx Capacity sensors) + # Sensor values represent total device capacity, not per-interface + 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 + + # Use the same capacity for all timestamps + Enum.map(all_stats, fn stat -> + {stat.timestamp_ms, rx_cap_bps, tx_cap_bps} + end) + 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 + # Build a map of interface_id -> {rx_capacity, tx_capacity} for quick lookup + interface_capacity_map = + device.interfaces + |> Enum.filter(& &1.monitored) + |> Map.new(fn interface -> + cap = interface.configured_capacity_bps || interface.if_speed || 0 + {interface.id, {cap, cap}} + end) + + # Get the raw interface stats to determine which interfaces were active at each timestamp + twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour) + + # Track which interfaces have data at each timestamp + interface_activity_by_timestamp = + device.interfaces + |> Enum.flat_map(fn interface -> + interface.id + |> Snmp.get_interface_stats(since: twenty_four_hours_ago, limit: 1000) + |> Enum.map(fn stat -> + ts_seconds = DateTime.to_unix(stat.checked_at) + rounded_ts_ms = div(ts_seconds, 60) * 60 * 1000 + {rounded_ts_ms, interface.id} + end) + end) + |> Enum.group_by(fn {ts, _} -> ts end, fn {_, iface_id} -> iface_id end) + + # Calculate capacity at each timestamp based on active interfaces + Enum.map(all_stats, fn stat -> + active_interfaces = Map.get(interface_activity_by_timestamp, stat.timestamp_ms, []) + + {total_rx, total_tx} = + active_interfaces + |> Enum.uniq() + |> Enum.reduce({0, 0}, fn iface_id, {rx_acc, tx_acc} -> + {rx_cap, tx_cap} = Map.get(interface_capacity_map, iface_id, {0, 0}) + {rx_acc + rx_cap, tx_acc + tx_cap} + end) + + {stat.timestamp_ms, total_rx, total_tx} + end) + end + # Get capacity in bps from a sensor (sensor values are in Mbps, convert to bps) defp get_capacity_bps(nil), do: nil diff --git a/lib/towerops_web/live/device_live/show.html.heex b/lib/towerops_web/live/device_live/show.html.heex index 6cf72a3c..2b22c6a9 100644 --- a/lib/towerops_web/live/device_live/show.html.heex +++ b/lib/towerops_web/live/device_live/show.html.heex @@ -1167,7 +1167,9 @@ <%= if sensor.sensor_type in ["clients", "ccq", "utilization"] do %> {trunc(sensor.latest_reading.value)} <% else %> - {Float.round(sensor.latest_reading.value, 1)} + {:erlang.float_to_binary(sensor.latest_reading.value * 1.0, + decimals: 1 + )} <% end %> <%= if sensor.sensor_unit && sensor.sensor_unit != "" do %> {sensor.sensor_unit} diff --git a/priv/static/changelog.txt b/priv/static/changelog.txt index da71b7b3..d0eb73e9 100644 --- a/priv/static/changelog.txt +++ b/priv/static/changelog.txt @@ -1,3 +1,7 @@ +2026-03-22 — Bug Fixes +* Fixed capacity reference lines showing inflated values (5-6x higher than actual) on backhaul device traffic charts +* Fixed wireless sensor values (frequency, power, etc.) displaying in scientific notation instead of standard format + 2026-03-22 — Simplified Device Types * Device type selection simplified from 10 options to 6: Server, Switch, Router, Access Point, Backhaul, and Other * Field renamed from "Device Role" to "Device Type" for clarity