fix: correct airos SNMP OIDs to match UBNT-AirMAX-MIB definitions

8 of 13 OIDs were pointing to wrong MIB columns:
- RSSI: was reading ubntWlStatSignal(.5), now ubntWlStatRssi(.6)
- Quality: was reading ubntWlStatSignal(.5), now ubntAirMaxQuality(.6.1.3)
- Tx Rate: was reading ubntWlStatSsid(.2), now ubntWlStatTxRate(.9)
- Rx Rate: was reading ubntWlStatHideSsid(.3), now ubntWlStatRxRate(.10)
- Capacity: was reading ubntWlStatTxRate(.9), now ubntAirMaxCapacity(.6.1.4)
- Distance: was reading ubntWlStatSecurity(.11), now ubntRadioDistance(.1.1.7)
- Utilization: was reading WDS/Repeater booleans(.12/.13), now ubntAirMaxAirtime(.6.1.7)
- Added Signal Level sensor from ubntWlStatSignal(.5)
This commit is contained in:
Graham McIntire 2026-03-10 16:53:08 -05:00
parent 9b3ffa874a
commit 835b38c4de
No known key found for this signature in database
2 changed files with 220 additions and 81 deletions

View file

@ -2,10 +2,13 @@ defmodule Towerops.Snmp.Profiles.Vendors.Airos do
@moduledoc """
Ubiquiti AirOS device-specific SNMP handling.
Mirrors LibreNMS/OS/Airos.php for wireless sensor discovery and hardware detection.
Supported devices:
- AirMax (LiteBeam, PowerBeam, NanoStation, etc.)
OIDs sourced from UBNT-AirMAX-MIB:
- Radio table: 1.3.6.1.4.1.41112.1.4.1 (ubntRadioTable)
- WlStat table: 1.3.6.1.4.1.41112.1.4.5 (ubntWlStatTable)
- AirMax table: 1.3.6.1.4.1.41112.1.4.6 (ubntAirMaxTable)
"""
@behaviour Towerops.Snmp.Profiles.Vendors.Vendor
@ -16,7 +19,6 @@ defmodule Towerops.Snmp.Profiles.Vendors.Airos do
require Logger
# OIDs for hardware detection
# Use get_next like LibreNMS to get the first available value in the IEEE802dot11-MIB tree
# IEEE802dot11-MIB::dot11manufacturerProductName = 1.2.840.10036.3.1.2.1.3
# IEEE802dot11-MIB::dot11manufacturerProductVersion = 1.2.840.10036.3.1.2.1.4
@product_name_oid "1.2.840.10036.3.1.2.1.3"
@ -41,7 +43,6 @@ defmodule Towerops.Snmp.Profiles.Vendors.Airos do
other ->
Logger.warning("AirOS: Failed to detect hardware, got: #{inspect(other)}")
# If get_next fails, return nil (will fallback to sysDescr in Dynamic module)
nil
end
end
@ -83,7 +84,9 @@ defmodule Towerops.Snmp.Profiles.Vendors.Airos do
@impl true
def wireless_oid_defs do
[
# Frequency in MHz
# === Radio table (ubntRadioTable) - 1.3.6.1.4.1.41112.1.4.1 ===
# ubntRadioFreq - entry 4
%{
oid: "1.3.6.1.4.1.41112.1.4.1.1.4.1",
sensor_type: "frequency",
@ -91,47 +94,7 @@ defmodule Towerops.Snmp.Profiles.Vendors.Airos do
sensor_unit: "MHz",
sensor_divisor: 1
},
# Capacity in Mbps
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.9.1",
sensor_type: "rate",
sensor_descr: "Capacity",
sensor_unit: "Mbps",
sensor_divisor: 1
},
# CCQ (Client Connection Quality) %
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.7.1",
sensor_type: "ccq",
sensor_descr: "CCQ",
sensor_unit: "%",
sensor_divisor: 1
},
# Connected clients
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.15.1",
sensor_type: "clients",
sensor_descr: "Clients",
sensor_unit: "",
sensor_divisor: 1
},
# Distance in meters
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.11.1",
sensor_type: "distance",
sensor_descr: "Distance",
sensor_unit: "m",
sensor_divisor: 1
},
# Noise floor in dBm
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.8.1",
sensor_type: "noise-floor",
sensor_descr: "Noise Floor",
sensor_unit: "dBm",
sensor_divisor: 1
},
# Tx Power in dBm
# ubntRadioTxPower - entry 6
%{
oid: "1.3.6.1.4.1.41112.1.4.1.1.6.1",
sensor_type: "power",
@ -139,53 +102,99 @@ defmodule Towerops.Snmp.Profiles.Vendors.Airos do
sensor_unit: "dBm",
sensor_divisor: 1
},
# Signal Quality %
# ubntRadioDistance - entry 7 (meters, divide by 1000 for km)
%{
oid: "1.3.6.1.4.1.41112.1.4.1.1.7.1",
sensor_type: "distance",
sensor_descr: "Distance",
sensor_unit: "km",
sensor_divisor: 1000
},
# === Wireless statistics table (ubntWlStatTable) - 1.3.6.1.4.1.41112.1.4.5 ===
# ubntWlStatSignal - entry 5 (signal strength dBm)
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.5.1",
sensor_type: "quality",
sensor_descr: "Signal Quality",
sensor_unit: "%",
sensor_type: "power",
sensor_descr: "Signal Level",
sensor_unit: "dBm",
sensor_divisor: 1
},
# Tx Rate in Mbps
# ubntWlStatRssi - entry 6
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.2.1",
sensor_type: "rate",
sensor_descr: "Tx Rate",
sensor_unit: "Mbps",
sensor_divisor: 1
},
# Rx Rate in Mbps
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.3.1",
sensor_type: "rate",
sensor_descr: "Rx Rate",
sensor_unit: "Mbps",
sensor_divisor: 1
},
# RSSI in dBm
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.5.1",
oid: "1.3.6.1.4.1.41112.1.4.5.1.6.1",
sensor_type: "rssi",
sensor_descr: "RSSI",
sensor_unit: "dBm",
sensor_divisor: 1
},
# Channel utilization Tx %
# ubntWlStatCcq - entry 7
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.12.1",
sensor_type: "utilization",
sensor_descr: "Channel Utilization Tx",
oid: "1.3.6.1.4.1.41112.1.4.5.1.7.1",
sensor_type: "ccq",
sensor_descr: "CCQ",
sensor_unit: "%",
sensor_divisor: 1
},
# Channel utilization Rx %
# ubntWlStatNoiseFloor - entry 8
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.13.1",
sensor_type: "utilization",
sensor_descr: "Channel Utilization Rx",
oid: "1.3.6.1.4.1.41112.1.4.5.1.8.1",
sensor_type: "noise-floor",
sensor_descr: "Noise Floor",
sensor_unit: "dBm",
sensor_divisor: 1
},
# ubntWlStatTxRate - entry 9
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.9.1",
sensor_type: "rate",
sensor_descr: "Tx Rate",
sensor_unit: "Mbps",
sensor_divisor: 1
},
# ubntWlStatRxRate - entry 10
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.10.1",
sensor_type: "rate",
sensor_descr: "Rx Rate",
sensor_unit: "Mbps",
sensor_divisor: 1
},
# ubntWlStatStaCount - entry 15
%{
oid: "1.3.6.1.4.1.41112.1.4.5.1.15.1",
sensor_type: "clients",
sensor_descr: "Clients",
sensor_unit: "",
sensor_divisor: 1
},
# === AirMax table (ubntAirMaxTable) - 1.3.6.1.4.1.41112.1.4.6 ===
# ubntAirMaxQuality - entry 3 (percentage)
%{
oid: "1.3.6.1.4.1.41112.1.4.6.1.3.1",
sensor_type: "quality",
sensor_descr: "airMAX Quality",
sensor_unit: "%",
sensor_divisor: 1
},
# ubntAirMaxCapacity - entry 4 (percentage)
%{
oid: "1.3.6.1.4.1.41112.1.4.6.1.4.1",
sensor_type: "capacity",
sensor_descr: "Capacity",
sensor_unit: "%",
sensor_divisor: 1
},
# ubntAirMaxAirtime - entry 7 (% multiplied by 10)
%{
oid: "1.3.6.1.4.1.41112.1.4.6.1.7.1",
sensor_type: "utilization",
sensor_descr: "Airtime",
sensor_unit: "%",
sensor_divisor: 10
}
]
end

