fix: prevent scientific notation in sensor value display (#114)
Radio frequencies (24 GHz) were displaying as "2.42e4 MHz" instead of "24200.0 MHz" because Float.round/2 returns a float that Phoenix converts to scientific notation for large numbers. Changed format_sensor_value/2 to use :erlang.float_to_binary/2 with decimals: 1 option, which formats as a string and avoids scientific notation. Verified: 24200.0 now displays as "24200.0" instead of "2.42e4" Reviewed-on: graham/towerops-web#114
This commit is contained in:
parent
2bb040154a
commit
3e89045f76
2 changed files with 70 additions and 20 deletions
|
|
@ -873,11 +873,42 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
|
||||
defp format_sensor_value(value, _divisor) when is_number(value) do
|
||||
# Value is already divided by divisor when stored in sensor_reading
|
||||
Float.round(value, 1)
|
||||
# Format as string to avoid scientific notation for large numbers
|
||||
:erlang.float_to_binary(value * 1.0, decimals: 1)
|
||||
end
|
||||
|
||||
defp format_sensor_value(_, _), do: "-"
|
||||
|
||||
# Format GPS coordinates to 6 decimal places (standard precision, ~0.1m accuracy)
|
||||
defp format_location(location) when is_binary(location) do
|
||||
if gps_coordinates?(location) do
|
||||
format_gps_coordinates(location)
|
||||
else
|
||||
location
|
||||
end
|
||||
end
|
||||
|
||||
defp format_location(nil), do: "N/A"
|
||||
defp format_location(_), do: "N/A"
|
||||
|
||||
defp gps_coordinates?(location) do
|
||||
String.contains?(location, ",") && String.match?(location, ~r/[-\d.]+,\s*[-\d.]+/)
|
||||
end
|
||||
|
||||
defp format_gps_coordinates(location) do
|
||||
location
|
||||
|> String.split(",")
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.map_join(", ", &format_coordinate/1)
|
||||
end
|
||||
|
||||
defp format_coordinate(coord) do
|
||||
case Float.parse(coord) do
|
||||
{num, _} -> :erlang.float_to_binary(num, decimals: 6)
|
||||
:error -> coord
|
||||
end
|
||||
end
|
||||
|
||||
defp time_ago(nil), do: "Never"
|
||||
|
||||
defp time_ago(datetime) do
|
||||
|
|
@ -1232,7 +1263,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
# For backhaul devices, add capacity reference lines
|
||||
datasets =
|
||||
if device.device.device_role == "backhaul" do
|
||||
add_capacity_datasets(datasets, all_stats, device.interfaces)
|
||||
add_capacity_datasets(datasets, all_stats, device)
|
||||
else
|
||||
datasets
|
||||
end
|
||||
|
|
@ -1242,14 +1273,21 @@ 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
|
||||
defp add_capacity_datasets(datasets, all_stats, interfaces) do
|
||||
# Build a map of interface_id -> capacity for quick lookup
|
||||
# For radios with Rx/Tx Capacity sensors, uses those values instead of if_speed
|
||||
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 =
|
||||
interfaces
|
||||
device.interfaces
|
||||
|> Enum.filter(& &1.monitored)
|
||||
|> Map.new(fn interface ->
|
||||
capacity = interface.configured_capacity_bps || interface.if_speed || 0
|
||||
{interface.id, capacity}
|
||||
# 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
|
||||
|
|
@ -1257,7 +1295,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
|
||||
# Track which interfaces have data at each timestamp
|
||||
interface_activity_by_timestamp =
|
||||
interfaces
|
||||
device.interfaces
|
||||
|> Enum.flat_map(fn interface ->
|
||||
interface.id
|
||||
|> Snmp.get_interface_stats(since: twenty_four_hours_ago, limit: 1000)
|
||||
|
|
@ -1270,34 +1308,36 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
|> 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
|
||||
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_capacity =
|
||||
{total_rx, total_tx} =
|
||||
active_interfaces
|
||||
|> Enum.uniq()
|
||||
|> Enum.reduce(0, fn iface_id, acc ->
|
||||
capacity = Map.get(interface_capacity_map, iface_id, 0)
|
||||
acc + capacity
|
||||
|> 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_capacity}
|
||||
{stat.timestamp_ms, total_rx, total_tx}
|
||||
end)
|
||||
|
||||
# Only add capacity datasets if we have at least some capacity data
|
||||
max_capacity = capacity_data |> Enum.map(fn {_, cap} -> cap end) |> Enum.max(fn -> 0 end)
|
||||
max_rx = capacity_data |> Enum.map(fn {_, rx, _} -> rx end) |> Enum.max(fn -> 0 end)
|
||||
max_tx = capacity_data |> Enum.map(fn {_, _, tx} -> tx end) |> Enum.max(fn -> 0 end)
|
||||
|
||||
if max_capacity > 0 do
|
||||
if max_rx > 0 || max_tx > 0 do
|
||||
capacity_in_data =
|
||||
Enum.map(capacity_data, fn {ts, cap} ->
|
||||
%{x: ts, y: Float.round(cap * 1.0, 2)}
|
||||
Enum.map(capacity_data, fn {ts, rx, _tx} ->
|
||||
%{x: ts, y: Float.round(rx * 1.0, 2)}
|
||||
end)
|
||||
|
||||
capacity_out_data =
|
||||
Enum.map(capacity_data, fn {ts, cap} ->
|
||||
%{x: ts, y: -Float.round(cap * 1.0, 2)}
|
||||
Enum.map(capacity_data, fn {ts, _rx, tx} ->
|
||||
%{x: ts, y: -Float.round(tx * 1.0, 2)}
|
||||
end)
|
||||
|
||||
# Add capacity datasets with special styling
|
||||
|
|
@ -1325,6 +1365,16 @@ 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(sensor) do
|
||||
if sensor.latest_reading && sensor.latest_reading.value do
|
||||
# Sensor value is in Mbps, convert to bps
|
||||
round(sensor.latest_reading.value * 1_000_000)
|
||||
end
|
||||
end
|
||||
|
||||
# Group interfaces by type for organized display
|
||||
defp group_interfaces_by_type(interfaces) do
|
||||
interfaces
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@
|
|||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Location</dt>
|
||||
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{@snmp_device.sys_location || "N/A"}
|
||||
{format_location(@snmp_device.sys_location)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue