fix: correct capacity calculation and wireless sensor display (#116)

- 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: graham/towerops-web#116
This commit is contained in:
Graham McIntire 2026-03-22 15:49:39 -05:00 committed by graham
parent 6c82a5cfbf
commit 2b24626baa
4 changed files with 89 additions and 46 deletions

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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