View file

@ -53,7 +53,7 @@ defmodule Towerops.Snmp.Profiles.Vendors.AirosTest do
defs = Airos.wireless_oid_defs()
assert is_list(defs)
assert length(defs) >= 10
assert length(defs) == 13
end
test "each definition has required fields" do
@ -68,23 +68,103 @@ defmodule Towerops.Snmp.Profiles.Vendors.AirosTest do
end)
end
test "includes frequency sensor" do
test "frequency uses ubntRadioFreq OID" do
defs = Airos.wireless_oid_defs()
freq = Enum.find(defs, &(&1.sensor_descr == "Frequency"))
assert freq
assert freq.oid == "1.3.6.1.4.1.41112.1.4.1.1.4.1"
assert freq.sensor_type == "frequency"
assert freq.sensor_unit == "MHz"
end
test "includes CCQ sensor" do
test "tx power uses ubntRadioTxPower OID" do
defs = Airos.wireless_oid_defs()
sensor = Enum.find(defs, &(&1.sensor_descr == "Tx Power"))
assert sensor.oid == "1.3.6.1.4.1.41112.1.4.1.1.6.1"
assert sensor.sensor_type == "power"
assert sensor.sensor_unit == "dBm"
end
test "signal level uses ubntWlStatSignal OID" do
defs = Airos.wireless_oid_defs()
sensor = Enum.find(defs, &(&1.sensor_descr == "Signal Level"))
assert sensor.oid == "1.3.6.1.4.1.41112.1.4.5.1.5.1"
assert sensor.sensor_type == "power"
assert sensor.sensor_unit == "dBm"
end
test "RSSI uses ubntWlStatRssi OID (column 6, not Signal column 5)" do
defs = Airos.wireless_oid_defs()
rssi = Enum.find(defs, &(&1.sensor_type == "rssi"))
assert rssi.oid == "1.3.6.1.4.1.41112.1.4.5.1.6.1"
assert rssi.sensor_unit == "dBm"
end
test "CCQ uses ubntWlStatCcq OID" do
defs = Airos.wireless_oid_defs()
ccq = Enum.find(defs, &(&1.sensor_descr == "CCQ"))
assert ccq
assert ccq.oid == "1.3.6.1.4.1.41112.1.4.5.1.7.1"
assert ccq.sensor_type == "ccq"
assert ccq.sensor_unit == "%"
end
test "tx/rx rate use ubntWlStatTxRate/RxRate OIDs (columns 9/10)" do
defs = Airos.wireless_oid_defs()
tx = Enum.find(defs, &(&1.sensor_descr == "Tx Rate"))
rx = Enum.find(defs, &(&1.sensor_descr == "Rx Rate"))
assert tx.oid == "1.3.6.1.4.1.41112.1.4.5.1.9.1"
assert rx.oid == "1.3.6.1.4.1.41112.1.4.5.1.10.1"
assert tx.sensor_type == "rate"
assert rx.sensor_type == "rate"
end
test "quality uses ubntAirMaxQuality OID from airMax table" do
defs = Airos.wireless_oid_defs()
quality = Enum.find(defs, &(&1.sensor_type == "quality"))
assert quality.oid == "1.3.6.1.4.1.41112.1.4.6.1.3.1"
assert quality.sensor_unit == "%"
end
test "capacity uses ubntAirMaxCapacity OID from airMax table" do
defs = Airos.wireless_oid_defs()
capacity = Enum.find(defs, &(&1.sensor_descr == "Capacity"))
assert capacity.oid == "1.3.6.1.4.1.41112.1.4.6.1.4.1"
assert capacity.sensor_type == "capacity"
assert capacity.sensor_unit == "%"
end
test "utilization uses ubntAirMaxAirtime OID with divisor 10" do
defs = Airos.wireless_oid_defs()
util = Enum.find(defs, &(&1.sensor_type == "utilization"))
assert util.oid == "1.3.6.1.4.1.41112.1.4.6.1.7.1"
assert util.sensor_divisor == 10
assert util.sensor_unit == "%"
end
test "distance uses ubntRadioDistance OID with divisor 1000" do
defs = Airos.wireless_oid_defs()
distance = Enum.find(defs, &(&1.sensor_type == "distance"))
assert distance.oid == "1.3.6.1.4.1.41112.1.4.1.1.7.1"
assert distance.sensor_divisor == 1000
assert distance.sensor_unit == "km"
end
test "no duplicate OIDs" do
defs = Airos.wireless_oid_defs()
oids = Enum.map(defs, & &1.oid)
assert length(oids) == length(Enum.uniq(oids)),
"Found duplicate OIDs: #{inspect(oids -- Enum.uniq(oids))}"
end
end
describe "discover_wireless_sensors/1" do
@ -114,5 +194,55 @@ defmodule Towerops.Snmp.Profiles.Vendors.AirosTest do
assert sensors == []
end
test "discovers sensors with correct OIDs and values" do
expect(SnmpMock, :get, 13, fn _target, oid, _opts ->
cond do
# Radio table OIDs
String.contains?(oid, "41112.1.4.1.1.4.1") -> {:ok, 5180}
String.contains?(oid, "41112.1.4.1.1.6.1") -> {:ok, 20}
String.contains?(oid, "41112.1.4.1.1.7.1") -> {:ok, 15_200}
# WlStat table OIDs
String.contains?(oid, "41112.1.4.5.1.5.1") -> {:ok, -62}
String.contains?(oid, "41112.1.4.5.1.6.1") -> {:ok, -60}
String.contains?(oid, "41112.1.4.5.1.7.1") -> {:ok, 85}
String.contains?(oid, "41112.1.4.5.1.8.1") -> {:ok, -95}
String.contains?(oid, "41112.1.4.5.1.9.1") -> {:ok, 300}
String.contains?(oid, "41112.1.4.5.1.10.1") -> {:ok, 300}
String.contains?(oid, "41112.1.4.5.1.15.1") -> {:ok, 12}
# AirMax table OIDs
String.contains?(oid, "41112.1.4.6.1.3.1") -> {:ok, 75}
String.contains?(oid, "41112.1.4.6.1.4.1") -> {:ok, 80}
String.contains?(oid, "41112.1.4.6.1.7.1") -> {:ok, 450}
true -> {:error, :no_such_object}
end
end)
sensors = Airos.discover_wireless_sensors(@client_opts)
assert length(sensors) == 13
# Verify frequency
freq = Enum.find(sensors, &(&1.sensor_type == "frequency"))
assert freq.last_value == 5180.0
# Verify RSSI uses correct OID (column 6)
rssi = Enum.find(sensors, &(&1.sensor_type == "rssi"))
assert rssi.last_value == -60.0
assert rssi.sensor_oid == "1.3.6.1.4.1.41112.1.4.5.1.6.1"
# Verify rates
rates = Enum.filter(sensors, &(&1.sensor_type == "rate"))
assert length(rates) == 2
# Verify distance with divisor
distance = Enum.find(sensors, &(&1.sensor_type == "distance"))
assert distance.sensor_divisor == 1000
assert distance.last_value == 15_200.0
# Verify utilization with divisor
util = Enum.find(sensors, &(&1.sensor_type == "utilization"))
assert util.sensor_divisor == 10
end
end
end