Skip to content

OpenWrt agent: add temperature sensors and harden wireless SNMP setup#613

Open
perceival wants to merge 8 commits into
librenms:masterfrom
perceival:add-openwrt-temperature-sensors-clean
Open

OpenWrt agent: add temperature sensors and harden wireless SNMP setup#613
perceival wants to merge 8 commits into
librenms:masterfrom
perceival:add-openwrt-temperature-sensors-clean

Conversation

@perceival

@perceival perceival commented Mar 13, 2026

Copy link
Copy Markdown

This change is adding temperature sensors and improving (creating automation) in SNMP setup for WiFi sensors. Dependent on librenms/librenms#19347 (recreated after botched previous PR - #595).

Notes for reviewers:

wlInterfaces.sh

Replaces static interface mapping with live discovery from hostapd/iwinfo.
Filters inactive/placeholder VAPs and prefers active hostapd-managed interfaces.
Produces stable labels and handles duplicate SSIDs by appending band suffixes (24/5/6).
Keeps fallback logic for systems without full hostapd metadata.
wlClients.sh

Moves from simple station-dump counting to MAC-based counting with dedupe.
Supports both per-interface and aggregate modes.
Uses hostapd get_clients when available, with iwinfo fallback.
Deduplicates MLO/MLD-related addresses in aggregate mode to avoid overcounting.
wlRate.sh, wlNoiseFloor.sh, wlSNR.sh, wlFrequency.sh

Adds fallback parsing paths for client-mode interfaces where assoclist can be empty.
Normalizes output for SNMP extend compatibility (first line numeric value, optional second context line).
setup-snmpd.sh and snmpd-config-generator.sh

Introduces install/apply workflow for OpenWrt helper scripts.
Ensures managed LibreNMS blocks are replaced cleanly instead of duplicated.
Supports non-interactive apply (-y) and optional no-restart (--no-restart).
lm-sensors-pass.sh

Adds pass script implementing LM-SENSORS-MIB subtree behavior for thermal zones.
Handles both GET and GETNEXT traversal semantics for SNMP requests.
Scope check

PR remains constrained to OpenWrt integration paths with one upstream-based commit and the expected 12-file diff.

@VVelox I have recreated the PR from the scratch including your requested changes. Kindly please review.

@perceival

Copy link
Copy Markdown
Author

@VVelox could you please take a look...?

@perceival

perceival commented Mar 20, 2026

Copy link
Copy Markdown
Author

Updated the net-snmp OpenWrt integration: the distro extend now uses /etc/os-release for improved detection, and the unused snmpd-base-config file has been removed.
@VVelox kindly please review.

@perceival

Copy link
Copy Markdown
Author

Opened #618 to give this broader visibility and track the feature request separately from the implementation. The PR remains ready for review.

@perceival

Copy link
Copy Markdown
Author

@VVelox could you...?

@sshockley

Copy link
Copy Markdown
Contributor

For what it's worth I'm testing this now and it's working well.

@perceival

Copy link
Copy Markdown
Author

Friendly bump @VVelox@sshockley's report above ("testing this now and it's working well") confirms the agent works in a second OpenWrt environment beyond my own. PR is mergeable, no conflicts, CLA signed. Could you take a look when you have a minute? Happy to address any review feedback.

@laf laf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly think this needs to be done using an LibreNMS MIB and snmpd pass_persist or possibly AgentX.

Comment thread snmp/Openwrt/LICENSE Outdated
Comment thread snmp/Openwrt/snmpd-config-generator.sh Outdated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@perceival

Copy link
Copy Markdown
Author

Thanks for the review, @laf!

  • snmp/Openwrt/LICENSE — dropped (stray, agreed).
  • snmp/Openwrt/snmpd-config-generator.sh — this is the helper that generates the snmpd extend config so users don't hand-edit snmpd.conf, so its fate is really part of the config approach. If we stay on extend I'm happy to drop the generator and document the snmpd lines for users to add manually instead; if we move to pass_persist/MIB it's moot anyway. Which leads to the main question:

On MIB + pass_persist/AgentX vs the current extend approach — I'd value your steer before building, since it cascades into the server-side PR #19347 too:

  • extend is what most existing LibreNMS agent scripts use, it drops in with no extra moving parts for users, and it's been validated in two independent OpenWrt deployments (mine + @sshockley's "working well" report above).
  • That said, I'm genuinely happy to implement a proper MIB + pass_persist (or AgentX) if that's the bar for inclusion — it's just a substantial rework across both the agent and the LibreNMS-side discovery, so I'd rather confirm the direction than build it twice.

So: is pass_persist/MIB a hard requirement, or would a cleaned-up extend implementation (incl. the snmp_*SnmpQuery conversion you flagged on #19347) be acceptable? Either way I'll take it there. Thanks!

Remove snmpd-config-generator.sh and setup-snmpd.sh (per review) and add
snmp/Openwrt/README.md documenting the scripts plus the generator as an
adaptable sample, including a safe re-apply using AUTOGEN markers.
@perceival perceival requested a review from laf June 22, 2026 18:27
@Torstein-Eide

Torstein-Eide commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

@perceival

I think it should be something like this:

you can take a look at

snmp/xcp-ng-vminfo
or
SNMP: MDADM agent and mib

it can be also good to look at mibs.observium.org if there is othere wireless metric to be included

OPENWRT-MIB DEFINITIONS ::= BEGIN

IMPORTS
    MODULE-IDENTITY, OBJECT-TYPE,
    Integer32, Unsigned32, enterprises
        FROM SNMPv2-SMI
    DisplayString
        FROM SNMPv2-TC
    MODULE-COMPLIANCE, OBJECT-GROUP
        FROM SNMPv2-CONF
    InterfaceIndex
        FROM IF-MIB;

openwrtMIB MODULE-IDENTITY
    LAST-UPDATED "202606250000Z"
    ORGANIZATION "Unassigned"
    CONTACT-INFO
        "OpenWrt SNMP agent MIB.

         Enterprise OID: 1.3.6.1.4.1.60652.102.0 "
    DESCRIPTION
        "The OPENWRT-MIB describes OpenWrt wireless metrics exported by
         the LibreNMS OpenWrt SNMP extension scripts."
    REVISION "202606250000Z"
    DESCRIPTION "Initial version for LibreNMS OpenWrt wireless extends."
    ::= { enterprises 60652 102 }

-- --------------------------------------------------------------------------
-- Top-level structure
-- --------------------------------------------------------------------------

openwrtObjects     OBJECT IDENTIFIER ::= { openwrtMIB 1 }
openwrtConformance OBJECT IDENTIFIER ::= { openwrtMIB 2 }
openwrtCompliances OBJECT IDENTIFIER ::= { openwrtConformance 1 }
openwrtGroups      OBJECT IDENTIFIER ::= { openwrtConformance 2 }

--- Maybe othere openwrt metadata

-- --------------------------------------------------------------------------
-- Section 10: Wireless metadata and aggregate scalars
-- --------------------------------------------------------------------------

openwrtWireless OBJECT IDENTIFIER ::= { openwrtObjects 10 }

openwrtWirelessInterfaceCount OBJECT-TYPE
    SYNTAX      Unsigned32
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Number of active wireless interfaces"
    ::= { openwrtWireless 1 }

openwrtWirelessClientCount OBJECT-TYPE
    SYNTAX      Unsigned32
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Aggregate count of unique associated wireless clients across the
         exported interfaces.
         NET-SNMP extend source: clients-wlan"
    ::= { openwrtWireless 2 }

-- --------------------------------------------------------------------------
-- Section 10B: Wireless interface table
-- --------------------------------------------------------------------------

openwrtWirelessInterfaceTable OBJECT-TYPE
    SYNTAX      SEQUENCE OF OpenwrtWirelessInterfaceEntry
    MAX-ACCESS  not-accessible
    STATUS      current
    DESCRIPTION
        "OpenWrt wireless interfaces and per-interface metrics."
    ::= { openwrtWireless 3 }

openwrtWirelessInterfaceEntry OBJECT-TYPE
    SYNTAX      OpenwrtWirelessInterfaceEntry
    MAX-ACCESS  not-accessible
    STATUS      current
    DESCRIPTION
        "Metrics for one OpenWrt wireless interface.  Rows are indexed by
         the corresponding IF-MIB::ifIndex value."
    INDEX { openwrtWlIfaceIfIndex }
    ::= { openwrtWirelessInterfaceTable 1 }

OpenwrtWirelessInterfaceEntry ::= SEQUENCE {
    openwrtWlIfaceIfIndex    InterfaceIndex,
    openwrtWlIfaceName       DisplayString,
    openwrtWlIfaceLabel      DisplayString,
    openwrtWlIfaceClients    Unsigned32,
    openwrtWlIfaceFrequency  Unsigned32,
    openwrtWlIfaceNoiseFloor Integer32,
    openwrtWlIfaceTxRateMin  Unsigned32,
    openwrtWlIfaceTxRateAvg  Unsigned32,
    openwrtWlIfaceTxRateMax  Unsigned32,
    openwrtWlIfaceRxRateMin  Unsigned32,
    openwrtWlIfaceRxRateAvg  Unsigned32,
    openwrtWlIfaceRxRateMax  Unsigned32,
    openwrtWlIfaceSnrMin     Integer32,
    openwrtWlIfaceSnrAvg     Integer32,
    openwrtWlIfaceSnrMax     Integer32
}

openwrtWlIfaceIfIndex OBJECT-TYPE
    SYNTAX      InterfaceIndex
    MAX-ACCESS  not-accessible
    STATUS      current
    DESCRIPTION
        "Interface index from IF-MIB::ifIndex for this wireless interface."
    ::= { openwrtWirelessInterfaceEntry 1 }

openwrtWlIfaceName OBJECT-TYPE
    SYNTAX      DisplayString
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "OpenWrt wireless interface name, for example wlan0 or phy0-ap0."
    ::= { openwrtWirelessInterfaceEntry 2 }

openwrtWlIfaceLabel OBJECT-TYPE
    SYNTAX      DisplayString
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Display label for the wireless interface, normally the SSID.  If
         multiple interfaces share an SSID, wlInterfaces.sh appends a band
         suffix such as 24, 5, or 6."
    ::= { openwrtWirelessInterfaceEntry 3 }

openwrtWlIfaceClients OBJECT-TYPE
    SYNTAX      Unsigned32
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Associated client count for this wireless interface."
    ::= { openwrtWirelessInterfaceEntry 4 }

openwrtWlIfaceFrequency OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "MHz"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Current channel frequency in MHz. "
    ::= { openwrtWirelessInterfaceEntry 5 }

openwrtWlIfaceNoiseFloor OBJECT-TYPE
    SYNTAX      Integer32
    UNITS       "dBm"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Noise floor for this wireless interface in dBm. "
    ::= { openwrtWirelessInterfaceEntry 6 }

openwrtWlIfaceTxRateMin OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Minimum transmit bitrate across associated stations."
    ::= { openwrtWirelessInterfaceEntry 7 }

openwrtWlIfaceTxRateAvg OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Average transmit bitrate across associated stations. "
    ::= { openwrtWirelessInterfaceEntry 8 }

openwrtWlIfaceTxRateMax OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Maximum transmit bitrate across associated stations. "
    ::= { openwrtWirelessInterfaceEntry 9 }

openwrtWlIfaceRxRateMin OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Minimum receive bitrate across associated stations. "
    ::= { openwrtWirelessInterfaceEntry 10 }

openwrtWlIfaceRxRateAvg OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Average receive bitrate across associated stations. "
    ::= { openwrtWirelessInterfaceEntry 11 }

openwrtWlIfaceRxRateMax OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Maximum receive bitrate across associated stations. "
    ::= { openwrtWirelessInterfaceEntry 12 }

openwrtWlIfaceSnrMin OBJECT-TYPE
    SYNTAX      Integer32
    UNITS       "dB"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Minimum signal-to-noise ratio across associated stations. "
    ::= { openwrtWirelessInterfaceEntry 13 }

openwrtWlIfaceSnrAvg OBJECT-TYPE
    SYNTAX      Integer32
    UNITS       "dB"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Average signal-to-noise ratio across associated stations. "
    ::= { openwrtWirelessInterfaceEntry 14 }

openwrtWlIfaceSnrMax OBJECT-TYPE
    SYNTAX      Integer32
    UNITS       "dB"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Maximum signal-to-noise ratio across associated stations."
    ::= { openwrtWirelessInterfaceEntry 15 }


END

@perceival

Copy link
Copy Markdown
Author

Thanks @Torstein-Eide — this is exactly the steer I was hoping for, and the xcp-ng-vminfo / mdadm references are very helpful as patterns to follow.

I pulled a clean baseline walk of vanilla OpenWrt (stock snmpd, no extends) to see what we actually have to build on, and it backs your table design well:

  • Nothing wireless exists in standard MIBs. IEEE 802.11 (.1.2.840.10036) is a single stub RTID line; there's no client count, frequency, noise floor, tx/rx rate or SNR anywhere. So every column in your openwrtWirelessInterfaceTable is genuinely new data — none of it duplicates an existing MIB.
  • The VAPs are already in IF-MIB with stable ifIndexes, e.g. 34=wl0-ap0, 35=wl1-ap0, 36=wl1-ap1, 37=wl1-ap2, 41=wl1-ap1.sta1. That makes indexing the table by ifIndex a clean join onto interface rows LibreNMS already discovers — nice call.
  • Temperature is separate: LM-SENSORS-MIB (.2021.13.16) is empty on the baseline, so that rides the lm-sensors-pass.sh → LM-SENSORS-MIB emulation path (which the server PR #19347 already keys on and is working in two deployments). I'd keep that out of OPENWRT-MIB and leave it as-is — so this rework is wireless-only.

One design point so the join is solid: the table index needs to be the device's real kernel ifIndex (/sys/class/net/<iface>/ifindex), not a counter the script invents — otherwise metrics can attach to the wrong interface row. The current wlInterfaces.sh was written for the flat extend model, so I'll adapt it to emit the kernel ifIndex. Worth noting one VAP in my walk is a client/STA-mode interface (wl1-ap1.sta1); I'll make sure those round-trip too. Does that match how you indexed the mdadm table?

@VVelox @laf — before I commit to the rework (it spans both this PR and server-side #19347), could you confirm the direction? Specifically:

  1. Is the MIB + pass_persist/AgentX approach the bar for inclusion, replacing the current extend implementation? Happy to build it — just want to confirm before doing it twice.
  2. OID sub-arc: the draft sits under enterprises 60652, which is LibreNMS's registered PEN — good. Who allocates the sub-arc beneath it (Torstein's draft uses .102), and is there an existing registry of LibreNMS sub-arcs I should slot into so it doesn't collide with LIBRENMS-NOTIFICATIONS-MIB? I'll also set the MODULE-IDENTITY ORGANIZATION/CONTACT-INFO to LibreNMS rather than the "Unassigned" placeholder.

Once the direction and the sub-arc are settled I'll do the OPENWRT-MIB + pass_persist rework here and the matching table-walk discovery in #19347 in one consistent pass.

@VVelox

VVelox commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

@perceival Yeah. Sounds good to me. Especially if it manages to avoid the mess of adding lots of stuff to the config, which is something I really hate about the current implementation for OpenWRT radio stuff... have to add a entry for every item instead one that will take care of it all.

@Torstein-Eide

Torstein-Eide commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

... Does that match how you indexed the mdadm table?

No the Madam uses a 31bit hash of md UUID, since Linux reuses the same short names, even when not restarting.

IfIndex is not unique across reboots, but other mibs uses it. There is logic in LibreNMS that groups sensors by IfIndex.

@perceival

perceival commented Jun 26, 2026

Copy link
Copy Markdown
Author

Progress + a couple of things to ratify before I build the agent/server sides on top.

I took @Torstein-Eide's draft and turned it into a lint-clean MIB. It's smilint -l 6 clean (no output) and net-snmp loads the full tree. A few decisions I made, flagged so they can be confirmed rather than silently baked in:

  • Arc: { librenms 102 } (i.e. .1.3.6.1.4.1.60652.102). This follows the existing LibreNMS agent-MIB convention — XCP-NG-VMINFO-MIB sits at { librenms 100 } and { librenms 1 } is alerting, so OS/agent MIBs live in the 100-block. I kept Torstein's 102 (101 also looks free). @VVelox / @laf — could you rubber-stamp the sub-arc number so it's official before it becomes the contract both PRs depend on?
  • Node descriptor librenms (lower-case) to match LIBRENMS-NOTIFICATIONS-MIB and SMI naming rules (XCP-NG-VMINFO-MIB spells it LibreNMS, which trips smilint — avoided here).
  • Index = IF-MIB ifIndex per Torstein's guidance, so the table joins onto the standard ifTable/ifXTable rows for the same VAP.
  • Rates in Mbit/s, not bit/s. Unsigned32 in bit/s overflows on Wi-Fi 6E/7 (a BE9300 negotiates ~9.3 Gbit/s); Mbit/s keeps headroom. (Server-side WirelessRateDiscovery works in bit/s, so it'll scale on the LibreNMS end.)

One shape question before I wire it up: my baseline walk includes a client/STA-mode interface (wl1-ap1.sta1). This table is AP-centric — openwrtWlIfaceClients (and arguably the per-station rate/SNR aggregates) don't really mean anything for a station. Should client-mode interfaces be included in the table (with N/A-ish values) or omitted entirely? I lean toward omitting STA interfaces from this table; happy to do either. (See follow-up below — on reflection I'd now include them; the MIB text below reflects that, with openwrtWlIfaceClients documented as the STA peer count 0/1.)

Temperature stays out of this MIB (rides LM-SENSORS-MIB emulation as before).

Full MIB below for review:

-- **********************************************************
-- OPENWRT-MIB
--
-- Custom MIB for OpenWrt wireless metrics exported by the
-- LibreNMS OpenWrt SNMP agent (pass_persist handler).
--
-- Enterprise Number: 60652 (LibreNMS, IANA PEN)
-- OID Base: .1.3.6.1.4.1.60652
--
-- NOTE: the sub-arc { LibreNMS 102 } is PROVISIONAL, following the
-- LibreNMS agent-MIB convention where OS/agent MIBs occupy the 100-block
-- (XCP-NG-VMINFO-MIB uses { LibreNMS 100 }; { LibreNMS 1 } is alerting).
-- Pending final assignment by a LibreNMS maintainer.
--
-- Temperature/thermal data is intentionally NOT defined here; it is
-- exported separately via LM-SENSORS-MIB emulation.
-- **********************************************************

OPENWRT-MIB DEFINITIONS ::= BEGIN

IMPORTS
    MODULE-IDENTITY, OBJECT-TYPE,
    Integer32, Unsigned32, enterprises
        FROM SNMPv2-SMI
    DisplayString
        FROM SNMPv2-TC
    MODULE-COMPLIANCE, OBJECT-GROUP
        FROM SNMPv2-CONF
    InterfaceIndex
        FROM IF-MIB;

openwrtMIB MODULE-IDENTITY
    LAST-UPDATED "202606250000Z"
    ORGANIZATION "LibreNMS"
    CONTACT-INFO
        "       LibreNMS Community
                https://www.librenms.org
                https://github.com/librenms/librenms
        "
    DESCRIPTION
        "MIB module describing OpenWrt wireless metrics exported by the
         LibreNMS OpenWrt SNMP agent. The agent discovers radios/VAPs live
         (hostapd/iwinfo) at request time and serves this subtree from a
         single net-snmp pass_persist handler, so no per-interface or
         per-metric snmpd configuration is required."
    REVISION "202606250000Z"
    DESCRIPTION
        "Initial version: wireless interface table and aggregate scalars."
    ::= { librenms 102 }

-- **********************************************************
-- Top-level enterprise OID
-- **********************************************************

-- Matches the `librenms' node descriptor used by LIBRENMS-NOTIFICATIONS-MIB
-- (lower-case per SMI naming rules; XCP-NG-VMINFO-MIB spells it `LibreNMS').
librenms OBJECT IDENTIFIER ::= { enterprises 60652 }

openwrtObjects     OBJECT IDENTIFIER ::= { openwrtMIB 1 }
openwrtConformance OBJECT IDENTIFIER ::= { openwrtMIB 2 }

-- **********************************************************
-- Wireless objects
-- **********************************************************

openwrtWireless OBJECT IDENTIFIER ::= { openwrtObjects 10 }

openwrtWirelessInterfaceCount OBJECT-TYPE
    SYNTAX      Unsigned32
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Number of active wireless interfaces exported in
         openwrtWirelessInterfaceTable."
    ::= { openwrtWireless 1 }

openwrtWirelessClientCount OBJECT-TYPE
    SYNTAX      Unsigned32
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Aggregate count of unique wireless clients served by this device
         across its AP/master interfaces. MLO/MLD-related addresses are
         de-duplicated so a single client associated over multiple links counts
         once. Upstream associations on client/STA-mode interfaces are excluded
         (this device's own uplink is not one of its clients)."
    ::= { openwrtWireless 2 }

-- **********************************************************
-- Wireless interface table
-- **********************************************************

openwrtWirelessInterfaceTable OBJECT-TYPE
    SYNTAX      SEQUENCE OF OpenwrtWirelessInterfaceEntry
    MAX-ACCESS  not-accessible
    STATUS      current
    DESCRIPTION
        "OpenWrt wireless interfaces and their per-interface metrics."
    ::= { openwrtWireless 3 }

openwrtWirelessInterfaceEntry OBJECT-TYPE
    SYNTAX      OpenwrtWirelessInterfaceEntry
    MAX-ACCESS  not-accessible
    STATUS      current
    DESCRIPTION
        "Metrics for one OpenWrt wireless interface. Rows are indexed by the
         corresponding IF-MIB ifIndex, so that this table joins directly onto
         the standard interface table for the same VAP."
    INDEX { openwrtWlIfaceIfIndex }
    ::= { openwrtWirelessInterfaceTable 1 }

OpenwrtWirelessInterfaceEntry ::= SEQUENCE {
    openwrtWlIfaceIfIndex    InterfaceIndex,
    openwrtWlIfaceName       DisplayString,
    openwrtWlIfaceLabel      DisplayString,
    openwrtWlIfaceClients    Unsigned32,
    openwrtWlIfaceFrequency  Unsigned32,
    openwrtWlIfaceNoiseFloor Integer32,
    openwrtWlIfaceTxRateMin  Unsigned32,
    openwrtWlIfaceTxRateAvg  Unsigned32,
    openwrtWlIfaceTxRateMax  Unsigned32,
    openwrtWlIfaceRxRateMin  Unsigned32,
    openwrtWlIfaceRxRateAvg  Unsigned32,
    openwrtWlIfaceRxRateMax  Unsigned32,
    openwrtWlIfaceSnrMin     Integer32,
    openwrtWlIfaceSnrAvg     Integer32,
    openwrtWlIfaceSnrMax     Integer32
}

openwrtWlIfaceIfIndex OBJECT-TYPE
    SYNTAX      InterfaceIndex
    MAX-ACCESS  not-accessible
    STATUS      current
    DESCRIPTION
        "The kernel interface index (IF-MIB ifIndex) of this wireless
         interface, taken from /sys/class/net/<iface>/ifindex. Used as the
         table index so rows align with the standard ifTable/ifXTable."
    ::= { openwrtWirelessInterfaceEntry 1 }

openwrtWlIfaceName OBJECT-TYPE
    SYNTAX      DisplayString
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "OpenWrt wireless interface name, for example wlan0 or phy0-ap0."
    ::= { openwrtWirelessInterfaceEntry 2 }

openwrtWlIfaceLabel OBJECT-TYPE
    SYNTAX      DisplayString
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Display label for the wireless interface, normally the SSID. If
         multiple interfaces share an SSID, the agent appends a band suffix
         such as 24, 5 or 6 to keep labels unique."
    ::= { openwrtWirelessInterfaceEntry 3 }

openwrtWlIfaceClients OBJECT-TYPE
    SYNTAX      Unsigned32
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Number of stations associated with this wireless interface.
         For an AP/master interface this is the count of connected clients.
         For a client/STA-mode interface (e.g. a wireless uplink or backhaul)
         this is the number of upstream associations, which is its peer count:
         1 while associated to an AP, 0 when there is no peer (not associated /
         link down) - so it doubles as a backhaul link-up indicator."
    ::= { openwrtWirelessInterfaceEntry 4 }

openwrtWlIfaceFrequency OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "MHz"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Current channel centre frequency in MHz."
    ::= { openwrtWirelessInterfaceEntry 5 }

openwrtWlIfaceNoiseFloor OBJECT-TYPE
    SYNTAX      Integer32
    UNITS       "dBm"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Noise floor for this wireless interface in dBm (typically negative)."
    ::= { openwrtWirelessInterfaceEntry 6 }

openwrtWlIfaceTxRateMin OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Minimum transmit bitrate across associated stations, in Mbit/s.
         Expressed in Mbit/s rather than bit/s so that Wi-Fi 6E/7 rates
         (multi-Gbit/s) do not overflow Unsigned32."
    ::= { openwrtWirelessInterfaceEntry 7 }

openwrtWlIfaceTxRateAvg OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Average transmit bitrate across associated stations, in Mbit/s."
    ::= { openwrtWirelessInterfaceEntry 8 }

openwrtWlIfaceTxRateMax OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Maximum transmit bitrate across associated stations, in Mbit/s."
    ::= { openwrtWirelessInterfaceEntry 9 }

openwrtWlIfaceRxRateMin OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Minimum receive bitrate across associated stations, in Mbit/s."
    ::= { openwrtWirelessInterfaceEntry 10 }

openwrtWlIfaceRxRateAvg OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Average receive bitrate across associated stations, in Mbit/s."
    ::= { openwrtWirelessInterfaceEntry 11 }

openwrtWlIfaceRxRateMax OBJECT-TYPE
    SYNTAX      Unsigned32
    UNITS       "Mbit/s"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Maximum receive bitrate across associated stations, in Mbit/s."
    ::= { openwrtWirelessInterfaceEntry 12 }

openwrtWlIfaceSnrMin OBJECT-TYPE
    SYNTAX      Integer32
    UNITS       "dB"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Minimum signal-to-noise ratio across associated stations, in dB."
    ::= { openwrtWirelessInterfaceEntry 13 }

openwrtWlIfaceSnrAvg OBJECT-TYPE
    SYNTAX      Integer32
    UNITS       "dB"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Average signal-to-noise ratio across associated stations, in dB."
    ::= { openwrtWirelessInterfaceEntry 14 }

openwrtWlIfaceSnrMax OBJECT-TYPE
    SYNTAX      Integer32
    UNITS       "dB"
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Maximum signal-to-noise ratio across associated stations, in dB."
    ::= { openwrtWirelessInterfaceEntry 15 }

-- **********************************************************
-- Conformance
-- **********************************************************

openwrtCompliances OBJECT IDENTIFIER ::= { openwrtConformance 1 }
openwrtGroups      OBJECT IDENTIFIER ::= { openwrtConformance 2 }

openwrtWirelessScalarGroup OBJECT-GROUP
    OBJECTS {
        openwrtWirelessInterfaceCount,
        openwrtWirelessClientCount
    }
    STATUS  current
    DESCRIPTION
        "Aggregate wireless scalars."
    ::= { openwrtGroups 1 }

openwrtWirelessInterfaceGroup OBJECT-GROUP
    OBJECTS {
        openwrtWlIfaceName,
        openwrtWlIfaceLabel,
        openwrtWlIfaceClients,
        openwrtWlIfaceFrequency,
        openwrtWlIfaceNoiseFloor,
        openwrtWlIfaceTxRateMin,
        openwrtWlIfaceTxRateAvg,
        openwrtWlIfaceTxRateMax,
        openwrtWlIfaceRxRateMin,
        openwrtWlIfaceRxRateAvg,
        openwrtWlIfaceRxRateMax,
        openwrtWlIfaceSnrMin,
        openwrtWlIfaceSnrAvg,
        openwrtWlIfaceSnrMax
    }
    STATUS  current
    DESCRIPTION
        "Per-interface wireless metrics."
    ::= { openwrtGroups 2 }

openwrtCompliance MODULE-COMPLIANCE
    STATUS  current
    DESCRIPTION
        "Compliance statement for OpenWrt wireless monitoring agents."
    MODULE  -- this module
    MANDATORY-GROUPS {
        openwrtWirelessScalarGroup,
        openwrtWirelessInterfaceGroup
    }
    ::= { openwrtCompliances 1 }

END

@perceival

Copy link
Copy Markdown
Author

Correction on my own STA-mode lean above — having thought about the device class, I'd now argue the opposite: include STA/client-mode interfaces in the table.

On an OpenWrt AP/router a STA interface is almost always a wireless uplink/backhaul (mesh node, repeater, wireless WAN), and that's exactly the link where rate, SNR and noise floor are the most worth monitoring — arguably more than on a serving AP VAP. Practically it's just another wireless interface that needs watching. The only column that's vacuous for a STA is openwrtWlIfaceClients, which simply reports 0; everything else applies cleanly. So no MIB change needed — I'll just not filter STA rows out.

If it's useful to tell AP vs STA (vs mesh) apart on the LibreNMS side, one option is an optional openwrtWlIfaceMode column (ap/sta/mesh/monitor) — IF-MIB ifType won't distinguish them. Happy to add it or leave it out; flagging it now since we're settling the table shape. @Torstein-Eide @VVelox — any preference?

@Torstein-Eide

Copy link
Copy Markdown
Contributor
  1. For dialog, like Pull request. avoid using LLM directly. The maintainers are overload with low "quality" pull request, so a way to standout is not give the impression that one is talking to a LLM.

For SNMP mib

  1. I wound at minimum add Metadata (.0)
    a. 0.1: openwrtMIBversion
    b. 0.2: openwrtOSversion
  2. I wound add Firewall 2.
    a. metatadata.
    b. table of firewall rules
    c. table of hitrate
    d. table of active sesions.
  3. I wound add DHCP 3
    a. metadata
    b. table of dhcp servers
    c. table of dhcp clients

and so on.

@perceival

perceival commented Jun 27, 2026

Copy link
Copy Markdown
Author

ad. 1. Good point. Turning off lazy mode ;-)
Regarding MIBs:
My approach would be to have it separated into several MIBs. Like there is no point of walking through firewall rules part of MIB if you are having only dumb APs.

  • OPENWRT-MIB - that's the place where your openwrtMIBversion and openwrtOSversion belong
  • OPENWRT-WIRELESS-MIB addressing my intended scope.

Curious of your opinion.

Serves the wireless subtree (.1.3.6.1.4.1.60652.102.1, OPENWRT-WIRELESS-MIB)
from a single pass_persist registration instead of one extend per metric.
Discovers radios/VAPs live via ubus (iwinfo/hostapd) with an iwinfo CLI
fallback, and exposes a per-interface table indexed by ifIndex: clients,
frequency, noise floor, tx/rx rate (min/avg/max), SNR, channel utilisation
and tx power, plus aggregate scalars. Reuses wlInterfaces.sh and wlClients.sh
for discovery and client counting; the snapshot is built once per request and
cached so a walk does not re-run iwinfo per OID.
@Torstein-Eide

Copy link
Copy Markdown
Contributor

Yes a MIB (the files that defined what the OID means) can be split over multiple MIB files.
For the logic that resolves it yes it possible to multiple program, but I think a good solution would be one program, to handle all OpenWrt unique elements.

A program will have if or case statements that gates what do to. The cost to read some more files and and format for SNMP is tiny for most low end devices, that OpenWrt runs on. LibreNMS will also reduse what is ask for from SNMP server, once a OID is add to a sensor it will only ask for that OID until next discovery.

There is also no expatiation that you implement more that what is pressing for your need. Ie the common + wireless stuff.

@perceival

Copy link
Copy Markdown
Author

@Torstein-Eide Your is reasonable. Going with single program and multiple MIBs aproach.

iwinfo's assoclist JSON has no per-station "snr" field, so the ubus path
reported SNR 0 and an associated-station count of 0. Compute SNR per station
as signal - noise (pairing the two arrays by index) and skip stations whose
noise floor is unavailable (0); count associated stations from the signal
array. Verified on hardware: a 2.4GHz VAP with 28 clients now reports SNR
~9..63 dB, while a 5GHz radio that does not expose survey noise reports 0.
Per review (single program, multiple MIBs): rename openwrt-wireless-pass.sh ->
openwrt-snmp-pass.sh and register it once at the OpenWrt root
(.1.3.6.1.4.1.60652.102) rather than just the wireless subtree. It now also
serves the common metadata - openwrtMibVersion and openwrtOsVersion (from
/etc/os-release) - alongside wireless; future areas add OIDs to the same
OID-gated snapshot. Verified on OpenWrt (SNAPSHOT): metadata + wireless served,
walk strictly ordered from the root.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants