Enhance MikroTik discovery with comprehensive sensor support

Based on MikroTik MIB analysis, added discovery for:

Device Information:
- Board name, display name, build time

Health Sensors - Voltages:
- Core voltage, 3.3V/5V/12V supplies, input voltage

Health Sensors - Temperatures:
- Sensor chip, CPU, board, system, processor temperatures

Health Sensors - Power & Cooling:
- Power consumption (W), current draw (mA)
- Processor frequency (MHz)
- Active fan status, fan speeds (RPM)
- Power supply state, backup PSU state

POE Monitoring (per port):
- Voltage, current, power consumption
- Automatically discovers all POE-enabled ports

Optical/SFP Monitoring (per interface):
- Temperature, voltage
- TX/RX optical power levels (dBm)
- Supports fiber transceivers and SFP modules

All sensors include proper units, divisors, and descriptive names.
This commit is contained in:
Graham McIntire 2026-01-05 14:06:46 -06:00
parent 6c631cbe26
commit bf301aa5fc
No known key found for this signature in database

View file

@ -22,17 +22,39 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
# System Information (mtxrSystem)
serial_number: "1.3.6.1.4.1.14988.1.1.7.3.0",
firmware_version: "1.3.6.1.4.1.14988.1.1.7.4.0",
board_name: "1.3.6.1.4.1.14988.1.1.7.9.0",
display_name: "1.3.6.1.4.1.14988.1.1.7.8.0",
build_time: "1.3.6.1.4.1.14988.1.1.7.6.0",
license_version: "1.3.6.1.4.1.14988.1.1.4.4.0",
# Health (mtxrHealth)
temperature: "1.3.6.1.4.1.14988.1.1.3.10.0",
cpu_temperature: "1.3.6.1.4.1.14988.1.1.3.11.0",
# Health - Voltages (mtxrHealth)
core_voltage: "1.3.6.1.4.1.14988.1.1.3.1.0",
three_volt: "1.3.6.1.4.1.14988.1.1.3.2.0",
five_volt: "1.3.6.1.4.1.14988.1.1.3.3.0",
twelve_volt: "1.3.6.1.4.1.14988.1.1.3.4.0",
voltage: "1.3.6.1.4.1.14988.1.1.3.8.0",
current: "1.3.6.1.4.1.14988.1.1.3.9.0",
power_supply_state: "1.3.6.1.4.1.14988.1.1.3.12.0",
# Health - Temperatures
sensor_temperature: "1.3.6.1.4.1.14988.1.1.3.5.0",
cpu_temperature: "1.3.6.1.4.1.14988.1.1.3.6.0",
board_temperature: "1.3.6.1.4.1.14988.1.1.3.7.0",
temperature: "1.3.6.1.4.1.14988.1.1.3.10.0",
processor_temperature: "1.3.6.1.4.1.14988.1.1.3.11.0",
# Health - Power & Current
current: "1.3.6.1.4.1.14988.1.1.3.13.0",
power: "1.3.6.1.4.1.14988.1.1.3.12.0",
# Health - Fans & Cooling
active_fan: "1.3.6.1.4.1.14988.1.1.3.9.0",
fan_speed_1: "1.3.6.1.4.1.14988.1.1.3.17.0",
fan_speed_2: "1.3.6.1.4.1.14988.1.1.3.18.0",
# Health - Power Supply
processor_frequency: "1.3.6.1.4.1.14988.1.1.3.14.0",
power_supply_state: "1.3.6.1.4.1.14988.1.1.3.15.0",
backup_psu_state: "1.3.6.1.4.1.14988.1.1.3.16.0",
# System Resources
cpu_load: "1.3.6.1.2.1.25.3.3.1.2.1",
total_memory: "1.3.6.1.2.1.25.2.2.0",
@ -41,6 +63,10 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
used_hdd: "1.3.6.1.4.1.14988.1.1.1.7.0"
}
# Table OID bases for walking
@poe_table "1.3.6.1.4.1.14988.1.1.15.1"
@optical_table "1.3.6.1.4.1.14988.1.1.16.1"
@doc """
Discovers system information using Base profile and adds MikroTik-specific details.
"""
@ -118,15 +144,19 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
oids = [
@mikrotik_oids.serial_number,
@mikrotik_oids.firmware_version,
@mikrotik_oids.board_name,
@mikrotik_oids.display_name,
@mikrotik_oids.license_version
]
case Client.get_multiple(client_opts, oids) do
{:ok, [serial, firmware, license]} ->
{:ok, [serial, firmware, board, display, license]} ->
{:ok,
%{
serial_number: serial,
firmware_version: firmware,
board_name: board,
display_name: display,
license_version: license
}}
@ -137,14 +167,28 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
end
defp discover_health_sensors(client_opts) do
# Try to get all health OIDs
# Try to get all health OIDs - voltages, temperatures, power, fans
health_oids = [
{@mikrotik_oids.temperature, "temperature", "°C", 10},
# Voltages
{@mikrotik_oids.core_voltage, "core_voltage", "V", 10},
{@mikrotik_oids.three_volt, "3v3_voltage", "V", 10},
{@mikrotik_oids.five_volt, "5v_voltage", "V", 10},
{@mikrotik_oids.twelve_volt, "12v_voltage", "V", 10},
{@mikrotik_oids.voltage, "input_voltage", "V", 10},
# Temperatures
{@mikrotik_oids.sensor_temperature, "sensor_temperature", "°C", 10},
{@mikrotik_oids.cpu_temperature, "cpu_temperature", "°C", 10},
{@mikrotik_oids.voltage, "voltage", "V", 10},
{@mikrotik_oids.board_temperature, "board_temperature", "°C", 10},
{@mikrotik_oids.temperature, "temperature", "°C", 10},
{@mikrotik_oids.processor_temperature, "processor_temperature", "°C", 10},
# Power & Current
{@mikrotik_oids.current, "current", "mA", 1},
{@mikrotik_oids.power, "power", "W", 10},
# Fans
{@mikrotik_oids.fan_speed_1, "fan1", "RPM", 1},
{@mikrotik_oids.fan_speed_2, "fan2", "RPM", 1}
{@mikrotik_oids.fan_speed_2, "fan2", "RPM", 1},
# Processor
{@mikrotik_oids.processor_frequency, "processor_frequency", "MHz", 1}
]
health_oids
@ -172,8 +216,10 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
defp discover_resource_sensors(client_opts) do
cpu_sensors = discover_cpu_sensors(client_opts)
storage_sensors = discover_storage_sensors(client_opts)
poe_sensors = discover_poe_sensors(client_opts)
optical_sensors = discover_optical_sensors(client_opts)
cpu_sensors ++ storage_sensors
cpu_sensors ++ storage_sensors ++ poe_sensors ++ optical_sensors
end
defp discover_cpu_sensors(client_opts) do
@ -224,18 +270,18 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
if size > 0 do
percent = used / size * 100
{type, descr_name} =
type =
cond do
String.contains?(descr, "memory") -> {"memory", "Memory"}
String.contains?(descr, "disk") -> {"disk", "Disk"}
true -> {"storage", descr}
String.contains?(descr, "memory") -> "memory"
String.contains?(descr, "disk") -> "disk"
true -> "storage"
end
%{
sensor_type: "#{type}_usage",
sensor_index: index,
sensor_oid: used_oid,
sensor_descr: "#{descr_name} Usage",
sensor_descr: descr,
sensor_unit: "%",
sensor_divisor: 1,
last_value: percent,
@ -253,12 +299,180 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
end
end
defp format_sensor_name("temperature"), do: "Board Temperature"
defp discover_poe_sensors(client_opts) do
# Walk POE table to get per-port POE statistics
# Table columns: Name(3), Status(4), Voltage(5), Current(6), Power(7)
with {:ok, name_results} <- Client.walk(client_opts, "#{@poe_table}.1.3"),
{:ok, voltage_results} <- Client.walk(client_opts, "#{@poe_table}.1.5"),
{:ok, current_results} <- Client.walk(client_opts, "#{@poe_table}.1.6"),
{:ok, power_results} <- Client.walk(client_opts, "#{@poe_table}.1.7") do
# Get indices from the name results
indices =
name_results
|> Map.keys()
|> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end)
# Create sensors for voltage, current, and power per port
Enum.flat_map(indices, fn index ->
name_oid = "#{@poe_table}.1.3.#{index}"
voltage_oid = "#{@poe_table}.1.5.#{index}"
current_oid = "#{@poe_table}.1.6.#{index}"
power_oid = "#{@poe_table}.1.7.#{index}"
port_name = Map.get(name_results, name_oid, "Port #{index}")
voltage = Map.get(voltage_results, voltage_oid, 0)
current = Map.get(current_results, current_oid, 0)
power = Map.get(power_results, power_oid, 0)
Enum.reject(
[
if voltage > 0 do
%{
sensor_type: "poe_voltage",
sensor_index: "poe_#{index}_voltage",
sensor_oid: voltage_oid,
sensor_descr: "#{port_name} POE Voltage",
sensor_unit: "V",
sensor_divisor: 10,
last_value: voltage / 10.0,
status: "ok"
}
end,
if current > 0 do
%{
sensor_type: "poe_current",
sensor_index: "poe_#{index}_current",
sensor_oid: current_oid,
sensor_descr: "#{port_name} POE Current",
sensor_unit: "mA",
sensor_divisor: 1,
last_value: current / 1.0,
status: "ok"
}
end,
if power > 0 do
%{
sensor_type: "poe_power",
sensor_index: "poe_#{index}_power",
sensor_oid: power_oid,
sensor_descr: "#{port_name} POE Power",
sensor_unit: "W",
sensor_divisor: 10,
last_value: power / 10.0,
status: "ok"
}
end
],
&is_nil/1
)
end)
else
_ -> []
end
end
defp discover_optical_sensors(client_opts) do
# Walk optical table to get SFP/optical monitoring data
# Table columns: Name(2), Temperature(3), Voltage(5), TxBiasCurrent(6), TxPower(7), RxPower(8)
with {:ok, name_results} <- Client.walk(client_opts, "#{@optical_table}.1.2"),
{:ok, temp_results} <- Client.walk(client_opts, "#{@optical_table}.1.3"),
{:ok, voltage_results} <- Client.walk(client_opts, "#{@optical_table}.1.5"),
{:ok, tx_power_results} <- Client.walk(client_opts, "#{@optical_table}.1.7"),
{:ok, rx_power_results} <- Client.walk(client_opts, "#{@optical_table}.1.8") do
# Get indices from the name results
indices =
name_results
|> Map.keys()
|> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end)
# Create sensors for each optical metric
Enum.flat_map(indices, fn index ->
name_oid = "#{@optical_table}.1.2.#{index}"
temp_oid = "#{@optical_table}.1.3.#{index}"
voltage_oid = "#{@optical_table}.1.5.#{index}"
tx_power_oid = "#{@optical_table}.1.7.#{index}"
rx_power_oid = "#{@optical_table}.1.8.#{index}"
sfp_name = Map.get(name_results, name_oid, "SFP #{index}")
temperature = Map.get(temp_results, temp_oid)
voltage = Map.get(voltage_results, voltage_oid)
tx_power = Map.get(tx_power_results, tx_power_oid)
rx_power = Map.get(rx_power_results, rx_power_oid)
Enum.reject(
[
if temperature do
%{
sensor_type: "sfp_temperature",
sensor_index: "sfp_#{index}_temp",
sensor_oid: temp_oid,
sensor_descr: "#{sfp_name} Temperature",
sensor_unit: "°C",
sensor_divisor: 1000,
last_value: temperature / 1000.0,
status: "ok"
}
end,
if voltage do
%{
sensor_type: "sfp_voltage",
sensor_index: "sfp_#{index}_voltage",
sensor_oid: voltage_oid,
sensor_descr: "#{sfp_name} Voltage",
sensor_unit: "V",
sensor_divisor: 1000,
last_value: voltage / 1000.0,
status: "ok"
}
end,
if tx_power do
%{
sensor_type: "sfp_tx_power",
sensor_index: "sfp_#{index}_tx",
sensor_oid: tx_power_oid,
sensor_descr: "#{sfp_name} TX Power",
sensor_unit: "dBm",
sensor_divisor: 1000,
last_value: tx_power / 1000.0,
status: "ok"
}
end,
if rx_power do
%{
sensor_type: "sfp_rx_power",
sensor_index: "sfp_#{index}_rx",
sensor_oid: rx_power_oid,
sensor_descr: "#{sfp_name} RX Power",
sensor_unit: "dBm",
sensor_divisor: 1000,
last_value: rx_power / 1000.0,
status: "ok"
}
end
],
&is_nil/1
)
end)
else
_ -> []
end
end
defp format_sensor_name("core_voltage"), do: "Core Voltage"
defp format_sensor_name("3v3_voltage"), do: "3.3V Supply"
defp format_sensor_name("5v_voltage"), do: "5V Supply"
defp format_sensor_name("12v_voltage"), do: "12V Supply"
defp format_sensor_name("input_voltage"), do: "Input Voltage"
defp format_sensor_name("sensor_temperature"), do: "Sensor Chip Temperature"
defp format_sensor_name("cpu_temperature"), do: "CPU Temperature"
defp format_sensor_name("voltage"), do: "Input Voltage"
defp format_sensor_name("board_temperature"), do: "Board Temperature"
defp format_sensor_name("temperature"), do: "System Temperature"
defp format_sensor_name("processor_temperature"), do: "Processor Temperature"
defp format_sensor_name("current"), do: "Current Draw"
defp format_sensor_name("power"), do: "Power Consumption"
defp format_sensor_name("fan1"), do: "Fan 1 Speed"
defp format_sensor_name("fan2"), do: "Fan 2 Speed"
defp format_sensor_name("processor_frequency"), do: "Processor Frequency"
defp format_sensor_name(other), do: String.capitalize(other)
defp cpu_status(load) when load < 70, do: "ok"