Files
open-isp/services/oltService.js
T
oxpoe eaf05f2811 OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor,
admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS.

Kredit & lisensi (ISC):
- Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak
  atas base kode dan repo yang bermanfaat.
- Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder.
- Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
2026-10-09 05:51:21 +07:00

2817 lines
97 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const snmp = require('net-snmp');
const net = require('net');
const Database = require('better-sqlite3');
const path = require('path');
const winston = require('winston');
const axios = require('axios');
const genieacs = require('../config/genieacs');
// Logger configuration
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});
const dbPath = path.join(__dirname, '../database/billing.db');
const db = new Database(dbPath);
/**
* Profil SNMP per brand OLT
* Setiap profil mendefinisikan OID tabel untuk status, nama, dan cara deteksi.
*/
const BRAND_PROFILES = {
hioso: [
{
name: 'HIOSO_EPON_C',
status_table: '1.3.6.1.4.1.25355.3.2.6.3.2.1.39',
name_table: '1.3.6.1.4.1.25355.3.2.6.3.2.1.37',
sn_table: '1.3.6.1.4.1.25355.3.2.6.3.2.1.11',
tx_power_table: '1.3.6.1.4.1.25355.3.2.6.14.2.1.4',
rx_power_table: '1.3.6.1.4.1.25355.3.2.6.14.2.1.8',
probe_oid: '1.3.6.1.4.1.25355.3.2.6.3.2.1.39',
/** OLT_OID_REFERENCE: integer meter (bukan per 0,1 m seperti ZTE/Huawei) */
distance_table: '1.3.6.1.4.1.25355.3.2.6.3.2.1.25',
distance_tenths_meter: false,
},
{
name: 'HIOSO_EPON_B',
status_table: '1.3.6.1.4.1.3320.101.10.1.1.26',
name_table: '1.3.6.1.4.1.3320.101.10.1.1.79',
sn_table: '1.3.6.1.4.1.3320.101.10.1.1.3',
tx_power_table: '1.3.6.1.4.1.3320.101.10.5.1.5',
rx_power_table: '1.3.6.1.4.1.3320.101.10.5.1.6',
offline_reason_table: '1.3.6.1.4.1.3320.101.11.1.1.11',
probe_oid: '1.3.6.1.4.1.3320.101.10.1.1.26',
},
{
name: 'HIOSO_GPON',
status_table: '1.3.6.1.4.1.25355.3.3.1.1.1.11',
name_table: '1.3.6.1.4.1.25355.3.3.1.1.1.2',
sn_table: '1.3.6.1.4.1.25355.3.3.1.1.1.5',
tx_power_table: '1.3.6.1.4.1.25355.3.3.1.1.4.1.2',
rx_power_table: '1.3.6.1.4.1.25355.3.3.1.1.4.1.1',
probe_oid: '1.3.6.1.4.1.25355.3.3.1.1.1.11',
},
],
hsgq: [
{
name: 'HSGQ_EPON',
status_table: '1.3.6.1.4.1.3320.101.10.1.1.26',
name_table: '1.3.6.1.4.1.3320.101.10.1.1.79',
sn_table: '1.3.6.1.4.1.3320.101.10.1.1.3',
tx_power_table: '1.3.6.1.4.1.3320.101.10.5.1.5',
rx_power_table: '1.3.6.1.4.1.3320.101.10.5.1.6',
offline_reason_table: '1.3.6.1.4.1.3320.101.11.1.1.11',
probe_oid: '1.3.6.1.4.1.3320.101.10.1.1.26',
},
],
zte: [
{
name: 'ZTE_GPON_C300',
status_table: '1.3.6.1.4.1.3902.1082.500.10.2.3.3.1.9',
name_table: '1.3.6.1.4.1.3902.1082.500.10.2.3.3.1.2',
sn_table: '1.3.6.1.4.1.3902.1082.500.10.2.3.3.1.6',
tx_power_table: '1.3.6.1.4.1.3902.1015.1010.11.2.1.1', // TX Optical power (0.01 dBm)
rx_power_table: '1.3.6.1.4.1.3902.1015.1010.11.2.1.2', // RX Optical power (0.01 dBm)
offline_reason_table: '1.3.6.1.4.1.3902.1082.500.10.2.3.8.1.7',
probe_oid: '1.3.6.1.4.1.3902.1082.500.10.2.3.3.1.9',
unauth_sn_table: '1.3.6.1.4.1.3902.1012.3.13.3.1.2',
unauth_type_table: '1.3.6.1.4.1.3902.1012.3.13.3.1.10',
distance_table: '1.3.6.1.4.1.3902.1015.1010.11.2.1.4',
distance_tenths_meter: true,
firmware_table: '1.3.6.1.4.1.3902.1015.1010.11.2.1.5',
uptime_table: '1.3.6.1.4.1.3902.1015.1010.11.2.1.6',
},
{
name: 'ZTE_GPON_C600',
status_table: '1.3.6.1.4.1.3902.1082.500.12.2.3.3.1.10',
name_table: '1.3.6.1.4.1.3902.1082.500.12.2.3.3.1.2',
sn_table: '1.3.6.1.4.1.3902.1082.500.12.2.3.3.1.3',
tx_power_table: '1.3.6.1.4.1.3902.1082.500.12.2.3.7.1.2',
rx_power_table: '1.3.6.1.4.1.3902.1082.500.12.2.3.7.1.3',
offline_reason_table: '1.3.6.1.4.1.3902.1082.500.12.2.3.8.1.7',
probe_oid: '1.3.6.1.4.1.3902.1082.500.12.2.3.3.1.10',
unauth_sn_table: '1.3.6.1.4.1.3902.1082.500.12.2.3.11.1.2',
unauth_type_table: '1.3.6.1.4.1.3902.1082.500.12.2.3.11.1.10',
},
{
name: 'ZTE_GPON_OLD',
status_table: '1.3.6.1.4.1.3902.1012.3.28.2.1.4',
name_table: '1.3.6.1.4.1.3902.1082.500.10.2.3.3.1.2',
sn_table: '1.3.6.1.4.1.3902.1082.500.10.2.3.3.1.6',
tx_power_table: '1.3.6.1.4.1.3902.1015.1010.11.2.1.1',
rx_power_table: '1.3.6.1.4.1.3902.1015.1010.11.2.1.2',
probe_oid: '1.3.6.1.4.1.3902.1012.3.28.2.1.4',
unauth_sn_table: '1.3.6.1.4.1.3902.1012.3.13.3.1.2',
}
],
vsol: [
{
name: 'VSOL_EPON',
status_table: '1.3.6.1.4.1.37950.1.1.5.13.1.1.4',
name_table: '1.3.6.1.4.1.37950.1.1.5.13.1.1.10',
sn_table: '1.3.6.1.4.1.37950.1.1.5.13.1.1.2',
tx_power_table: '1.3.6.1.4.1.37950.1.1.5.13.1.1.20', // 0.1 dBm
rx_power_table: '1.3.6.1.4.1.37950.1.1.5.13.1.1.21', // 0.1 dBm
probe_oid: '1.3.6.1.4.1.37950.1.1.5.13.1.1.4',
},
],
huawei: [
{
name: 'HUAWEI_GPON',
status_table: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.11',
name_table: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.3',
sn_table: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.9',
tx_power_table: '1.3.6.1.4.1.2011.6.128.1.1.2.46.1.3', // 0.01 dBm
rx_power_table: '1.3.6.1.4.1.2011.6.128.1.1.2.46.1.4', // 0.01 dBm
offline_reason_table: '1.3.6.1.4.1.2011.6.128.1.1.2.46.1.24',
probe_oid: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.11',
unauth_sn_table: '1.3.6.1.4.1.2011.6.128.1.1.2.45.1.4',
unauth_type_table: '1.3.6.1.4.1.2011.6.128.1.1.2.45.1.5',
distance_table: '1.3.6.1.4.1.2011.6.128.1.1.2.46.1.20',
distance_tenths_meter: true,
firmware_table: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.10',
uptime_table: '1.3.6.1.4.1.2011.6.128.1.1.2.46.1.15',
}
],
fiberhome: [
{
name: 'FIBERHOME_GPON',
status_table: '1.3.6.1.4.1.27332.1.1.1.8.1.7',
name_table: '1.3.6.1.4.1.27332.1.1.1.8.1.3',
sn_table: '1.3.6.1.4.1.27332.1.1.1.8.1.4',
tx_power_table: '1.3.6.1.4.1.27332.1.1.1.11.1.3',
rx_power_table: '1.3.6.1.4.1.27332.1.1.1.11.1.4',
probe_oid: '1.3.6.1.4.1.27332.1.1.1.8.1.7',
unauth_sn_table: '1.3.6.1.4.1.27332.1.1.1.1.1.1.10',
}
],
bdcom: [
{
name: 'BDCOM_GPON',
status_table: '1.3.6.1.4.1.3320.101.10.1.1.7',
name_table: '1.3.6.1.4.1.3320.101.10.1.1.3',
sn_table: '1.3.6.1.4.1.3320.101.10.1.1.4',
tx_power_table: '1.3.6.1.4.1.3320.101.10.3.1.3',
rx_power_table: '1.3.6.1.4.1.3320.101.10.3.1.4',
probe_oid: '1.3.6.1.4.1.3320.101.10.1.1.7',
}
],
cdata: [
{
name: 'CDATA_EPON',
status_table: '1.3.6.1.4.1.34592.1.3.100.12.1.1.1.15',
name_table: '1.3.6.1.4.1.34592.1.3.100.12.1.1.1.10',
sn_table: '1.3.6.1.4.1.34592.1.3.100.12.1.1.1.2', // LLID / MAC table (bukan .10)
tx_power_table: '1.3.6.1.4.1.34592.1.3.100.12.1.1.1.20',
rx_power_table: '1.3.6.1.4.1.34592.1.3.100.12.1.1.1.21', // 0.1 dBm
probe_oid: '1.3.6.1.4.1.34592.1.3.100.12.1.1.1.15',
}
],
generic: [
{
name: 'GENERIC_EPON_CTC',
status_table: '1.3.6.1.4.1.3320.101.10.1.1.26',
name_table: '1.3.6.1.4.1.3320.101.10.1.1.79',
sn_table: '1.3.6.1.4.1.3320.101.10.1.1.3',
tx_power_table: '1.3.6.1.4.1.3320.101.10.5.1.5',
rx_power_table: '1.3.6.1.4.1.3320.101.10.5.1.6',
probe_oid: '1.3.6.1.4.1.3320.101.10.1.1.26',
},
{
name: 'GENERIC_GPON_STD',
status_table: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.11',
name_table: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.3',
sn_table: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.9',
tx_power_table: '1.3.6.1.4.1.2011.6.128.1.1.2.46.1.3',
rx_power_table: '1.3.6.1.4.1.2011.6.128.1.1.2.46.1.4',
probe_oid: '1.3.6.1.4.1.2011.6.128.1.1.2.43.1.11',
}
]
};
// Nilai status yang dianggap "online" per brand
const ONLINE_VALUES = {
hioso: [1, 3, 4],
hsgq: [1, 3, 4],
zte: [1, 3, 'working', 'online'],
vsol: [1, 3, 4],
huawei: [5, 1, 'active', 'online'], // 5: operation
fiberhome: [1, 2, 3],
bdcom: [1, 2, 3],
cdata: [1, 3],
generic: [1, 2, 3, 4, 5, 'online', 'active', 'working'],
};
const getOnlineValues = (brandKey, profile) => {
const base = ONLINE_VALUES[brandKey] || [1, 3];
const profileName = String(profile?.name || '').toLowerCase();
const nameOid = String(profile?.name_table || '');
const isHiosoGpon = (brandKey === 'hioso' || brandKey === 'hsgq')
&& (profileName.includes('gpon') || nameOid.includes('.25355.3.3.'));
if (isHiosoGpon) return [2, 3, 4];
return base;
};
/**
* OID sistem per brand untuk mengambil metrics hardware.
* Semua diambil dengan snmp.get (bukan walk).
*/
const SYSTEM_OIDS = {
hioso: {
temp: '1.3.6.1.4.1.25355.3.2.1.1.1.0', // Suhu (°C)
cpu: '1.3.6.1.4.1.25355.3.2.1.1.2.0', // CPU Usage (%)
ram: '1.3.6.1.4.1.25355.3.2.1.1.3.0', // RAM Usage (%)
uplink_rx: '1.3.6.1.2.1.31.1.1.1.6.1', // ifHCInOctets (uplink port 1)
uplink_tx: '1.3.6.1.2.1.31.1.1.1.10.1', // ifHCOutOctets (uplink port 1)
},
hsgq: {
temp: '1.3.6.1.4.1.3320.101.11.1.12.1', // Temp sensor
cpu: '1.3.6.1.4.1.3320.101.11.1.13.1', // CPU Usage
ram: '1.3.6.1.4.1.3320.101.11.1.14.1', // RAM Usage
uplink_rx: '1.3.6.1.2.1.31.1.1.1.6.1',
uplink_tx: '1.3.6.1.2.1.31.1.1.1.10.1',
},
zte: {
temp: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.19.1.1.1', // Temp sensor (zxAnCardCpuTemp)
cpu: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.10.1.1.1', // CPU Usage
ram: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.11.1.1.1', // RAM Usage
uplink_rx: '1.3.6.1.2.1.31.1.1.1.6.1',
uplink_tx: '1.3.6.1.2.1.31.1.1.1.10.1',
},
vsol: {
temp: '1.3.6.1.4.1.37950.1.1.5.1.1.11',
cpu: '1.3.6.1.4.1.37950.1.1.5.1.1.2',
ram: '1.3.6.1.4.1.37950.1.1.5.1.1.4',
uplink_rx: '1.3.6.1.2.1.31.1.1.1.6.1',
uplink_tx: '1.3.6.1.2.1.31.1.1.1.10.1',
},
huawei: {
temp: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.13.0.0', // Suhu (°C)
cpu: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.14.0.0', // CPU (%)
ram: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.15.0.0', // RAM (%)
uplink_rx: '1.3.6.1.2.1.31.1.1.1.6.1',
uplink_tx: '1.3.6.1.2.1.31.1.1.1.10.1',
},
fiberhome: {
temp: '1.3.6.1.4.1.27332.1.1.1.9.1.11.1.1', // Suhu (°C)
cpu: '1.3.6.1.4.1.27332.1.1.1.9.1.12.1.1', // CPU (%)
ram: '1.3.6.1.4.1.27332.1.1.1.9.1.14.1.1', // RAM (%)
uplink_rx: '1.3.6.1.2.1.31.1.1.1.6.1',
uplink_tx: '1.3.6.1.2.1.31.1.1.1.10.1',
},
bdcom: {
temp: '1.3.6.1.4.1.3320.101.11.1.12.1', // Suhu (°C)
cpu: '1.3.6.1.4.1.3320.101.11.1.13.1', // CPU (%)
ram: '1.3.6.1.4.1.3320.101.11.1.14.1', // RAM (%)
uplink_rx: '1.3.6.1.2.1.31.1.1.1.6.1',
uplink_tx: '1.3.6.1.2.1.31.1.1.1.10.1',
},
cdata: {
temp: '1.3.6.1.4.1.34592.1.3.100.1.1.1.2.0',
cpu: '1.3.6.1.4.1.34592.1.3.100.1.1.1.3.0',
ram: '1.3.6.1.4.1.34592.1.3.100.1.1.1.4.0',
uplink_rx: '1.3.6.1.2.1.31.1.1.1.6.1',
uplink_tx: '1.3.6.1.2.1.31.1.1.1.10.1',
},
};
const CARD_OIDS = {
zte: {
type: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.4',
status: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.6',
ports: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.7',
cpu: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.10',
ram: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.11',
serial: '1.3.6.1.4.1.3902.1082.500.10.2.2.4.1.12',
},
huawei: {
type: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.4',
status: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.6',
ports: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.3',
cpu: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.14',
ram: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.15',
serial: '1.3.6.1.4.1.2011.6.128.1.1.2.23.1.9',
},
fiberhome: {
type: '1.3.6.1.4.1.27332.1.1.1.9.1.3',
status: '1.3.6.1.4.1.27332.1.1.1.9.1.5',
ports: '1.3.6.1.4.1.27332.1.1.1.9.1.6',
cpu: '1.3.6.1.4.1.27332.1.1.1.9.1.12',
ram: '1.3.6.1.4.1.27332.1.1.1.9.1.14',
serial: '1.3.6.1.4.1.27332.1.1.1.9.1.8',
},
bdcom: {
type: '1.3.6.1.4.1.3320.101.11.1.3',
status: '1.3.6.1.4.1.3320.101.11.1.5',
ports: '1.3.6.1.4.1.3320.101.11.1.6',
cpu: '1.3.6.1.4.1.3320.101.11.1.13',
ram: '1.3.6.1.4.1.3320.101.11.1.14',
serial: '1.3.6.1.4.1.3320.101.11.1.8',
}
};
const CARD_STATUS_MAP = {
1: 'INSERVICE',
2: 'STANDBY',
3: 'OFFLINE',
4: 'FAILED',
5: 'INIT',
};
// ─── DB CRUD ────────────────────────────────────────────────────────────────
function getAllOlts() {
return db.prepare('SELECT * FROM olts ORDER BY created_at DESC').all();
}
function getActiveOlts() {
return db.prepare('SELECT * FROM olts WHERE is_active = 1').all();
}
function getOltById(id) {
return db.prepare('SELECT * FROM olts WHERE id = ?').get(id);
}
function createOlt(data) {
const apiBase = String(data.api_base_url || '').trim();
const telnetPort = parseInt(data.telnet_port, 10);
const stmt = db.prepare(`
INSERT INTO olts (name, host, snmp_community, snmp_port, brand, description, is_active, web_user, web_password, api_base_url, telnet_port, enable_password)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
return stmt.run(
data.name,
data.host,
data.snmp_community || 'public',
data.snmp_port || 161,
data.brand || 'hioso',
data.description || '',
data.is_active !== undefined ? data.is_active : 1,
data.web_user || '',
data.web_password || '',
apiBase || null,
Number.isFinite(telnetPort) && telnetPort > 0 ? telnetPort : 23,
data.enable_password != null && String(data.enable_password).length ? String(data.enable_password) : null
);
}
function updateOlt(id, data) {
const prev = getOltById(id);
const apiBase = String(data.api_base_url || '').trim();
const telnetPort = parseInt(data.telnet_port, 10);
const enablePass = String(data.enable_password || '').trim().length > 0
? String(data.enable_password).trim()
: (prev && prev.enable_password) || null;
const stmt = db.prepare(`
UPDATE olts
SET name = ?, host = ?, snmp_community = ?, snmp_port = ?, brand = ?, description = ?, is_active = ?, web_user = ?, web_password = ?, api_base_url = ?, telnet_port = ?, enable_password = ?
WHERE id = ?
`);
return stmt.run(
data.name,
data.host,
data.snmp_community,
data.snmp_port,
data.brand,
data.description,
data.is_active ? 1 : 0,
data.web_user || '',
data.web_password || '',
apiBase || null,
Number.isFinite(telnetPort) && telnetPort > 0 ? telnetPort : 23,
enablePass,
id
);
}
function deleteOlt(id) {
return db.prepare('DELETE FROM olts WHERE id = ?').run(id);
}
// ─── SNMP HELPERS ────────────────────────────────────────────────────────────
/**
* Normalisasi OID: hapus prefix 'iso.' atau awalan '1.' yang ganda
*/
const normalizeOid = (oid) => {
if (!oid) return '';
return oid.replace(/^iso\./, '1.').replace(/^\./, '');
};
/**
* Ekstrak suffix index dari OID yang di-walk
*/
const extractIdx = (rawOid, baseOid) => {
const normRaw = normalizeOid(rawOid);
const normBase = normalizeOid(baseOid);
if (normRaw.startsWith(normBase + '.')) {
return normRaw.substring(normBase.length + 1);
}
if (normRaw.startsWith(normBase)) {
return normRaw.substring(normBase.length).replace(/^\./, '');
}
// fallback
return rawOid.split('.').slice(-1)[0];
};
/**
* Cek apakah OID yang di-return masih di bawah baseOid
*/
const oidUnderBase = (rawOid, baseOid) => {
const normRaw = normalizeOid(rawOid);
const normBase = normalizeOid(baseOid);
return normRaw.startsWith(normBase + '.') || normRaw === normBase;
};
function formatSnWithColons(val) {
if (!val || typeof val !== 'string') return val;
const clean = val.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
// Jika 12 karakter hex (MAC address format MikroTik, misal 2C334187A820 -> 2C:33:41:87:A8:20)
if (/^[0-9A-F]{12}$/.test(clean)) {
return clean.match(/.{1,2}/g).join(':');
}
// Jika 16 karakter hex (8 bytes GPON hex)
if (/^[0-9A-F]{16}$/.test(clean)) {
return clean.match(/.{1,2}/g).join(':');
}
return val;
}
const decodeSn = (val) => {
if (!val) return 'N/A';
let res = '';
if (Buffer.isBuffer(val)) {
const ascii = val.toString('utf8').replace(/\0/g, '').trim();
const looksAscii = ascii.length >= 4 && /^[\x20-\x7E]+$/.test(ascii);
res = looksAscii ? ascii.toUpperCase() : val.toString('hex').toUpperCase();
} else {
res = val.toString().toUpperCase();
}
return formatSnWithColons(res);
};
/**
* SN di SNMP sering jadi hex rapat (mis. 88D2742B900D); di CLI OLT bisa ditampilkan bertitik dua (88:D2:74:2B:90:0D).
* Untuk perintah otorisasi, keduanya harus jadi bentuk yang sama (hex huruf besar tanpa separator, atau SN GPON vendor).
*/
function normalizeSnForOltProvision(sn) {
const raw = String(sn == null ? '' : sn).trim();
if (!raw) return raw;
const alnum = raw.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
if (/^[A-F0-9]+$/.test(alnum) && (alnum.length === 12 || alnum.length === 16)) return alnum;
if (/^[A-Z]{4}/.test(alnum) && alnum.length >= 8) return alnum;
return alnum;
}
const decodeUptime = (ticks) => {
if (!ticks) return 'N/A';
let seconds = Math.floor(ticks / 100);
const days = Math.floor(seconds / 86400);
seconds %= 86400;
const hours = Math.floor(seconds / 3600);
seconds %= 3600;
const minutes = Math.floor(seconds / 60);
return `${days}d ${hours}h ${minutes}m`;
};
const hiosoOnuIdFromIndex = (index) => {
if (index == null) return null;
const s = String(index).trim();
if (!s) return null;
if (s.includes('.')) {
const parts = s.split('.').filter(Boolean);
if (parts.length >= 2) {
const onu = parseInt(parts[parts.length - 1], 10);
const port = parseInt(parts[parts.length - 2], 10);
if (Number.isFinite(port) && Number.isFinite(onu)) return `0/${port}:${onu}`;
}
return null;
}
const intIdx = parseInt(s, 10);
if (!Number.isFinite(intIdx)) return null;
let port = (intIdx >> 16) & 0xff;
if (port === 0 || port > 16) port = (intIdx >> 8) & 0xff;
const onu = intIdx & 0xff;
if (port === 0) return null;
return `0/${port}:${onu}`;
};
const telnetReadUntil = (socket, matcher, timeoutMs) => {
return new Promise((resolve, reject) => {
let buf = '';
const onData = (chunk) => {
const s = chunk.toString('utf8');
buf += s;
if (buf.includes('--More--') || buf.includes('--Press Enter--') || buf.includes('Enter Key To Continue')) {
socket.write(' ');
}
if (typeof matcher === 'function' ? matcher(buf) : matcher.test(buf)) {
cleanup();
resolve(buf);
}
};
const onErr = (e) => { cleanup(); reject(e); };
const onClose = () => { cleanup(); reject(new Error('Telnet connection closed')); };
const t = setTimeout(() => { cleanup(); reject(new Error('Telnet timeout')); }, timeoutMs);
const cleanup = () => {
clearTimeout(t);
socket.off('data', onData);
socket.off('error', onErr);
socket.off('close', onClose);
};
socket.on('data', onData);
socket.on('error', onErr);
socket.on('close', onClose);
});
};
const telnetLoginAndRun = async (host, user, pass, commands, opts = {}) => {
const telnetPort = Number(opts.port) > 0 ? Number(opts.port) : 23;
const enablePassword = opts.enablePassword != null && String(opts.enablePassword).length > 0
? String(opts.enablePassword)
: null;
const socket = new net.Socket();
socket.setTimeout(15000);
await new Promise((resolve, reject) => {
socket.connect(telnetPort, host, resolve);
socket.once('error', reject);
});
const promptRe = /[>#]\s*$/m;
const loginRe = /(login|username)\s*[:>]\s*$/im;
const passRe = /(password|passwd)\s*[:>]\s*$/im;
let banner = '';
try {
banner = await telnetReadUntil(socket, (b) => loginRe.test(b) || passRe.test(b) || promptRe.test(b), 8000);
} catch (e) {
socket.destroy();
throw e;
}
if (loginRe.test(banner)) {
socket.write(String(user || 'admin') + '\r\n');
await telnetReadUntil(socket, passRe, 8000);
socket.write(String(pass || 'admin') + '\r\n');
await telnetReadUntil(socket, promptRe, 8000);
} else if (passRe.test(banner)) {
socket.write(String(pass || 'admin') + '\r\n');
await telnetReadUntil(socket, promptRe, 8000);
} else if (promptRe.test(banner)) {
} else {
socket.destroy();
throw new Error('Telnet prompt not detected');
}
if (enablePassword) {
try {
socket.write('enable\r\n');
const enBuf = await telnetReadUntil(socket, (b) => /password\s*[:>]\s*$/im.test(b) || promptRe.test(b), 12000);
if (/password\s*[:>]\s*$/im.test(enBuf)) {
socket.write(enablePassword + '\r\n');
await telnetReadUntil(socket, promptRe, 12000);
}
} catch (e) {
socket.destroy();
throw new Error('Telnet enable gagal: ' + (e.message || String(e)));
}
}
let cmdList = Array.isArray(commands) ? [...commands] : [];
while (cmdList.length && /^enable\s*$/i.test(String(cmdList[0]).trim())) {
cmdList.shift();
}
const outputs = [];
for (const cmd of cmdList) {
socket.write(cmd + '\r\n');
const out = await telnetReadUntil(socket, promptRe, 15000);
outputs.push(out);
}
socket.end();
socket.destroy();
return outputs.join('\n');
};
const parseHiosoOnuTable = (text) => {
if (!text) return [];
const lines = String(text).split(/\r?\n/);
const rows = [];
for (const line of lines) {
const t = line.trim();
if (!/^0\/\d+:\d+/.test(t)) continue;
const parts = t.split(/\s+/);
if (parts.length >= 20) {
// 22-column legacy format
const id = parts[0];
const name = parts[1];
const mac = parts[2];
const status = parts[3];
const fwVersion = parts[4];
const chipId = parts[5];
const ports = parts[6];
const rtt = parts[7];
const distance = parts[8];
const ctcStatus = parts[9];
const ctcVer = parts[10];
const activate = parts[11];
const temperature = parts[12];
const txPower = parts[13];
const rxPower = parts[14];
const onlineTime = parts[15] + ' ' + parts[16];
const offlineTime = parts[17] + ' ' + parts[18];
const offlineReason = parts[19];
const deregisterCnt = parts[parts.length - 1];
const online = parts.slice(20, parts.length - 1).join(' ');
rows.push({
id,
name: name === 'NA' ? null : name,
mac: formatSnWithColons(mac),
status,
fwVersion,
chipId,
ports,
rtt,
distance,
ctcStatus,
ctcVer,
activate,
temperature,
txPower,
rxPower,
onlineTime,
offlineTime,
offlineReason,
online,
deregisterCnt,
});
} else if (parts.length >= 10) {
// 12-column modern format (HA7304 / HA7308)
// OnuId MacAddress Status Firmware ChipId Ge Fe Pots CtcStatus CtcVer Activate Uptime [Name]
const id = parts[0];
const mac = parts[1];
const status = parts[2];
const fwVersion = parts[3];
const chipId = parts[4];
const ports = (parts[5] || '0') + '/' + (parts[6] || '0');
const ctcStatus = parts[8];
const ctcVer = parts[9];
const activate = parts[10];
const uptime = parts[11];
const name = parts.length > 12 ? parts.slice(12).join(' ') : null;
rows.push({
id,
name: name === 'NA' || !name ? null : name,
mac,
status,
fwVersion,
chipId,
ports,
distance: '-',
ctcStatus,
ctcVer,
activate,
uptime,
txPower: 'N/A',
rxPower: 'N/A'
});
}
}
return rows;
};
const fetchHiosoOnuDetailViaTelnet = async (olt) => {
const user = olt.web_user || 'admin';
const pass = olt.web_password || 'admin';
const cmds = [
'show onu info epon 0/1 all',
'show onu info epon 0/2 all',
'show onu info epon 0/3 all',
'show onu info epon 0/4 all',
'show onu',
'show onu all',
'show onu info',
'show onu status',
];
try {
const out = await telnetLoginAndRun(olt.host, user, pass, cmds, {
port: olt.telnet_port || 23,
enablePassword: olt.enable_password || pass || 'admin'
});
const rows = parseHiosoOnuTable(out);
return rows.length > 0 ? rows : null;
} catch (e) {
return null;
}
};
// ─── CORE SNMP FUNCTIONS ─────────────────────────────────────────────────────
const slowWalk = async (session, baseOid, maxEntries = 5000) => {
const results = {};
let walkCount = 0;
return new Promise((resolve) => {
session.subtree(
baseOid,
20, // maxRepetitions (GETBULK size)
(varbinds) => {
for (const vb of varbinds) {
if (!vb || vb.oid == null) continue;
if (snmp.isVarbindError(vb)) continue;
if (!oidUnderBase(vb.oid, baseOid)) continue;
const idx = extractIdx(vb.oid, baseOid);
results[idx] = vb.value;
walkCount++;
if (walkCount >= maxEntries) {
break;
}
}
},
(error) => {
// Resolve results when done, ignoring errors
resolve(results);
}
);
});
};
const walkSample = async (session, baseOid, maxItems = 3) => {
let currentOid = baseOid;
let count = 0;
const values = [];
while (count < maxItems) {
try {
const vb = await new Promise((rv, rj) => {
session.getNext([currentOid], (err, vbs) => {
if (err) rj(err);
else rv(vbs[0]);
});
});
if (!vb || vb.oid == null) break;
if (vb.type === snmp.ObjectType.EndOfMibView || vb.type === snmp.ObjectType.NoSuchObject || vb.type === snmp.ObjectType.NoSuchInstance) break;
if (!oidUnderBase(vb.oid, baseOid)) break;
values.push(vb.value);
currentOid = normalizeOid(vb.oid);
count++;
} catch (e) {
break;
}
}
return values;
};
/**
* Test apakah sebuah OID probe memberikan respons SNMP getNext yang valid
*/
const probeOid = async (session, oid) => {
try {
const vb = await new Promise((rv, rj) => {
session.getNext([oid], (err, vbs) => {
if (err) rj(err);
else rv(vbs[0]);
});
});
if (!vb || vb.type === snmp.ObjectType.EndOfMibView || vb.type === snmp.ObjectType.NoSuchObject) return false;
// Cek apakah hasil masih di bawah OID ini atau sub-treenya ada data
return oidUnderBase(vb.oid, oid);
} catch (e) {
return false;
}
};
/**
* Ambil satu atau beberapa OID sekaligus menggunakan snmp.get.
* Kembalikan array nilai (atau null jika error/tidak ada).
*/
const snmpGet = async (session, oids) => {
try {
const vbs = await new Promise((rv, rj) => {
session.get(oids, (err, result) => {
if (err) rj(err);
else rv(result);
});
});
return vbs.map(vb => {
if (!vb || vb.type === snmp.ObjectType.NoSuchObject ||
vb.type === snmp.ObjectType.NoSuchInstance ||
vb.type === snmp.ObjectType.EndOfMibView) return null;
return vb.value;
});
} catch (e) {
return oids.map(() => null);
}
};
const bufferToInt = (val) => {
if (val == null) return null;
if (typeof val === 'number') return val;
if (typeof val === 'bigint') {
if (val <= BigInt(Number.MAX_SAFE_INTEGER) && val >= BigInt(Number.MIN_SAFE_INTEGER)) return Number(val);
return null;
}
if (typeof val === 'string') {
const n = Number(val);
return Number.isFinite(n) ? n : null;
}
if (Buffer.isBuffer(val)) {
if (val.length === 1) return val.readUInt8(0);
if (val.length === 2) return val.readInt16BE(0);
if (val.length === 4) return val.readInt32BE(0);
if (val.length === 8) {
try {
const bi = val.readBigUInt64BE(0);
if (bi <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(bi);
} catch (e) {}
return null;
}
const s = val.toString();
const n = Number(s);
return Number.isFinite(n) ? n : null;
}
return null;
};
const bufferToCounterBigInt = (val) => {
if (val == null) return null;
if (typeof val === 'bigint') return val;
if (typeof val === 'number') {
if (!Number.isFinite(val)) return null;
return BigInt(Math.max(0, Math.floor(val)));
}
if (typeof val === 'string') {
try {
if (val.trim() === '') return null;
return BigInt(val);
} catch (e) {
return null;
}
}
if (Buffer.isBuffer(val)) {
if (val.length === 8) {
try { return val.readBigUInt64BE(0); } catch (e) { return null; }
}
if (val.length === 4) {
try { return BigInt(val.readUInt32BE(0)); } catch (e) { return null; }
}
const s = val.toString();
try { return BigInt(s); } catch (e) { return null; }
}
return null;
};
const getByIdx = (map, idx) => {
if (!map) return undefined;
if (map[idx] != null) return map[idx];
const keys = Object.keys(map);
if (keys.length === 0) return undefined;
const idxParts = String(idx).split('.').filter(Boolean);
const maxSegments = Math.min(6, idxParts.length);
for (let seg = 2; seg <= maxSegments; seg++) {
const suffix = '.' + idxParts.slice(-seg).join('.');
const matches = keys.filter(k => k.endsWith(suffix));
if (matches.length === 1) return map[matches[0]];
}
return undefined;
};
const toSigned16 = (n) => {
if (!Number.isFinite(n)) return null;
const u = n & 0xffff;
return u > 0x7fff ? u - 0x10000 : u;
};
const pickFirstPlausible = (candidates) => {
for (const v of candidates) {
if (!Number.isFinite(v)) continue;
if (v < -50 || v > 10) continue;
if (v > 0) continue;
return v;
}
return null;
};
const computeRxDbm = (brand, raw) => {
const signal = parseSignal(raw);
if (signal != null) return signal;
const n = bufferToInt(raw);
if (n == null) return null;
if (n === 0 || n === 65535) return null;
const signed = (n >= 0 && n <= 65535) ? toSigned16(n) : n;
if (signed == null) return null;
if (typeof signed === 'number' && signed < 0 && signed >= -60 && signed <= 10) return signed;
const ordered = brand === 'huawei'
? [signed / 100, signed / 10, signed / 1000, (signed / 100) - 100, (signed / 10) - 100]
: [signed / 10, signed / 100, signed / 1000, (signed / 10) - 100, (signed / 100) - 100];
const picked = pickFirstPlausible(ordered);
if (picked != null) return picked;
const fallback = pickFirstPlausible([signed / 10, signed / 100, signed / 1000]);
return fallback;
};
const safeToString = (val) => {
if (val == null) return null;
if (typeof val === 'string') return val;
if (typeof val === 'number') return String(val);
if (typeof val === 'bigint') return val.toString();
if (Buffer.isBuffer(val)) return val.toString();
return String(val);
};
const walkValues = async (session, baseOid) => Object.values(await slowWalk(session, baseOid));
const isLikelySn = (sample) => {
if (sample == null) return false;
let s = safeToString(sample);
if (!s) return false;
s = s.replace(/["']/g, '').replace(/\s+/g, '').trim();
if (s.length < 6) return false;
if (/(registered|offline|active|online|down|up|power|alarm)/i.test(s)) return false;
return true;
};
const pickSnTable = async (session, activeProfile) => {
if (activeProfile.sn_table) {
try {
const m = await slowWalk(session, activeProfile.sn_table);
if (m && Object.keys(m).length > 0) {
return { oid: activeProfile.sn_table, map: m };
}
} catch (e) {}
}
const nameOid = activeProfile.name_table || '';
const lastDot = nameOid.lastIndexOf('.');
const parentBranch = lastDot > 0 ? nameOid.slice(0, lastDot) : nameOid;
const candidates = [
activeProfile.sn_table,
`${parentBranch}.11`,
'1.3.6.1.4.1.25355.3.2.10.1.1.2',
`${parentBranch}.12`,
`${parentBranch}.2`,
'1.3.6.1.4.1.25355.3.2.1.2.1.2',
'1.3.6.1.4.1.25355.3.2.6.1.1.18',
'1.3.6.1.4.1.25355.3.2.6.3.2.1.12',
'1.3.6.1.4.1.25355.3.2.6.1.1.2.1.6',
'1.3.6.1.4.1.25355.3.3.1.1.1.5',
'1.3.6.1.4.1.3320.101.10.1.1.3',
].filter(Boolean);
const seen = new Set();
const unique = [];
for (const oid of candidates) {
const n = normalizeOid(oid);
if (seen.has(n)) continue;
seen.add(n);
unique.push(oid);
}
for (const oid of unique) {
const samples = await walkSample(session, oid, 2);
if (samples.length === 0) continue;
if (!isLikelySn(samples[0])) continue;
const m = await slowWalk(session, oid);
return { oid, map: m };
}
return { oid: activeProfile.sn_table || null, map: {} };
};
const parseSignal = (val) => {
if (val == null) return null;
if (typeof val === 'number') return Number.isFinite(val) ? val : null;
if (typeof val === 'bigint') return null;
const s = safeToString(val);
if (!s) return null;
const m = s.match(/[-+]?\d*\.?\d+/);
if (!m) return null;
const num = Number(m[0]);
if (!Number.isFinite(num)) return null;
const abs = Math.abs(num);
if (abs > 500) return num / 100;
if (abs > 50) return num / 10;
return num;
};
const getHrCpuPercent = async (session) => {
const values = await walkValues(session, '1.3.6.1.2.1.25.3.3.1.2');
const nums = values.map(v => bufferToInt(v)).filter(v => Number.isFinite(v));
if (nums.length === 0) return null;
const avg = nums.reduce((s, v) => s + v, 0) / nums.length;
return Math.max(0, Math.min(100, Math.round(avg)));
};
const getHrRamPercent = async (session) => {
// Fast path: try direct GET on common hrStorage indices first (1, 3, 6, 7)
// hrStorageSize and hrStorageUsed with allocation unit 1024 bytes
const fastOids = [
'1.3.6.1.2.1.25.2.2.0', // hrMemorySize (total KB)
'1.3.6.1.2.1.25.2.3.1.4.1', // hrStorageAllocationUnits.1
'1.3.6.1.2.1.25.2.3.1.5.1', // hrStorageSize.1
'1.3.6.1.2.1.25.2.3.1.6.1', // hrStorageUsed.1
'1.3.6.1.2.1.25.2.3.1.2.1', // hrStorageType.1 (OID value)
];
try {
const vbs = await snmpGet(session, fastOids);
const totalKb = bufferToInt(vbs[0]);
const allocUnit = bufferToInt(vbs[1]);
const size = bufferToInt(vbs[2]);
const used = bufferToInt(vbs[3]);
if (Number.isFinite(totalKb) && totalKb > 0 &&
Number.isFinite(allocUnit) && allocUnit > 0 &&
Number.isFinite(size) && size > 0 &&
Number.isFinite(used)) {
// size/used are in allocation units
const usedKb = (used * allocUnit) / 1024;
const pct = Math.round((usedKb / totalKb) * 100);
return Math.max(0, Math.min(100, pct));
}
// Fallback: size as a percentage directly
if (Number.isFinite(size) && size > 0 && Number.isFinite(used)) {
return Math.max(0, Math.min(100, Math.round((used / size) * 100)));
}
} catch (e) {
// fall through to slow path
}
// Slow path: full walk (for other brands)
const descrMap = await slowWalk(session, '1.3.6.1.2.1.25.2.3.1.3');
const typeMap = await slowWalk(session, '1.3.6.1.2.1.25.2.3.1.2');
const sizeMap = await slowWalk(session, '1.3.6.1.2.1.25.2.3.1.5');
const usedMap = await slowWalk(session, '1.3.6.1.2.1.25.2.3.1.6');
const hrStorageRamType = '1.3.6.1.2.1.25.2.1.2';
const indices = new Set([
...Object.keys(descrMap),
...Object.keys(typeMap),
...Object.keys(sizeMap),
...Object.keys(usedMap),
]);
let best = null;
for (const idx of indices) {
const descr = (safeToString(descrMap[idx]) || '').toLowerCase();
const type = safeToString(typeMap[idx]) || '';
const size = bufferToInt(sizeMap[idx]);
const used = bufferToInt(usedMap[idx]);
if (!Number.isFinite(size) || !Number.isFinite(used) || size <= 0) continue;
const isRam = type.includes(hrStorageRamType) || descr.includes('ram') || descr.includes('memory') || descr.includes('mem');
if (!isRam) continue;
const pct = Math.round((used / size) * 100);
const entry = { pct, size, used };
if (!best) best = entry;
else if (entry.size > best.size) best = entry;
}
if (!best) return null;
return Math.max(0, Math.min(100, best.pct));
};
const getEntityTemperatureC = async (session) => {
const typeMap = await slowWalk(session, '1.3.6.1.2.1.99.1.1.1.1');
const valueMap = await slowWalk(session, '1.3.6.1.2.1.99.1.1.1.4');
const precisionMap = await slowWalk(session, '1.3.6.1.2.1.99.1.1.1.3');
const indices = new Set([
...Object.keys(typeMap),
...Object.keys(valueMap),
...Object.keys(precisionMap),
]);
const candidates = [];
for (const idx of indices) {
const t = bufferToInt(typeMap[idx]);
if (t !== 8) continue;
const v = bufferToInt(valueMap[idx]);
const p = bufferToInt(precisionMap[idx]);
if (!Number.isFinite(v) || !Number.isFinite(p)) continue;
const c = v / Math.pow(10, p);
if (Number.isFinite(c) && c >= -20 && c <= 120) candidates.push(c);
}
if (candidates.length === 0) return null;
candidates.sort((a, b) => b - a);
return candidates[0];
};
const autoPickUplinkCandidates = async (session) => {
const ifNameMap = await slowWalk(session, '1.3.6.1.2.1.31.1.1.1.1');
const ifDescrMap = await slowWalk(session, '1.3.6.1.2.1.2.2.1.2');
const ifOperMap = await slowWalk(session, '1.3.6.1.2.1.2.2.1.8');
const ifHighSpeedMap = await slowWalk(session, '1.3.6.1.2.1.31.1.1.1.15');
const ifSpeedMap = await slowWalk(session, '1.3.6.1.2.1.2.2.1.5');
const indices = new Set([
...Object.keys(ifNameMap),
...Object.keys(ifDescrMap),
...Object.keys(ifOperMap),
...Object.keys(ifHighSpeedMap),
...Object.keys(ifSpeedMap),
]);
const candidates = [];
for (const idx of indices) {
const name = (safeToString(ifNameMap[idx]) || safeToString(ifDescrMap[idx]) || '').toLowerCase();
const oper = bufferToInt(ifOperMap[idx]);
const hs = bufferToInt(ifHighSpeedMap[idx]);
const sp = bufferToInt(ifSpeedMap[idx]);
const speedScore = Number.isFinite(hs) && hs > 0 ? hs : (Number.isFinite(sp) ? (sp / 1_000_000) : 0);
let nameScore = 0;
if (name.includes('uplink')) nameScore += 50;
if (name.includes('trunk')) nameScore += 30;
if (name.includes('xge')) nameScore += 25;
if (name.includes('10g')) nameScore += 25;
if (name.includes('ge')) nameScore += 10;
if (name.includes('eth')) nameScore += 5;
if (name.includes('gpon') || name.includes('epon') || name.includes('pon') || name.includes('onu') || name.includes('llid')) nameScore -= 35;
if (name.includes('mgmt') || name.includes('loopback') || name.includes('null')) nameScore -= 50;
const operScore = oper === 1 ? 20 : 0;
const score = nameScore + operScore + Math.min(100, speedScore);
candidates.push({ idx, score, name });
}
candidates.sort((a, b) => b.score - a.score);
return candidates;
};
const autoPickUplinkIfIndex = async (session) => {
const candidates = await autoPickUplinkCandidates(session);
return candidates[0]?.idx || null;
};
const uplinkCache = new Map();
const pickUplinkIfIndexByTraffic = async (session, oltId) => {
if (oltId) {
const now = Date.now();
const cached = uplinkCache.get(oltId);
if (cached && (now - cached.timestamp < 3600000)) { // 1 hour cache
return cached.ifIndex;
}
}
const candidates = await autoPickUplinkCandidates(session);
if (candidates.length === 0) return null;
const top = candidates.slice(0, 6);
let best = null;
for (const c of top) {
const first = await readInterfaceOctets(session, c.idx);
await new Promise(rv => setTimeout(rv, 100)); // Jedah scan lebih cepat (100ms)
const second = await readInterfaceOctets(session, c.idx);
const wrap64 = BigInt(1) << BigInt(64);
let sum = BigInt(0);
let ok = false;
if (first.rx != null && second.rx != null) {
let d = second.rx - first.rx;
if (d < BigInt(0)) d = d + wrap64;
if (d > BigInt(0)) ok = true;
sum += d;
}
if (first.tx != null && second.tx != null) {
let d = second.tx - first.tx;
if (d < BigInt(0)) d = d + wrap64;
if (d > BigInt(0)) ok = true;
sum += d;
}
if (!ok) continue;
if (!best || sum > best.sum) best = { idx: c.idx, sum };
}
const ifIndex = best?.idx || candidates[0].idx;
if (oltId && ifIndex) {
uplinkCache.set(oltId, { ifIndex, timestamp: Date.now() });
}
return ifIndex;
};
const readInterfaceOctets = async (session, ifIndex) => {
const [hcIn, hcOut] = await snmpGet(session, [
`1.3.6.1.2.1.31.1.1.1.6.${ifIndex}`,
`1.3.6.1.2.1.31.1.1.1.10.${ifIndex}`,
]);
let rx = bufferToCounterBigInt(hcIn);
let tx = bufferToCounterBigInt(hcOut);
if (rx == null || tx == null) {
const [in32, out32] = await snmpGet(session, [
`1.3.6.1.2.1.2.2.1.10.${ifIndex}`,
`1.3.6.1.2.1.2.2.1.16.${ifIndex}`,
]);
if (rx == null) rx = bufferToCounterBigInt(in32);
if (tx == null) tx = bufferToCounterBigInt(out32);
}
return {
rx,
tx,
};
};
/**
* Ambil CPU, RAM, Suhu modul optik, dan Uptime via Telnet CLI untuk Hioso / HSGQ.
* Berfungsi sebagai fallback handal jika OLT tidak mendukung OID Suhu/CPU vendor via SNMP.
*/
const fetchHiosoSystemMetricsViaTelnet = async (olt, stats) => {
if (!olt || !olt.host) return;
const user = olt.web_user || 'admin';
const pass = olt.web_password || 'admin';
const cmds = [
'show cpu resource',
'show epon optical-ddm 0/1'
];
try {
const out = await telnetLoginAndRun(olt.host, user, pass, cmds, {
port: olt.telnet_port || 23,
enablePassword: olt.enable_password || pass || 'admin'
});
if (!out) return;
if (stats.cpu === 'N/A' || !stats.cpu || stats.cpu === '-') {
const cpuMatch = out.match(/CPU usage\s*:\s*(\d+)%/i);
if (cpuMatch) stats.cpu = `${cpuMatch[1]}%`;
}
if (stats.ram === 'N/A' || !stats.ram || stats.ram === '-') {
const ramMatch = out.match(/Memory usage\s*:\s*([\d\.]+)%/i);
if (ramMatch) stats.ram = `${Math.round(parseFloat(ramMatch[1]))}%`;
}
if (stats.temp === 'N/A' || !stats.temp || stats.temp === '-') {
const tempMatch = out.match(/Temperature\s*:\s*([\d\.]+)\s*C/i);
if (tempMatch) stats.temp = `${parseFloat(tempMatch[1]).toFixed(0)}°C`;
}
if (stats.uptime === 'N/A' || !stats.uptime || stats.uptime === '-') {
const uptimeMatch = out.match(/OS up time\s*:\s*([^\r\n]+)/i);
if (uptimeMatch) stats.uptime = uptimeMatch[1].trim();
}
} catch (e) {
logger.debug(`[oltService] fetchHiosoSystemMetricsViaTelnet: ${e.message}`);
}
};
/**
* Ambil system metrics (temp, cpu, ram, uplink) untuk brand tertentu.
* Mengisi field stats secara langsung.
*/
const fetchSystemMetrics = async (session, brandKey, stats, oltOrId) => {
const oids = SYSTEM_OIDS[brandKey] || SYSTEM_OIDS.hioso;
const oltId = (oltOrId && typeof oltOrId === 'object') ? oltOrId.id : oltOrId;
try {
const [temp, cpu, ram, rx, tx] = await snmpGet(session, [
oids.temp, oids.cpu, oids.ram, oids.uplink_rx, oids.uplink_tx
]);
const tempN = bufferToInt(temp);
const cpuN = bufferToInt(cpu);
const ramN = bufferToInt(ram);
if (Number.isFinite(tempN) && tempN >= -20 && tempN <= 120 && tempN !== 0) stats.temp = `${tempN}°C`;
if (cpuN != null) stats.cpu = `${cpuN}%`;
if (ramN != null) stats.ram = `${ramN}%`;
if (stats.temp === 'N/A') {
const c = await getEntityTemperatureC(session);
if (c != null) stats.temp = `${c.toFixed(1)}°C`;
}
const ifIndex = await pickUplinkIfIndexByTraffic(session, oltId);
if (ifIndex) {
const first = await readInterfaceOctets(session, ifIndex);
await new Promise(rv => setTimeout(rv, 800));
const second = await readInterfaceOctets(session, ifIndex);
const seconds = 0.8;
const wrap64 = BigInt(1) << BigInt(64);
if (first.rx != null && second.rx != null) {
let delta = second.rx - first.rx;
if (delta < BigInt(0)) delta = delta + wrap64;
const rate = Number(delta) / seconds;
stats.uplink_rx = Number.isFinite(rate) ? Math.max(0, Math.round(rate)) : 0;
}
if (first.tx != null && second.tx != null) {
let delta = second.tx - first.tx;
if (delta < BigInt(0)) delta = delta + wrap64;
const rate = Number(delta) / seconds;
stats.uplink_tx = Number.isFinite(rate) ? Math.max(0, Math.round(rate)) : 0;
}
} else {
const rxN = bufferToInt(rx);
const txN = bufferToInt(tx);
if (rxN != null) stats.uplink_rx = rxN;
if (txN != null) stats.uplink_tx = txN;
}
if (stats.cpu === 'N/A') {
const hrCpu = await getHrCpuPercent(session);
if (hrCpu != null) stats.cpu = `${hrCpu}%`;
}
if (stats.ram === 'N/A') {
const hrRam = await getHrRamPercent(session);
if (hrRam != null) stats.ram = `${hrRam}%`;
}
// Telnet Fallback untuk Suhu/CPU/RAM pada Hioso/HSGQ jika SNMP tidak menyediakan
if (stats.cpu === 'N/A' || stats.ram === 'N/A' || stats.temp === 'N/A') {
if (brandKey === 'hioso' || brandKey === 'hsgq') {
const oltObj = (oltOrId && typeof oltOrId === 'object') ? oltOrId : getOltById(oltOrId);
if (oltObj) {
await fetchHiosoSystemMetricsViaTelnet(oltObj, stats);
}
}
}
} catch (e) {
}
};
/**
* Ambil daftar card/board OLT.
*/
const fetchCardMetrics = async (session, brandKey, stats) => {
const oids = CARD_OIDS[brandKey];
if (!oids) return;
try {
const typeMap = await slowWalk(session, oids.type);
const statusMap = await slowWalk(session, oids.status);
const portMap = await slowWalk(session, oids.ports);
const serialMap = await slowWalk(session, oids.serial);
const cpuMap = await slowWalk(session, oids.cpu);
const ramMap = await slowWalk(session, oids.ram);
const cards = [];
const indices = Object.keys(typeMap);
for (const idx of indices) {
const type = safeToString(typeMap[idx]);
if (!type || type === '0') continue;
const statusNum = bufferToInt(statusMap[idx]);
const statusText = CARD_STATUS_MAP[statusNum] || (statusNum != null ? String(statusNum) : 'UNKNOWN');
const cpuVal = bufferToInt(cpuMap[idx]);
const ramVal = bufferToInt(ramMap[idx]);
cards.push({
index: idx,
type: type,
status: statusText,
ports: bufferToInt(portMap[idx]) || 0,
serial: decodeSn(serialMap[idx]),
cpu: cpuVal != null ? `${cpuVal}%` : 'N/A',
ram: ramVal != null ? `${ramVal}%` : 'N/A',
});
}
if (cards.length > 0) {
stats.cards = cards.sort((a, b) => String(a.index).localeCompare(String(b.index), undefined, { numeric: true }));
}
} catch (e) {
logger.error(`fetchCardMetrics error: ${e.message}`);
}
};
/**
* Ambil daftar ONU yang belum diotorisasi (Unregistered).
*/
const fetchUnauthOnus = async (session, profile, stats) => {
if (!profile.unauth_sn_table) return;
try {
const snMap = await slowWalk(session, profile.unauth_sn_table);
let typeMap = {};
if (profile.unauth_type_table) {
typeMap = await slowWalk(session, profile.unauth_type_table);
}
const indices = Object.keys(snMap);
for (const idx of indices) {
const sn = decodeSn(snMap[idx]);
if (!sn || sn === 'N/A') continue;
const type = safeToString(typeMap[idx]) || 'GENERIC';
const onuId = hiosoOnuIdFromIndex(idx);
stats.unauth_onus.push({
index: idx,
id: onuId || idx,
sn: sn,
type: type
});
}
} catch (e) {
logger.error(`fetchUnauthOnus error: ${e.message}`);
}
};
const decodeRxPower = (brand, val) => {
const rx = computeRxDbm(brand, val);
if (rx == null) return 'N/A';
return rx.toFixed(2);
};
function translateOfflineReason(brand, rawValue) {
if (rawValue == null) return null;
const valNum = bufferToInt(rawValue);
if (valNum == null) {
const valStr = String(rawValue).trim().toLowerCase();
if (valStr.includes('dying_gasp') || valStr.includes('power_off') || valStr.includes('poweroff')) {
return 'Mati Listrik / Adaptor Dicabut';
}
if (valStr.includes('los') || valStr.includes('wire_down') || valStr.includes('link_down')) {
return 'Kabel Fiber Optik Putus (LOS)';
}
if (valStr.includes('mpcp') || valStr.includes('timeout') || valStr.includes('unregister')) {
return 'Koneksi Terputus / Redaman Drop';
}
if (valStr.includes('manual') || valStr.includes('deactive')) {
return 'Dimatikan dari OLT (Deactive)';
}
return valStr;
}
const bKey = String(brand || '').toLowerCase();
if (bKey === 'zte') {
switch (valNum) {
case 2:
case 3:
return 'Kabel Fiber Optik Putus (LOS)';
case 4:
return 'Loss of Frame (Gangguan Sinyal)';
case 5:
return 'Signal Failure (Gangguan Sinyal)';
case 8:
return 'Gagal Otorisasi / Registrasi';
case 9:
return 'Mati Listrik / Adaptor Dicabut';
case 10:
return 'Dimatikan dari OLT (Deactive)';
case 12:
return 'ONU Reboot';
case 13:
return 'ONU Dimatikan';
default:
return `Penyebab Lain (${valNum})`;
}
}
if (bKey === 'huawei') {
switch (valNum) {
case 1:
case 2:
return 'Kabel Fiber Optik Putus (LOS)';
case 3:
return 'Loss of Frame (Gangguan Sinyal)';
case 4:
return 'Signal Failure (Gangguan Sinyal)';
case 7:
return 'Gagal Deaktivasi';
case 8:
return 'Dimatikan dari OLT (Deactive)';
case 9:
return 'ONU Reboot / Reset';
case 10:
return 'ONU Re-registrasi';
case 13:
return 'Mati Listrik / Adaptor Dicabut';
case 31:
case 32:
case 33:
return 'Reset oleh Pelanggan (Tombol/Sistem)';
case 30:
return 'Modul Optik Dimatikan';
default:
return `Penyebab Lain (${valNum})`;
}
}
if (bKey === 'hioso' || bKey === 'hsgq') {
switch (valNum) {
case 2:
return 'Normal / Berhasil Terkoneksi';
case 3:
return 'Koneksi Terputus (MPCP Down)';
case 4:
return 'Gangguan Link (OAM Down)';
case 7:
return 'Dimatikan dari OLT (Admin Down)';
case 8:
return 'Kabel Fiber Optik Putus (Wire Down)';
case 9:
return 'Mati Listrik / Adaptor Dicabut (Power Off)';
case 255:
return 'Penyebab tidak diketahui';
default:
return `Penyebab Lain (${valNum})`;
}
}
return `Kode (${valNum})`;
}
// ─── MAIN: getOltStats ────────────────────────────────────────────────────────
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p);
executing.add(p);
const clean = () => executing.delete(p);
p.then(clean, clean);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
const statsCache = new Map();
const pendingPromises = new Map();
async function getOltStats(id, full = false) {
const cacheKey = `${id}:${full}`;
const now = Date.now();
const cached = statsCache.get(cacheKey);
const cacheDuration = full ? 120000 : 10000; // 120s cache for full table, 10s for summary
if (cached && (now - cached.timestamp < cacheDuration)) {
logger.info(`[oltService] Returning cached stats for OLT ${id} (full: ${full})`);
return cached.data;
}
if (pendingPromises.has(cacheKey)) {
logger.info(`[oltService] Coalescing concurrent stats request for OLT ${id} (full: ${full})`);
return pendingPromises.get(cacheKey);
}
const promise = (async () => {
try {
const result = await getOltStatsInternal(id, full);
if (result && !result.error) {
statsCache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
}
return result;
} finally {
pendingPromises.delete(cacheKey);
}
})();
pendingPromises.set(cacheKey, promise);
return promise;
}
async function getOltStatsInternal(id, full = false) {
const olt = getOltById(id);
if (!olt) return null;
const stats = {
id: olt.id,
name: olt.name,
host: olt.host,
brand: olt.brand,
status: 'Offline',
error: null,
uptime: 'N/A',
temp: 'N/A',
cpu: 'N/A',
ram: 'N/A',
onus_total: 0,
onus_online: 0,
onus_offline: 0,
onus_weak: 0,
onus: [],
unauth_onus: [],
cards: [],
voltage: 'N/A',
uplink_rx: 0,
uplink_tx: 0,
};
const community = olt.snmp_community || 'public';
const brandKey = (olt.brand || 'hioso').toLowerCase();
// Gabungkan semua profil untuk deteksi otomatis jika brand yang dipilih tidak cocok
const selectedProfiles = (BRAND_PROFILES[brandKey] || []).map(p => ({ ...p, __brandKey: brandKey }));
const otherProfiles = Object.keys(BRAND_PROFILES)
.filter(k => k !== brandKey)
.reduce((acc, k) => acc.concat((BRAND_PROFILES[k] || []).map(p => ({ ...p, __brandKey: k }))), []);
const allAvailableProfiles = [...selectedProfiles, ...otherProfiles];
const session = snmp.createSession(olt.host, community, {
port: olt.snmp_port || 161,
timeout: full ? 10000 : 5000,
retries: full ? 2 : 1,
version: snmp.Version2c,
});
session.on('error', (err) => {
logger.warn(`[SNMP Session Error] OLT ${olt.host}: ${err.message}`);
});
let isResolved = false;
return new Promise((resolve) => {
const safeResolve = (data) => {
if (isResolved) return;
isResolved = true;
clearTimeout(globalTimeout);
try { session.close(); } catch (e) {}
resolve(data);
};
const timeoutMs = full ? 90000 : 20000;
const globalTimeout = setTimeout(() => {
stats.error = `Koneksi Timeout (${Math.round(timeoutMs / 1000)}s) - OLT ${olt.host} tidak merespons SNMP/Telnet`;
safeResolve(stats);
}, timeoutMs);
(async () => {
try {
// 1. Cek koneksi dasar (Uptime)
const uptimeVbs = await new Promise(rv => {
session.get(['1.3.6.1.2.1.1.3.0'], (err, vbs) => {
if (err) {
stats.error = err.message;
rv([]);
} else rv(vbs);
});
});
if (!uptimeVbs[0] || uptimeVbs[0].type === snmp.ObjectType.NoSuchObject || uptimeVbs[0].type === snmp.ObjectType.EndOfMibView) {
// Telnet Fallback for HIOSO/HSGQ if SNMP is not enabled
if (brandKey === 'hioso' || brandKey === 'hsgq') {
const telnetData = await fetchHiosoOnuDetailViaTelnet(olt);
if (telnetData && telnetData.length > 0) {
stats.status = 'Online';
stats.error = null;
stats.onus_total = telnetData.length;
stats.onus_online = telnetData.filter(r => String(r.status).toLowerCase().includes('on')).length;
stats.onus_offline = stats.onus_total - stats.onus_online;
if (full) {
stats.onus = telnetData.map((r, idx) => ({
index: r.id || idx,
id: r.id || '-',
name: r.name || r.mac || `ONU-${idx+1}`,
sn: formatSnWithColons(r.mac) || '-',
status: String(r.status).toLowerCase().includes('on') ? 'Online' : 'Offline',
offline_reason: translateOfflineReason(brandKey, r.offlineReason),
tx: r.txPower || 'N/A',
rx: r.rxPower || 'N/A',
distance: r.distance || '-',
firmware: r.fwVersion || '-',
uptime: r.onlineTime || '-'
}));
}
await fetchHiosoSystemMetricsViaTelnet(olt, stats);
safeResolve(stats);
return;
}
}
stats.error = `Tidak dapat terhubung ke OLT ${olt.host}:${olt.snmp_port || 161} (${stats.error || 'Offline / Socket Closed'}). Pastikan IP OLT aktif & Community string ('${community}') benar.`;
safeResolve(stats);
return;
}
stats.uptime = decodeUptime(uptimeVbs[0].value);
stats.status = 'Online';
// 3. Deteksi profil yang cocok
let activeProfile = null;
for (const profile of allAvailableProfiles) {
const ok = await probeOid(session, profile.probe_oid);
if (ok) {
activeProfile = profile;
break;
}
}
if (!activeProfile) {
// Telnet Fallback if profile not matched
if (brandKey === 'hioso' || brandKey === 'hsgq') {
const telnetData = await fetchHiosoOnuDetailViaTelnet(olt);
if (telnetData && telnetData.length > 0) {
stats.status = 'Online';
stats.error = null;
stats.onus_total = telnetData.length;
stats.onus_online = telnetData.filter(r => String(r.status).toLowerCase().includes('on')).length;
stats.onus_offline = stats.onus_total - stats.onus_online;
if (full) {
stats.onus = telnetData.map((r, idx) => ({
index: r.id || idx,
id: r.id || '-',
name: r.name || r.mac || `ONU-${idx+1}`,
sn: formatSnWithColons(r.mac) || '-',
status: String(r.status).toLowerCase().includes('on') ? 'Online' : 'Offline',
offline_reason: translateOfflineReason(brandKey, r.offlineReason),
tx: r.txPower || 'N/A',
rx: r.rxPower || 'N/A',
distance: r.distance || '-',
firmware: r.fwVersion || '-',
uptime: r.onlineTime || '-'
}));
}
await fetchHiosoSystemMetricsViaTelnet(olt, stats);
safeResolve(stats);
return;
}
}
stats.error = `Brand/Profile OID tidak cocok dengan perangkat OLT ${olt.host}. Silakan ganti Brand di menu 'Edit OLT'.`;
safeResolve(stats);
return;
}
const detectedBrandKey = activeProfile.__brandKey || brandKey;
stats.activeProfile = activeProfile;
stats.systemOids = SYSTEM_OIDS[detectedBrandKey] || null;
stats.cardOids = CARD_OIDS[detectedBrandKey] || null;
stats.snmpConfig = {
host: olt.host,
port: olt.snmp_port || 161,
community: olt.snmp_community || 'public',
brand: olt.brand
};
const onlineVals = getOnlineValues(detectedBrandKey, activeProfile);
// Start system metrics concurrently
const systemMetricsPromise = fetchSystemMetrics(session, detectedBrandKey, stats, olt);
// 4. Mode Counter (ZTE)
if (activeProfile.is_counter) {
const [onlineMap, totalMap] = await Promise.all([
slowWalk(session, activeProfile.status_table),
slowWalk(session, activeProfile.name_table)
]);
stats.onus_online = Object.values(onlineMap).reduce((s, v) => s + (bufferToInt(v) || 0), 0);
stats.onus_total = Object.values(totalMap).reduce((s, v) => s + (bufferToInt(v) || 0), 0);
stats.onus_offline = Math.max(0, stats.onus_total - stats.onus_online);
await systemMetricsPromise;
safeResolve(stats);
return;
}
// 5. Mode Table (Hioso, VSOL, HSGQ, etc)
const [statusMap, nameMap] = await Promise.all([
slowWalk(session, activeProfile.status_table),
slowWalk(session, activeProfile.name_table)
]);
let snMap = {};
let rxMap = {};
let txMap = {};
let distMap = {};
let fwMap = {};
let upMap = {};
let reasonMap = {};
// OPTIMIZATION: Only fetch detailed metrics if full=true
// Summary mode: skip rx/tx/distance/firmware/uptime/reason walks
// This makes summary 5-10x faster
if (full) {
// For Hioso/HSGQ, rx/tx DDM walks take ~33s (OLT measures all 256 slots on demand).
// Use a dedicated slow session (30s timeout, 0 retries) and walk them SEQUENTIALLY
// AFTER the fast walks complete. This prevents packet loss from concurrent walking.
const isSlowDdmBrand = (detectedBrandKey === 'hioso' || detectedBrandKey === 'hsgq');
if (isSlowDdmBrand && (activeProfile.rx_power_table || activeProfile.tx_power_table)) {
// --- Phase 1: Fast walks (status already done, now sn + distance + card + unauth) ---
const fastTasks = [];
fastTasks.push(() => fetchCardMetrics(session, detectedBrandKey, stats));
fastTasks.push(() => fetchUnauthOnus(session, activeProfile, stats));
fastTasks.push(() => pickSnTable(session, activeProfile).then(res => snMap = res.map || {}));
if (activeProfile.distance_table) {
fastTasks.push(() => slowWalk(session, activeProfile.distance_table).then(res => distMap = res));
}
if (activeProfile.firmware_table) {
fastTasks.push(() => slowWalk(session, activeProfile.firmware_table).then(res => fwMap = res));
}
if (activeProfile.uptime_table) {
fastTasks.push(() => slowWalk(session, activeProfile.uptime_table).then(res => upMap = res));
}
if (activeProfile.offline_reason_table) {
fastTasks.push(() => slowWalk(session, activeProfile.offline_reason_table).then(res => reasonMap = res));
}
await limitConcurrency(fastTasks, 2);
// --- Phase 2: Slow DDM walks on dedicated session (sequential, high timeout) ---
const slowSession = snmp.createSession(olt.host, community, {
port: olt.snmp_port || 161,
timeout: 30000, // 30s per GETBULK batch — DDM on Hioso needs long timeouts
retries: 0, // No retries; a retry on a 30s timeout would kill the budget
version: snmp.Version2c,
});
slowSession.on('error', (err) => {
logger.warn(`[SNMP SlowSession Error] OLT ${olt.host}: ${err.message}`);
});
try {
if (activeProfile.rx_power_table) {
logger.info(`[oltService] Walking rx_power_table for OLT ${olt.host} (slow DDM session)`);
rxMap = await slowWalk(slowSession, activeProfile.rx_power_table);
logger.info(`[oltService] rx_power_table done, ${Object.keys(rxMap).length} entries`);
}
if (activeProfile.tx_power_table) {
logger.info(`[oltService] Walking tx_power_table for OLT ${olt.host} (slow DDM session)`);
txMap = await slowWalk(slowSession, activeProfile.tx_power_table);
logger.info(`[oltService] tx_power_table done, ${Object.keys(txMap).length} entries`);
}
// Gap-fill: Hioso OLT truncates walk responses but individual GET works for all slots.
// Find indices present in statusMap but missing from rxMap/txMap, then GET them directly.
const statusIndices = Object.keys(statusMap);
const missingRx = activeProfile.rx_power_table
? statusIndices.filter(idx => rxMap[idx] == null)
: [];
const missingTx = activeProfile.tx_power_table
? statusIndices.filter(idx => txMap[idx] == null)
: [];
if (missingRx.length > 0 || missingTx.length > 0) {
logger.info(`[oltService] Gap-filling DDM: ${missingRx.length} rx + ${missingTx.length} tx missing, fetching via GET`);
// Build OID list for batch GET (up to 50 per request)
const batchGet = async (oidBase, indices, targetMap) => {
const BATCH = 50;
for (let i = 0; i < indices.length; i += BATCH) {
const batch = indices.slice(i, i + BATCH);
const oids = batch.map(idx => `${oidBase}.${idx}`);
await new Promise(rv => {
slowSession.get(oids, (err, vbs) => {
if (err) { rv(); return; }
for (const vb of vbs) {
if (!vb || snmp.isVarbindError(vb)) continue;
const suffix = vb.oid.split(oidBase + '.')[1];
if (!suffix) continue;
const val = Buffer.isBuffer(vb.value)
? vb.value.toString('utf8').replace(/\0/g,'').trim()
: String(vb.value).trim();
if (val && val !== 'null' && val !== '0') {
targetMap[suffix] = vb.value; // store raw for decoding
}
}
rv();
});
});
}
};
await batchGet(activeProfile.rx_power_table, missingRx, rxMap);
await batchGet(activeProfile.tx_power_table, missingTx, txMap);
logger.info(`[oltService] Gap-fill done, rx now ${Object.keys(rxMap).length}, tx now ${Object.keys(txMap).length}`);
}
} catch (ddmErr) {
logger.warn(`[oltService] DDM walk failed for OLT ${olt.host}: ${ddmErr.message}`);
} finally {
try { slowSession.close(); } catch (e) {}
}
} else {
// --- Standard path for all other brands ---
const tasks = [];
tasks.push(() => fetchCardMetrics(session, detectedBrandKey, stats));
tasks.push(() => fetchUnauthOnus(session, activeProfile, stats));
tasks.push(() => pickSnTable(session, activeProfile).then(res => snMap = res.map || {}));
if (activeProfile.rx_power_table) {
tasks.push(() => slowWalk(session, activeProfile.rx_power_table).then(res => rxMap = res));
}
if (activeProfile.tx_power_table) {
tasks.push(() => slowWalk(session, activeProfile.tx_power_table).then(res => txMap = res));
}
if (activeProfile.distance_table) {
tasks.push(() => slowWalk(session, activeProfile.distance_table).then(res => distMap = res));
}
if (activeProfile.firmware_table) {
tasks.push(() => slowWalk(session, activeProfile.firmware_table).then(res => fwMap = res));
}
if (activeProfile.uptime_table) {
tasks.push(() => slowWalk(session, activeProfile.uptime_table).then(res => upMap = res));
}
if (activeProfile.offline_reason_table) {
tasks.push(() => slowWalk(session, activeProfile.offline_reason_table).then(res => reasonMap = res));
}
// OPTIMIZATION: Tune concurrency per brand
// ZTE handles 5, others 3-4
const concurrencyMap = {
'zte': 5,
'huawei': 4,
'hioso': 2,
'vsol': 4,
'cdata': 3,
'hsgq': 2
};
const concurrency = concurrencyMap[detectedBrandKey] || 4;
await limitConcurrency(tasks, concurrency);
}
}
await systemMetricsPromise;
const allIndices = new Set([...Object.keys(statusMap), ...Object.keys(nameMap)]);
stats.onus_total = allIndices.size;
const onus = [];
let weakCount = 0;
for (const idx of allIndices) {
const stRaw = getByIdx(statusMap, idx);
const stInt = bufferToInt(stRaw);
const stStr = stRaw == null ? '' : String(stRaw).trim().toLowerCase();
const isUp = stInt != null
? onlineVals.includes(stInt)
: (stStr === 'online' || stStr === 'up' || stStr === 'on' || stStr === 'operation');
const nameRaw = getByIdx(nameMap, idx);
// SNMP returns ONU name as Buffer (OctetString) — must decode as UTF-8
const nameStr = nameRaw == null
? ''
: (Buffer.isBuffer(nameRaw)
? nameRaw.toString('utf8')
: String(nameRaw)
).replace(/\0/g, '').trim();
const name = nameStr || ('ONU-' + idx);
const snVal = getByIdx(snMap, idx);
const rxVal = getByIdx(rxMap, idx);
const txVal = getByIdx(txMap, idx);
const distVal = getByIdx(distMap, idx);
const fwVal = getByIdx(fwMap, idx);
const upVal = getByIdx(upMap, idx);
const sn = snVal ? decodeSn(snVal) : '-';
const rx = decodeRxPower(detectedBrandKey, rxVal);
const tx = decodeRxPower(detectedBrandKey, txVal);
const distInt = bufferToInt(distVal);
let distance = '-';
if (activeProfile.distance_table && distInt != null && distInt > 0) {
const tenths = activeProfile.distance_tenths_meter !== false;
distance = tenths ? (distInt / 10).toFixed(1) + ' m' : `${distInt} m`;
}
const firmware = safeToString(fwVal) || '-';
const onuUptime = upVal ? decodeUptime(bufferToInt(upVal)) : '-';
const onuId = hiosoOnuIdFromIndex(idx);
if (isUp) stats.onus_online++;
else stats.onus_offline++;
if (rx !== 'N/A') {
const n = parseFloat(rx);
if (Number.isFinite(n) && n < -27) weakCount++;
}
if (full) {
const rxShown = rx === 'N/A' ? rx : rx + ' dBm';
const txShown = tx === 'N/A' ? tx : tx + ' dBm';
const rawReason = getByIdx(reasonMap, idx);
const offlineReason = translateOfflineReason(detectedBrandKey, rawReason);
onus.push({
index: idx,
id: onuId || '-',
name,
sn,
status: isUp ? 'Online' : 'Offline',
offline_reason: isUp ? null : (offlineReason || null),
tx: txShown,
rx: rxShown,
distance,
firmware,
uptime: onuUptime
});
}
}
stats.onus_weak = weakCount;
stats.onus = enrichOnusWithCustomerData(onus.sort((a, b) => a.name.localeCompare(b.name)));
safeResolve(stats);
} catch (err) {
stats.error = err.message;
safeResolve(stats);
}
})();
});
}
function enrichOnusWithCustomerData(onus) {
if (!Array.isArray(onus) || onus.length === 0) return onus;
try {
const custs = db.prepare(`
SELECT id, name, pppoe_username, mac_address, genieacs_tag, phone
FROM customers
`).all() || [];
const pppoeMap = new Map();
const macMap = new Map();
const tagMap = new Map();
const nameMap = new Map();
for (const c of custs) {
if (c.pppoe_username) pppoeMap.set(String(c.pppoe_username).trim().toLowerCase(), c);
if (c.mac_address) {
const rawMac = String(c.mac_address).replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
if (rawMac) macMap.set(rawMac, c);
}
if (c.genieacs_tag) tagMap.set(String(c.genieacs_tag).trim().toLowerCase(), c);
if (c.name) nameMap.set(String(c.name).trim().toLowerCase(), c);
}
for (const onu of onus) {
const snClean = String(onu.sn || '').replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
const nameClean = String(onu.name || '').trim().toLowerCase();
let matched = null;
if (snClean && macMap.has(snClean)) matched = macMap.get(snClean);
else if (snClean && tagMap.has(snClean)) matched = tagMap.get(snClean);
else if (nameClean && pppoeMap.has(nameClean)) matched = pppoeMap.get(nameClean);
else if (nameClean && nameMap.has(nameClean)) matched = nameMap.get(nameClean);
else if (snClean.length >= 6) {
for (const [mac, c] of macMap.entries()) {
if (mac.length >= 8 && (snClean.includes(mac) || mac.includes(snClean))) {
matched = c;
break;
}
}
}
if (matched) {
onu.customer_id = matched.id;
onu.customer_name = matched.name;
onu.customer_phone = matched.phone || null;
}
}
} catch (e) {
logger.warn(`[OLT] Error enriching ONUs with customer data: ${e.message}`);
}
return onus;
}
// ─── ONU ACTIONS ─────────────────────────────────────────────────────────────
function decodeOltSnmpIndex(brand, index) {
const idxNum = parseInt(index, 10);
if (!Number.isFinite(idxNum)) {
const s = String(index || '');
let frame = '0', board = '1', port = '1', onuId = '1';
const parts = s.split(/[/: ]+/).filter(Boolean);
if (parts.length >= 3) {
board = parts[parts.length - 3];
port = parts[parts.length - 2];
onuId = parts[parts.length - 1];
}
return { frame, board, port, onuId };
}
const bKey = String(brand || '').toLowerCase();
if (bKey === 'zte') {
const board = (idxNum >> 16) & 0xFF;
const port = (idxNum >> 8) & 0xFF;
const onuId = idxNum & 0xFF;
return { frame: '0', board: String(board), port: String(port), onuId: String(onuId) };
} else if (bKey === 'huawei') {
const frame = (idxNum >> 21) & 0x1F;
const slot = (idxNum >> 16) & 0x1F;
const port = (idxNum >> 8) & 0xFF;
const onuId = idxNum & 0xFF;
return { frame: String(frame), board: String(slot), port: String(port), onuId: String(onuId) };
} else {
let port = (idxNum >> 16) & 0xFF;
if (port === 0 || port > 16) port = (idxNum >> 8) & 0xFF;
const onuId = idxNum & 0xFF;
return { frame: '0', board: '0', port: String(port), onuId: String(onuId) };
}
}
async function rebootOnu(oltId, index) {
const olt = getOltById(oltId);
if (!olt) throw new Error('OLT tidak ditemukan');
const brand = (olt.brand || 'hioso').toLowerCase();
if (brand === 'zte' || brand === 'huawei') {
const onuProvisionSvc = require('./onuProvisionService');
const oltConfig = {
vendor: olt.brand,
host: olt.host,
username: olt.web_user,
password: olt.web_password
};
const portVal = parseInt(olt.telnet_port, 10);
oltConfig.port = (Number.isFinite(portVal) && portVal > 0) ? portVal : 22;
const parsed = decodeOltSnmpIndex(olt.brand, index);
return await onuProvisionSvc.rebootONU(oltConfig, olt.brand, parsed);
}
const community = olt.snmp_community || 'public';
const session = snmp.createSession(olt.host, community, { port: olt.snmp_port || 161, version: snmp.Version2c });
const oid = `1.3.6.1.4.1.25355.3.2.6.3.2.1.40.${index}`;
return new Promise((resolve, reject) => {
session.set([{ oid, type: snmp.ASN1.Integer, value: 1 }], (error) => {
session.close();
if (error) reject(error);
else resolve(true);
});
});
}
async function renameOnu(oltId, index, newName) {
const olt = getOltById(oltId);
if (!olt) throw new Error('OLT tidak ditemukan');
const brand = (olt.brand || 'hioso').toLowerCase();
if (brand === 'zte' || brand === 'huawei') {
const onuProvisionSvc = require('./onuProvisionService');
const oltConfig = {
vendor: olt.brand,
host: olt.host,
username: olt.web_user,
password: olt.web_password
};
const portVal = parseInt(olt.telnet_port, 10);
oltConfig.port = (Number.isFinite(portVal) && portVal > 0) ? portVal : 22;
const parsed = decodeOltSnmpIndex(olt.brand, index);
parsed.newName = newName;
return await onuProvisionSvc.renameONU(oltConfig, olt.brand, parsed);
}
const community = olt.snmp_community || 'public';
const session = snmp.createSession(olt.host, community, { port: olt.snmp_port || 161, version: snmp.Version2c });
const oid = `1.3.6.1.4.1.25355.3.2.6.3.2.1.37.${index}`;
return new Promise((resolve, reject) => {
session.set([{ oid, type: snmp.ASN1.OctetString, value: newName }], (error) => {
session.close();
if (error) reject(error);
else resolve(true);
});
});
}
/**
* Otorisasi ONU (Provisioning) via Telnet CLI.
*/
async function authorizeOnu(oltId, data) {
const olt = getOltById(oltId);
if (!olt) throw new Error('OLT tidak ditemukan');
const brand = (olt.brand || 'hioso').toLowerCase();
const { index, sn, name, vlan } = data;
const snCli = normalizeSnForOltProvision(sn);
// Parsing index (format ZTE: 1/board/port:onuId)
let board = 1, port = 1, onuId = 1;
if (index.includes('/')) {
const parts = index.split(/[/: ]+/).filter(Boolean);
if (parts.length >= 3) {
board = parts[parts.length - 3];
port = parts[parts.length - 2];
onuId = parts[parts.length - 1];
}
}
const cmds = [];
if (brand === 'zte') {
cmds.push('enable');
cmds.push('configure terminal');
cmds.push(`interface gpon-olt_1/${board}/${port}`);
cmds.push(`onu ${onuId} type ALL sn ${snCli}`);
cmds.push('exit');
cmds.push(`interface gpon-onu_1/${board}/${port}:${onuId}`);
if (name) cmds.push(`name ${name}`);
cmds.push('tcont 1 profile UP-100M'); // Default profile
cmds.push('gemport 1 tcont 1');
if (vlan) cmds.push(`service-port 1 vport 1 user-vlan ${vlan} vlan ${vlan}`);
cmds.push('exit');
cmds.push('end');
cmds.push('write');
} else if (brand === 'huawei') {
cmds.push('enable');
cmds.push('config');
cmds.push(`interface gpon 0/${board}`);
cmds.push(`ont add ${port} ${onuId} sn-auth ${snCli} omci ont-lineprofile-id 1 ont-srvprofile-id 1`);
if (name) cmds.push(`ont name ${port} ${onuId} "${name}"`);
cmds.push('quit');
if (vlan) cmds.push(`service-port vlan ${vlan} gpon 0/${board}/${port} ont ${onuId} gemport 1 multi-service user-vlan ${vlan}`);
cmds.push('save');
} else {
throw new Error(`Fitur otorisasi otomatis belum didukung untuk brand ${brand}`);
}
return await telnetLoginAndRun(olt.host, olt.web_user, olt.web_password, cmds, telnetOptsFromOlt(olt));
}
/**
* Cari data tambahan ONU dari ACS (WiFi, Client Count, dll)
*/
async function enrichOnusWithAcsData(onus) {
try {
logger.info(`[ACS-Sync] Memulai sinkronisasi data untuk ${onus.length} ONU`);
const acsDevices = await genieacs.getDevices();
if (!acsDevices || !acsDevices.length) {
logger.warn('[ACS-Sync] Tidak ada perangkat ditemukan di GenieACS');
return onus;
}
const normalizeSN = (sn) => String(sn || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
for (const onu of onus) {
const targetSn = normalizeSN(onu.sn);
if (!targetSn || targetSn === '-') continue;
logger.debug(`[ACS-Sync] Mencari SN: ${targetSn}`);
// Cari device di ACS berdasarkan SN (fuzzy match)
const acsDev = acsDevices.find(d => {
const sn1 = d.Device?.DeviceInfo?.SerialNumber?._value;
const sn2 = d.InternetGatewayDevice?.DeviceInfo?.SerialNumber?._value;
const sn3 = d._id;
const acsSn = normalizeSN(sn1 || sn2 || sn3);
const match = acsSn.includes(targetSn) || targetSn.includes(acsSn);
if (match) logger.info(`[ACS-Sync] Match Found: ${targetSn} matches ${acsSn} (ID: ${d._id})`);
return match;
});
if (acsDev) {
// Ambil SSID (Cek beberapa path umum)
const ssid =
acsDev.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['1']?.SSID?._value ||
acsDev.Device?.WiFi?.SSID?.['1']?.SSID?._value ||
acsDev.Device?.WiFi?.SSID?.['1']?.SSID ||
acsDev.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['5']?.SSID?._value || '-';
onu.wifi_ssid = ssid;
// Hitung Client Connected (Sum up all radios if possible)
let associations = 0;
const paths = [
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.TotalAssociations',
'Device.WiFi.AccessPoint.1.AssociatedDeviceNumberOfEntries',
'Device.WiFi.AccessPoint.2.AssociatedDeviceNumberOfEntries',
'InternetGatewayDevice.LANDevice.1.Hosts.HostNumberOfEntries',
'Device.Hosts.HostNumberOfEntries'
];
for (const p of paths) {
const val = getNestedValue(acsDev, p);
if (val !== undefined && val !== null) {
associations += parseInt(val) || 0;
}
}
onu.client_count = associations;
onu.acs_id = acsDev._id;
logger.info(`[ACS-Sync] Updated ${onu.sn}: SSID=${onu.wifi_ssid}, Users=${onu.client_count}`);
}
}
} catch (e) {
logger.error(`[ACS-Sync] Error: ${e.message}`);
}
return onus;
}
function getNestedValue(obj, path) {
return path.split('.').reduce((prev, curr) => {
if (prev && prev[curr] !== undefined) {
const val = prev[curr];
return (val && val._value !== undefined) ? val._value : val;
}
return undefined;
}, obj);
}
function parseZteOnuIndex(index) {
let board = 1;
let port = 1;
let onuId = 1;
const s = String(index || '');
if (s.includes('/')) {
const parts = s.split(/[/: ]+/).filter(Boolean);
if (parts.length >= 3) {
board = parts[parts.length - 3];
port = parts[parts.length - 2];
onuId = parts[parts.length - 1];
}
}
return { board: String(board), port: String(port), onuId: String(onuId) };
}
const telnetOptsFromOlt = (olt) => ({
port: Number(olt.telnet_port) > 0 ? Number(olt.telnet_port) : 23,
enablePassword: olt.enable_password != null && String(olt.enable_password).length > 0 ? String(olt.enable_password) : null
});
/**
* Delegasi VLAN / service-port ke [go-api-c320](https://github.com/s4lfanet/go-api-c320) — POST /api/v1/vlan/onu
* Format pon_port: rack/shelf/slot (contoh 1/2/7) dari indeks gpon-onu_1/2/7:5
*/
async function configureZteWanViaGoApi(oltId, data) {
const olt = getOltById(oltId);
if (!olt) throw new Error('OLT tidak ditemukan');
const base = String(olt.api_base_url || '').trim().replace(/\/+$/, '');
if (!base) {
throw new Error('Isi "API Base URL" pada data OLT (contoh http://10.0.0.5:8081) agar metode Go API dapat memanggil go-api-c320.');
}
const mode = String(data.mode || 'Bridge');
if (mode === 'PPPoE') {
throw new Error('Integrasi REST ini memetakan ke VLAN/service-port go-api-c320. Untuk PPPoE gunakan Telnet CLI (OMCI) atau TR069.');
}
const { board, port, onuId } = parseZteOnuIndex(data.index);
const ponPort = `1/${board}/${port}`;
if (!/^\d+\/\d+\/\d+$/.test(ponPort)) {
throw new Error('Indeks ONU tidak valid untuk Go API (butuh slot/port/onu, mis. gpon-onu_1/2/7:5 → pon_port 1/2/7).');
}
const onu_id = parseInt(String(onuId), 10);
if (!Number.isFinite(onu_id) || onu_id < 1 || onu_id > 128) {
throw new Error('ONU ID tidak valid (1–128).');
}
const svlan = parseInt(String(data.vlan != null ? data.vlan : data.svlan), 10);
const cvlanIn = data.cvlan != null && String(data.cvlan).trim() !== '' ? parseInt(String(data.cvlan), 10) : NaN;
const cvlan = Number.isFinite(cvlanIn) ? cvlanIn : svlan;
if (!Number.isFinite(svlan) || svlan < 1 || svlan > 4094) {
throw new Error('VLAN / SVLAN harus antara 1 dan 4094.');
}
const vlan_mode = String(data.vlan_mode || 'tag').toLowerCase();
const allowed = ['tag', 'translation', 'transparent'];
if (!allowed.includes(vlan_mode)) {
throw new Error('vlan_mode harus: tag, translation, atau transparent (sesuai go-api-c320).');
}
if (vlan_mode === 'translation' && (!Number.isFinite(cvlan) || cvlan < 1)) {
throw new Error('Mode translation membutuhkan CVLAN.');
}
const priority = Number.isFinite(parseInt(data.priority, 10)) ? parseInt(data.priority, 10) : 0;
if (priority < 0 || priority > 7) {
throw new Error('Priority harus 0–7.');
}
const url = `${base}/api/v1/vlan/onu`;
const body = {
pon_port: ponPort,
onu_id,
svlan,
cvlan: vlan_mode === 'translation' ? cvlan : (Number.isFinite(cvlan) ? cvlan : svlan),
vlan_mode,
priority
};
const res = await axios.post(url, body, {
timeout: 90000,
headers: { 'Content-Type': 'application/json' },
validateStatus: () => true
});
const payload = res.data;
const httpOk = res.status >= 200 && res.status < 300;
const apiCode = payload != null && payload.code != null ? Number(payload.code) : null;
if (!httpOk || (apiCode != null && apiCode >= 400)) {
const msg = payload?.message || payload?.status || payload?.error || `HTTP ${res.status}`;
throw new Error(`go-api-c320: ${msg}`);
}
return typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
}
/**
* Konfigurasi WAN ONU via TR069 (GenieACS).
*/
async function configureWanViaAcs(sn, data) {
const acsDevices = await genieacs.getDevices();
const acsDev = acsDevices.find(d => d._id.includes(sn) || (d.Device?.DeviceInfo?.SerialNumber?._value && d.Device.DeviceInfo.SerialNumber._value.includes(sn)));
if (!acsDev) throw new Error(`Perangkat dengan SN ${sn} tidak ditemukan di ACS.`);
const { mode, vlan, username, password, lans, ssids } = data;
const params = {};
// Helper to build binding strings
// Typical formats: "LAN1,LAN2" or "WLAN1,WLAN2"
const lanBind = lans ? lans.split(',').map(l => `LAN${l}`).join(',') : '';
const ssidBind = ssids ? ssids.split(',').map(s => `WLAN${s}`).join(',') : '';
if (mode === 'PPPoE') {
const basePath = "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.";
params[`${basePath}Enable`] = true;
params[`${basePath}ConnectionType`] = "IP_Routed";
params[`${basePath}Name`] = "INTERNET";
params[`${basePath}Username`] = username;
params[`${basePath}Password`] = password;
if (vlan) {
params[`${basePath}VLANID`] = vlan;
}
// LAN & SSID Binding for Broadcom/Typical ONUs
if (lanBind) params[`${basePath}X_BROADCOM_COM_LANBind`] = lanBind;
if (ssidBind) params[`${basePath}X_BROADCOM_COM_WLANBind`] = ssidBind;
} else {
const basePath = "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.";
params[`${basePath}Enable`] = true;
params[`${basePath}ConnectionType`] = "IP_Bridged";
if (vlan) {
params[`${basePath}VLANID`] = vlan;
}
// LAN & SSID Binding
if (lanBind) params[`${basePath}X_BROADCOM_COM_LANBind`] = lanBind;
if (ssidBind) params[`${basePath}X_BROADCOM_COM_WLANBind`] = ssidBind;
}
return await genieacs.setParameterValues(acsDev._id, params);
}
/**
* Konfigurasi WAN ONU (PPPoE / Bridge) via Telnet CLI.
*/
async function configureOnuWan(oltId, data) {
const olt = getOltById(oltId);
if (!olt) throw new Error('OLT tidak ditemukan');
const brand = (olt.brand || 'hioso').toLowerCase();
const method = String(data.method || 'telnet').toLowerCase();
if (method === 'go-api' && brand !== 'zte') {
throw new Error('Metode REST (go-api-c320) hanya untuk OLT brand ZTE.');
}
if (brand === 'zte' && method === 'go-api') {
return configureZteWanViaGoApi(oltId, data);
}
const { index, mode, vlan, username, password, lans, ssids } = data;
// Parsing index
let board = 1, port = 1, onuId = 1;
if (index.includes('/')) {
const parts = index.split(/[/: ]+/).filter(Boolean);
if (parts.length >= 3) {
board = parts[parts.length - 3];
port = parts[parts.length - 2];
onuId = parts[parts.length - 1];
}
}
const cmds = [];
if (brand === 'zte') {
cmds.push('enable');
cmds.push('configure terminal');
if (mode === 'PPPoE') {
// ZTE C300/C320 PPPoE Config via OMCI (pon-onu-mng)
cmds.push(`pon-onu-mng gpon-onu_1/${board}/${port}:${onuId}`);
// 1. Configure WAN IP with PPPoE
// Note: vlan here is used as vlan-profile name. Usually named 'VLAN100' or just '100'
cmds.push(`wan-ip 1 mode pppoe username ${username} password ${password} vlan-profile ${vlan} host 1`);
// 2. Binding LAN & SSID
if (lans || ssids) {
let bindCmd = `wan 1`;
if (lans) bindCmd += ` ethuni ${lans}`;
if (ssids) bindCmd += ` ssid ${ssids}`;
bindCmd += ` service internet host 1`;
cmds.push(bindCmd);
}
cmds.push('exit');
// OLT side service-port
cmds.push(`interface gpon-onu_1/${board}/${port}:${onuId}`);
cmds.push(`service-port 1 vport 1 user-vlan ${vlan} vlan ${vlan}`);
} else {
// Bridge Mode
cmds.push(`pon-onu-mng gpon-onu_1/${board}/${port}:${onuId}`);
// Tagging each selected port to the target VLAN
if (vlan) {
if (lans) {
lans.split(',').forEach(l => {
cmds.push(`vlan port eth_0/${l} mode tag vlan ${vlan}`);
});
}
if (ssids) {
ssids.split(',').forEach(s => {
cmds.push(`vlan port wifi_0/${s} mode tag vlan ${vlan}`);
});
}
}
cmds.push('exit');
// OLT side service-port
cmds.push(`interface gpon-onu_1/${board}/${port}:${onuId}`);
cmds.push(`service-port 1 vport 1 user-vlan ${vlan} vlan ${vlan}`);
}
cmds.push('exit');
cmds.push('end');
cmds.push('write');
return await telnetLoginAndRun(olt.host, olt.web_user, olt.web_password, cmds, telnetOptsFromOlt(olt));
} else if (brand === 'huawei') {
cmds.push('enable');
cmds.push('config');
if (mode === 'PPPoE') {
// Huawei usually configures WAN via OMCI/TR069, but basic service-port is needed
cmds.push(`service-port vlan ${vlan} gpon 0/${board}/${port} ont ${onuId} gemport 1 multi-service user-vlan ${vlan}`);
} else {
cmds.push(`service-port vlan ${vlan} gpon 0/${board}/${port} ont ${onuId} gemport 1 multi-service user-vlan ${vlan}`);
}
cmds.push('save');
} else {
throw new Error(`Fitur konfigurasi WAN belum didukung untuk brand ${brand}`);
}
return await telnetLoginAndRun(olt.host, olt.web_user, olt.web_password, cmds, telnetOptsFromOlt(olt));
}
async function getAllOltsStats(full = false) {
const olts = getActiveOlts();
if (!olts || olts.length === 0) {
return {
is_all: true,
onus_total: 0,
onus_online: 0,
onus_offline: 0,
onus_weak: 0,
onus: [],
unauth_onus: [],
olts_summary: [],
error: 'Tidak ada OLT aktif'
};
}
const results = await Promise.all(olts.map(o => getOltStats(o.id, full)));
let onus_total = 0;
let onus_online = 0;
let onus_offline = 0;
let onus_weak = 0;
let uplink_rx = 0;
let uplink_tx = 0;
const allOnus = [];
const allUnauth = [];
const olts_summary = [];
const tempValues = [];
const cpuValues = [];
const ramValues = [];
for (let i = 0; i < results.length; i++) {
const s = results[i];
const oltInfo = olts[i];
if (s) {
onus_total += (s.onus_total || 0);
onus_online += (s.onus_online || 0);
onus_offline += (s.onus_offline || 0);
onus_weak += (s.onus_weak || 0);
uplink_rx += (s.uplink_rx || 0);
uplink_tx += (s.uplink_tx || 0);
const tN = parseFloat(s.temp);
if (Number.isFinite(tN) && tN > 0) tempValues.push(tN);
const cN = parseFloat(s.cpu);
if (Number.isFinite(cN) && cN >= 0) cpuValues.push(cN);
const rN = parseFloat(s.ram);
if (Number.isFinite(rN) && rN > 0) ramValues.push(rN);
olts_summary.push({
id: oltInfo.id,
name: oltInfo.name,
host: oltInfo.host,
brand: oltInfo.brand,
status: s.status,
onus_total: s.onus_total,
onus_online: s.onus_online,
onus_offline: s.onus_offline,
onus_weak: s.onus_weak,
temp: s.temp,
cpu: s.cpu,
ram: s.ram,
uptime: s.uptime,
uplink_rx: s.uplink_rx || 0,
uplink_tx: s.uplink_tx || 0,
error: s.error,
activeProfile: s.activeProfile || null,
systemOids: s.systemOids || null,
snmpConfig: s.snmpConfig || null
});
if (Array.isArray(s.onus)) {
s.onus.forEach(onu => {
allOnus.push({ ...onu, olt_id: oltInfo.id, olt_name: oltInfo.name });
});
}
if (Array.isArray(s.unauth_onus)) {
s.unauth_onus.forEach(u => {
allUnauth.push({ ...u, olt_id: oltInfo.id, olt_name: oltInfo.name });
});
}
}
}
let temp = 'N/A';
let cpu = 'N/A';
let ram = 'N/A';
let uptime = 'Multi-OLT Active';
if (olts_summary.length === 1) {
const s0 = olts_summary[0];
temp = s0.temp || 'N/A';
cpu = s0.cpu || 'N/A';
ram = s0.ram || 'N/A';
uptime = s0.uptime || 'N/A';
} else if (olts_summary.length > 1) {
if (tempValues.length > 0) {
const avgT = Math.round(tempValues.reduce((a, b) => a + b, 0) / tempValues.length);
temp = `${avgT}°C (rata-rata)`;
}
if (cpuValues.length > 0) {
const avgC = Math.round(cpuValues.reduce((a, b) => a + b, 0) / cpuValues.length);
cpu = `${avgC}% (rata-rata)`;
}
if (ramValues.length > 0) {
const avgR = Math.round(ramValues.reduce((a, b) => a + b, 0) / ramValues.length);
ram = `${avgR}% (rata-rata)`;
}
const onlineCount = olts_summary.filter(o => o.status === 'Online').length;
uptime = `${onlineCount}/${olts_summary.length} OLT Online`;
}
return {
is_all: true,
onus_total,
onus_online,
onus_offline,
onus_weak,
temp,
cpu,
ram,
uptime,
uplink_rx,
uplink_tx,
onus: allOnus,
unauth_onus: allUnauth,
olts_summary
};
}
async function testOltConnection(data) {
const host = String(data.host || '').trim();
const snmpPort = parseInt(data.snmp_port, 10) || 161;
const snmpCommunity = String(data.snmp_community || 'public').trim();
const brand = String(data.brand || 'generic').toLowerCase();
const telnetPort = parseInt(data.telnet_port, 10) || 23;
const webUser = String(data.web_user || 'admin').trim();
const webPassword = String(data.web_password || 'admin').trim();
const enablePassword = data.enable_password ? String(data.enable_password).trim() : null;
if (!host) {
return { success: false, message: 'Host/IP OLT harus diisi.' };
}
const result = {
success: false,
host,
snmp: { ok: false, error: null, uptime: null, sysDescr: null, profile: null, onus: 0 },
telnet: { ok: false, error: null },
messages: []
};
// 1. Test SNMP v2c
try {
const session = snmp.createSession(host, snmpCommunity, {
port: snmpPort,
timeout: 3000,
retries: 1,
version: snmp.Version2c
});
const vbs = await new Promise((resolve) => {
session.get(['1.3.6.1.2.1.1.3.0'], (err, res) => {
if (err) resolve(null);
else resolve(res);
});
});
if (vbs && vbs[0] && !snmp.isVarbindError(vbs[0])) {
result.snmp.ok = true;
result.snmp.uptime = decodeUptime(vbs[0].value);
// Probe profile
const selectedProfiles = (BRAND_PROFILES[brand] || []).map(p => ({ ...p, __brandKey: brand }));
const otherProfiles = Object.keys(BRAND_PROFILES)
.filter(k => k !== brand)
.reduce((acc, k) => acc.concat((BRAND_PROFILES[k] || []).map(p => ({ ...p, __brandKey: k }))), []);
const allProfiles = [...selectedProfiles, ...otherProfiles];
for (const p of allProfiles) {
const ok = await probeOid(session, p.probe_oid);
if (ok) {
result.snmp.profile = p.name;
try {
const statusMap = await slowWalk(session, p.status_table, 500);
result.snmp.onus = Object.keys(statusMap).length;
} catch (e) {}
break;
}
}
result.messages.push(`SNMP OK (Uptime: ${result.snmp.uptime}, Profil: ${result.snmp.profile || 'Standar'}, ${result.snmp.onus} ONU)`);
} else {
result.snmp.error = 'Tidak ada respon SNMP. Pastikan Service SNMP di OLT sudah Aktif dan IP/Community benar.';
}
try { session.close(); } catch (e) {}
} catch (e) {
result.snmp.error = e.message;
}
// 2. Test Telnet
try {
await telnetLoginAndRun(host, webUser, webPassword, ['show version'], {
port: telnetPort,
enablePassword
});
result.telnet.ok = true;
result.messages.push('Telnet CLI OK (Login Berhasil)');
} catch (e) {
result.telnet.error = e.message;
}
result.success = result.snmp.ok || result.telnet.ok;
result.message = result.messages.join(' | ') || (result.snmp.error || result.telnet.error || 'Gagal terhubung ke OLT');
return result;
}
module.exports = {
getAllOlts, getActiveOlts, getOltById, createOlt, updateOlt, deleteOlt, getOltStats, getAllOltsStats, testOltConnection, rebootOnu, renameOnu, authorizeOnu,
configureOnuWan, configureZteWanViaGoApi, configureWanViaAcs,
// Diekspor untuk kebutuhan tooling (scripts/olt-probe.js) — sumber profil yang sama
BRAND_PROFILES, SYSTEM_OIDS, telnetLoginAndRun, telnetOptsFromOlt
};