'use strict' const snmp = require('net-snmp') const cfg = require('./config') const { log } = require('./helpers') const olts = new Map() let pollTimer = null const POLL_INTERVAL = cfg.OLT_POLL_INTERVAL || 300000 const SNMP_TIMEOUT = 4000 // --- TP-Link DeltaStream GPON OLT MIB (enterprise 11863) --- // ONU table: .6.100.1.7.2.1.{col}.0.{port}.{onu} const TPLINK = { BASE: '1.3.6.1.4.1.11863.6.100.1.7.2.1', COL_PORT_NAME: 3, COL_ONU_IDX: 4, COL_DESCRIPTION: 5, COL_SERIAL: 6, COL_LINE_PROF: 7, COL_STATUS: 8, COL_VENDOR: 15, COL_MODEL: 16, COL_UPTIME: 19, COL_HW_VER: 20, COL_RX_POWER: 26, COL_TX_POWER: 27, COL_ONU_RX: 28, COL_DISTANCE: 30, COL_TEMPERATURE: 31, COL_LAST_DOWN: 32, COL_FIRMWARE: 33, COL_OFFLINE_CAUSE: 42, COL_MAC: 43, COL_MAP: {}, FETCH_COLS: [], } TPLINK.COL_MAP = { [TPLINK.COL_PORT_NAME]: 'port', [TPLINK.COL_ONU_IDX]: 'onuIdx', [TPLINK.COL_DESCRIPTION]: 'description', [TPLINK.COL_SERIAL]: 'serial', [TPLINK.COL_VENDOR]: 'vendor', [TPLINK.COL_MODEL]: 'model', [TPLINK.COL_UPTIME]: 'uptime', [TPLINK.COL_RX_POWER]: 'rxPowerOlt', [TPLINK.COL_TX_POWER]: 'txPowerOlt', [TPLINK.COL_ONU_RX]: 'rxPowerOnu', [TPLINK.COL_DISTANCE]: 'distance', [TPLINK.COL_TEMPERATURE]: 'temperature', [TPLINK.COL_FIRMWARE]: 'firmware', [TPLINK.COL_OFFLINE_CAUSE]: 'lastOfflineCause', [TPLINK.COL_MAC]: 'mac', [TPLINK.COL_LINE_PROF]: 'lineProfile', } TPLINK.FETCH_COLS = [ TPLINK.COL_PORT_NAME, TPLINK.COL_ONU_IDX, TPLINK.COL_DESCRIPTION, TPLINK.COL_SERIAL, TPLINK.COL_STATUS, TPLINK.COL_VENDOR, TPLINK.COL_MODEL, TPLINK.COL_UPTIME, TPLINK.COL_RX_POWER, TPLINK.COL_TX_POWER, TPLINK.COL_ONU_RX, TPLINK.COL_DISTANCE, TPLINK.COL_TEMPERATURE, TPLINK.COL_FIRMWARE, TPLINK.COL_OFFLINE_CAUSE, TPLINK.COL_MAC, TPLINK.COL_LINE_PROF, ] // backward compat alias const OID = TPLINK // --- Raisecom ISCOM6800 GPON OLT MIB (enterprise 8886) --- // ONU table: 1.3.6.1.4.1.8886.18.3.1.3.1.1.{col}.{onu_id} // Signal table: 1.3.6.1.4.1.8886.18.3.1.3.3.1.1.{onu_id} (rx_power in dBm*10) // onu_id encoding: (slot+32)*8388608 + port*65536 + ontid const RAISECOM = { BASE_TABLE: '1.3.6.1.4.1.8886.18.3.1.3.1.1', BASE_SIGNAL: '1.3.6.1.4.1.8886.18.3.1.3.3.1.1', COL_SERIAL: 2, COL_DISTANCE: 6, COL_FIRMWARE: 7, COL_STATUS: 8, COL_LINE_PROF: 9, COL_DEREG_COUNT: 15, COL_ONU_RX: 16, FETCH_COLS: [2, 6, 7, 8, 9, 16], COL_MAP: { 2: 'serial', 6: 'distance', 7: 'firmware', 9: 'lineProfile', 16: 'rxPowerOnu' }, } function decodeRaisecomOnuId (onuId) { const slot = Math.floor(onuId / 8388608) - 32 const port = Math.floor((onuId % 8388608) / 65536) const ontid = onuId % 65536 return { slot, port, ontid } } function registerOlt ({ host, community, name, type }) { if (!host || !community) return if (olts.has(host)) return olts.get(host) const vendor = (type || 'tplink').toLowerCase() if (vendor !== 'tplink' && vendor !== 'raisecom') { log(`OLT ${name || host}: unknown type "${type}", defaulting to tplink`) } const olt = { host, community, name: name || host, type: (vendor === 'raisecom') ? 'raisecom' : 'tplink', lastPoll: null, lastError: null, lastDuration: null, onuCount: 0, onlineCount: 0, onus: new Map(), } olts.set(host, olt) log(`OLT registered: ${name || host} (${host}) [${olt.type}]`) return olt } const createSession = (host, community) => snmp.createSession(host, community, { timeout: SNMP_TIMEOUT, retries: 1, version: snmp.Version2c }) function snmpWalk (session, oid) { return new Promise((resolve) => { const results = [] const timer = setTimeout(() => { try { session.close() } catch {} resolve(results) }, SNMP_TIMEOUT + 2000) session.subtree(oid, 50, (varbinds) => { for (const vb of varbinds) { if (!snmp.isVarbindError(vb)) results.push(vb) } }, (error) => { clearTimeout(timer) resolve(results) }) }) } function extractVal (vb) { if (vb.type === snmp.ObjectType.OctetString) return vb.value.toString() if (vb.type === snmp.ObjectType.Integer) return vb.value return vb.value?.toString?.() || '' } function parseTplinkTable (varbinds) { const byKey = new Map() const baseLen = TPLINK.BASE.split('.').length for (const vb of varbinds) { const suffix = vb.oid.toString().split('.').slice(baseLen) if (suffix.length < 3) continue const col = parseInt(suffix[0]) const key = suffix.slice(1).join('.') if (!byKey.has(key)) byKey.set(key, {}) const entry = byKey.get(key) const val = extractVal(vb) if (col === TPLINK.COL_STATUS) { entry.status = (val === 1 || val === '1') ? 'online' : 'offline' } else if (TPLINK.COL_MAP[col]) { entry[TPLINK.COL_MAP[col]] = val } } const onus = [] for (const [key, entry] of byKey) { if (!entry.serial) continue entry._key = key onus.push(entry) } return onus } function parseRaisecomTable (tableVarbinds, signalVarbinds) { const byId = new Map() const tableBaseLen = RAISECOM.BASE_TABLE.split('.').length for (const vb of tableVarbinds) { const suffix = vb.oid.toString().split('.').slice(tableBaseLen) if (suffix.length < 2) continue const col = parseInt(suffix[0]) const onuId = parseInt(suffix[1]) if (isNaN(onuId)) continue if (!byId.has(onuId)) byId.set(onuId, { _onuId: onuId }) const entry = byId.get(onuId) const val = extractVal(vb) if (col === RAISECOM.COL_STATUS) { entry.status = (val === 1 || val === '1') ? 'online' : 'offline' } else if (RAISECOM.COL_MAP[col]) { entry[RAISECOM.COL_MAP[col]] = val } } // Parse signal table for OLT-side rx power const sigBaseLen = RAISECOM.BASE_SIGNAL.split('.').length for (const vb of signalVarbinds) { const suffix = vb.oid.toString().split('.').slice(sigBaseLen) if (suffix.length < 1) continue const onuId = parseInt(suffix[0]) if (isNaN(onuId) || !byId.has(onuId)) continue const val = extractVal(vb) // Raisecom reports rx_power in dBm * 10 byId.get(onuId).rxPowerOlt = typeof val === 'number' ? val / 10 : val } const onus = [] for (const [onuId, entry] of byId) { if (!entry.serial) continue const { slot, port, ontid } = decodeRaisecomOnuId(onuId) entry.port = `0/${slot}/${port}` entry.onuIdx = ontid entry._key = `${slot}.${port}.${ontid}` onus.push(entry) } return onus } // backward compat alias const parseOnuTable = parseTplinkTable async function pollTplink (session) { const allVarbinds = [] for (const col of TPLINK.FETCH_COLS) { const vbs = await snmpWalk(session, `${TPLINK.BASE}.${col}`) allVarbinds.push(...vbs) } return parseTplinkTable(allVarbinds) } async function pollRaisecom (olt) { // Raisecom needs a fresh session per walk (session gets closed after subtree timeout) const tableVarbinds = [] for (const col of RAISECOM.FETCH_COLS) { const s = createSession(olt.host, olt.community) const vbs = await snmpWalk(s, `${RAISECOM.BASE_TABLE}.${col}`) try { s.close() } catch {} tableVarbinds.push(...vbs) } const sigSession = createSession(olt.host, olt.community) const signalVarbinds = await snmpWalk(sigSession, RAISECOM.BASE_SIGNAL) try { sigSession.close() } catch {} return parseRaisecomTable(tableVarbinds, signalVarbinds) } async function pollOlt (olt) { const startMs = Date.now() let session try { session = createSession(olt.host, olt.community) let onus if (olt.type === 'raisecom') { session.close(); session = null onus = await pollRaisecom(olt) } else { onus = await pollTplink(session) session.close(); session = null } const prevOnus = new Map(olt.onus) olt.onus.clear() let onlineCount = 0 for (const onu of onus) { olt.onus.set(onu.serial, onu) if (onu.status === 'online') onlineCount++ const prev = prevOnus.get(onu.serial) if (prev && prev.status !== onu.status) { const event = onu.status === 'offline' ? 'olt_offline' : 'olt_online' logOnuEvent(olt, onu, event, onu.status === 'offline' ? (onu.lastOfflineCause || null) : null) } } olt.onuCount = onus.length olt.onlineCount = onlineCount olt.lastPoll = new Date().toISOString() olt.lastError = null olt.lastDuration = Date.now() - startMs log(`OLT ${olt.name}: ${onus.length} ONUs (${onlineCount} online) in ${olt.lastDuration}ms`) } catch (e) { olt.lastError = { message: e.message, ts: new Date().toISOString() } olt.lastDuration = Date.now() - startMs log(`OLT ${olt.name} SNMP FAILED (${olt.lastDuration}ms): ${e.message} — resuming next`) } finally { if (session) try { session.close() } catch {} } } async function logOnuEvent (olt, onu, event, reason) { log(`ONU ${onu.serial} on ${olt.name}: ${event}${reason ? ' (' + reason + ')' : ''}`) try { const { logDeviceEvent } = require('./devices') if (logDeviceEvent) { await logDeviceEvent(onu.serial, event, reason, { olt: olt.name, port: onu.port, mac: onu.mac, rxPowerOlt: onu.rxPowerOlt, rxPowerOnu: onu.rxPowerOnu, distance: onu.distance, lastOfflineCause: onu.lastOfflineCause, }) } } catch (e) { log(`Failed to log ONU event: ${e.message}`) } } async function pollAllOlts () { if (!olts.size) return const startMs = Date.now() let ok = 0, failed = 0 for (const [, olt] of olts) { await pollOlt(olt) olt.lastError ? failed++ : ok++ } if (failed > 0) log(`OLT poll complete: ${ok} OK, ${failed} FAILED in ${Date.now() - startMs}ms`) } function getOnuBySerial (serial) { if (!serial) return null const normalized = serial.replace(/^(TPLG)([A-F0-9])/i, '$1-$2').toUpperCase() for (const [, olt] of olts) { const enrich = (o) => ({ ...o, oltName: olt.name, oltHost: olt.host }) let onu = olt.onus.get(serial) || olt.onus.get(normalized) if (onu) return enrich(onu) for (const [s, o] of olt.onus) { if (s.includes(serial) || serial.includes(s.replace(/-/g, ''))) return enrich(o) } } return null } const getOltStats = () => [...olts.entries()].map(([host, olt]) => ({ host, name: olt.name, type: olt.type, onuCount: olt.onuCount, onlineCount: olt.onlineCount, lastPoll: olt.lastPoll, lastDuration: olt.lastDuration, lastError: olt.lastError, })) const getAllOnus = () => { const all = [] for (const [, olt] of olts) { for (const [, onu] of olt.onus) all.push({ ...onu, oltName: olt.name }) } return all } function startOltPoller () { for (const o of (cfg.OLT_LIST || [])) registerOlt(o) if (cfg.OLT_HOST && !olts.has(cfg.OLT_HOST)) { registerOlt({ host: cfg.OLT_HOST, community: cfg.OLT_COMMUNITY || 'public', name: cfg.OLT_NAME || cfg.OLT_HOST, type: cfg.OLT_TYPE }) } if (!olts.size) return log('OLT SNMP poller: no OLTs configured') log(`OLT SNMP poller: ${olts.size} OLT(s), polling every ${POLL_INTERVAL / 1000}s`) setTimeout(() => { pollAllOlts().catch(e => log('OLT poll error:', e.message)) pollTimer = setInterval(() => { pollAllOlts().catch(e => log('OLT poll error:', e.message)) }, POLL_INTERVAL) }, 10000) } function stopOltPoller () { if (pollTimer) { clearInterval(pollTimer); pollTimer = null } } module.exports = { registerOlt, startOltPoller, stopOltPoller, getOnuBySerial, getOltStats, getAllOnus, pollAllOlts, }