Files
open-isp/services/onuSyncService.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

227 lines
8.0 KiB
JavaScript

/**
* Service: Sinkronisasi ONU (OLT) dengan Data Pelanggan.
*
* Dua tugas dalam satu siklus:
* 1. Auto-isi/perbarui `mac_address` pelanggan dari caller-id sesi PPPoE aktif
* HANYA bila caller-id terverifikasi sebagai serial ONU yang benar-benar ada
* di OLT (agar MAC router/Cpe bridge tidak tersimpan salah). Nilai lama akan
* diperbarui sehingga penggantian ONU ikut terdeteksi otomatis.
* 2. Ganti nama ONU di OLT menjadi `pppoe_username` / nama pelanggan berdasarkan
* kecocokan: mac_address pelanggan, enrichment stats, atau caller-id PPPoE live.
*
* Satu kali pengambilan daftar ONU per OLT (SNMP walk) dipakai untuk kedua tugas.
*/
const { logger } = require('../config/logger');
const db = require('../config/database');
const oltService = require('./oltService');
const mikrotikService = require('./mikrotikService');
function normalizeKey(s) {
return String(s || '').replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
}
/**
* Normalisasi MAC menjadi format AA:BB:CC:DD:EE:FF. Kembalikan null bila bukan 12 hex.
*/
function formatMac(raw) {
const hex = String(raw || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (hex.length !== 12) return null;
return hex.match(/.{2}/g).join(':');
}
function loadCustomerContext() {
const custs = db.prepare('SELECT id, name, pppoe_username, mac_address FROM customers').all() || [];
const byId = new Map();
const byMac = new Map();
const byUser = new Map();
for (const c of custs) {
byId.set(c.id, c);
if (c.mac_address) byMac.set(normalizeKey(c.mac_address), c);
if (c.pppoe_username) byUser.set(String(c.pppoe_username).trim().toLowerCase(), c);
}
return { custs, byId, byMac, byUser };
}
/**
* Bangun peta caller-id (MAC) -> username PPPoE dari sesi aktif.
*/
function buildMacToUser(sessions) {
const map = new Map();
for (const [username, s] of sessions.entries()) {
const k = normalizeKey(s.callerId);
if (k && !map.has(k)) map.set(k, username);
}
return map;
}
/**
* Isi/perbarui mac_address pelanggan dari caller-id PPPoE, hanya untuk MAC ONU
* yang terverifikasi ada di daftar serial OLT. Tampilkan/lewati konflik.
*/
function refreshCustomerMacs(sessions, byUser, byMac, onuSerialSet, dryRun) {
const stmt = db.prepare('UPDATE customers SET mac_address = ? WHERE id = ?');
const summary = { total: sessions.size, filled: 0, refreshed: 0, skippedNonOnu: 0, noCustomer: 0, conflict: 0, errors: [] };
for (const [username, s] of sessions.entries()) {
const cust = byUser.get(username);
if (!cust) {
summary.noCustomer++;
continue;
}
const mac = formatMac(s.callerId);
if (!mac) {
summary.skippedNonOnu++;
continue;
}
const norm = normalizeKey(mac);
if (!onuSerialSet.has(norm)) {
summary.skippedNonOnu++;
continue;
}
const owner = byMac.get(norm);
if (owner && owner.id !== cust.id) {
summary.conflict++;
logger.warn(`[SyncMAC] MAC ONU ${mac} sudah terpakai pelanggan lain (${owner.name}); lewati ${cust.name}`);
continue;
}
const current = String(cust.mac_address || '').trim();
if (current && normalizeKey(current) === norm) continue;
try {
if (!dryRun) stmt.run(mac, cust.id);
if (current) summary.refreshed++;
else summary.filled++;
if (current) byMac.delete(normalizeKey(current));
byMac.set(norm, cust);
logger.info(`[SyncMAC] ${dryRun ? '[DRY-RUN] ' : ''}${current ? 'update' : 'isi'} mac_address ${cust.name} => ${mac}`);
} catch (err) {
summary.errors.push({ customer: cust.name, error: err.message });
logger.error(`[SyncMAC] Gagal ${current ? 'update' : 'isi'} mac_address ${cust.name}: ${err.message}`);
}
}
return summary;
}
/**
* Tentukan target nama untuk satu ONU.
* @returns {{customer: object|null, target: string|null, via: string|null}}
*/
function resolveOnuTarget(onu, byId, byMac, byUser, macToUser) {
const snNorm = normalizeKey(onu.sn);
let customer = onu.customer_id ? byId.get(onu.customer_id) : null;
let via = 'mac_map';
if (!customer) {
if (snNorm && byMac.has(snNorm)) {
customer = byMac.get(snNorm);
via = 'mac_map';
} else if (snNorm && macToUser.has(snNorm)) {
const uname = macToUser.get(snNorm);
customer = byUser.get(uname) || null;
via = customer ? 'pppoe_live' : null;
}
}
if (!customer) return { customer: null, target: null, via: null };
const target = (customer.pppoe_username && String(customer.pppoe_username).trim())
? String(customer.pppoe_username).trim()
: String(customer.name || '').trim();
return { customer, target, via };
}
async function syncOnuNamesFromStats(olt, onus, ctx, macToUser, dryRun) {
const summary = { oltId: olt.id, oltName: olt.name, total: onus.length, renamed: 0, failed: 0, same: 0, unmatch: 0, errors: [] };
for (const onu of onus) {
const { customer, target, via } = resolveOnuTarget(onu, ctx.byId, ctx.byMac, ctx.byUser, macToUser);
if (!customer || !target) {
summary.unmatch++;
continue;
}
const writable = target.slice(0, 30);
if (String(onu.name || '').trim() === writable) {
summary.same++;
continue;
}
try {
if (!dryRun) await oltService.renameOnu(olt.id, onu.index, target);
summary.renamed++;
logger.info(`[SyncONU] ${dryRun ? '[DRY-RUN] akan rename' : 'rename'} ${onu.id} (${onu.sn}) => ${target} (${via})`);
} catch (err) {
summary.failed++;
summary.errors.push({ onu: onu.id, sn: onu.sn, target, error: err.message });
logger.error(`[SyncONU] Gagal rename ${onu.id} => ${target}: ${err.message}`);
}
}
return summary;
}
/**
* Orkestrator sinkronisasi: mengambil daftar ONU sekali per OLT, lalu
* (1) auto-isi/perbarui mac_address dan (2) rename nama ONU.
* @param {{dryRun?: boolean, oltId?: number|string, skipFill?: boolean, skipNames?: boolean}} opts
*/
async function runSyncOnu(opts = {}) {
const dryRun = !!opts.dryRun;
const onlyOltId = opts.oltId != null ? String(opts.oltId) : null;
let olts = (oltService.getAllOlts() || []).filter((o) => o.is_active === 1 || o.is_active === true);
if (onlyOltId) olts = olts.filter((o) => String(o.id) === onlyOltId);
// 1) Ambil daftar ONU (satu walk per OLT) + kumpulkan serial ONU
const onusByOlt = [];
const onuSerialSet = new Map();
for (const olt of olts) {
try {
const stats = await oltService.getOltStats(olt.id, true);
const onus = (stats && stats.onus) || [];
onusByOlt.push({ olt, onus });
for (const onu of onus) {
const k = normalizeKey(onu.sn);
if (k && !onuSerialSet.has(k)) onuSerialSet.set(k, { oltId: olt.id, oltName: olt.name, onuId: onu.id });
}
} catch (err) {
logger.error(`[SyncONU] Gagal ambil daftar ONU ${olt.name}: ${err.message}`);
onusByOlt.push({ olt, onus: [], error: err.message });
}
}
// 2) Sesi PPPoE aktif
let sessions = new Map();
try {
sessions = await mikrotikService.getActivePppoeSessionsMap();
} catch (e) {
logger.error(`[SyncMAC] Gagal ambil sesi PPPoE: ${e.message}`);
}
// 3) Auto-isi/perbarui mac_address (hanya MAC ONU terverifikasi)
let mac;
if (opts.skipFill) {
mac = { skipped: true, total: sessions.size };
} else {
const ctx0 = loadCustomerContext();
mac = refreshCustomerMacs(sessions, ctx0.byUser, ctx0.byMac, onuSerialSet, dryRun);
}
// 4) Sinkronkan nama ONU (pakai data pelanggan terbaru)
let summaries = [];
if (!opts.skipNames) {
const macToUser = buildMacToUser(sessions);
const ctx1 = loadCustomerContext();
for (const { olt, onus } of onusByOlt) {
summaries.push(await syncOnuNamesFromStats(olt, onus, ctx1, macToUser, dryRun));
}
}
return { mac, olts: summaries };
}
/**
* Sinkronkan nama ONU untuk satu OLT (kompatibilitas).
*/
async function syncOnuCustomerNames(oltId, opts = {}) {
const res = await runSyncOnu({ dryRun: !!opts.dryRun, oltId, skipFill: true });
const s = (res.olts && res.olts[0]) || null;
if (!s) throw new Error('OLT tidak ditemukan');
return s;
}
module.exports = { runSyncOnu, syncOnuCustomerNames, formatMac };