Files
open-isp/config/genieacs.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

1196 lines
47 KiB
JavaScript

const axios = require('axios');
require('dotenv').config();
const { logger } = require('./logger');
const db = require('./database');
const { getSetting } = require('./settingsManager');
// ─── Built-in ACS Helpers ────────────────────────────────────────────────────
/** Check if built-in ACS mode is active */
function isBuiltinAcsEnabled() {
return getSetting('use_builtin_acs', false) === true || getSetting('use_builtin_acs', false) === 'true';
}
/**
* Inflate flat params object into nested GenieACS-style structure.
* e.g. { "InternetGatewayDevice.DeviceInfo.SerialNumber": "ABC" }
* becomes { InternetGatewayDevice: { DeviceInfo: { SerialNumber: { _value: "ABC" } } } }
*/
function inflateParams(flatObj) {
const result = {};
for (const [key, value] of Object.entries(flatObj)) {
const parts = key.split('.');
let current = result;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (i === parts.length - 1) {
current[part] = { _value: value, _type: typeof value === 'number' ? 'xsd:unsignedInt' : 'xsd:string', _timestamp: new Date().toISOString() };
} else {
if (!current[part] || typeof current[part] !== 'object' || current[part]._value !== undefined) {
current[part] = {};
}
current = current[part];
}
}
}
return result;
}
/** Convert a builtin acs_devices row into GenieACS-compatible device object */
function builtinRowToDevice(row) {
if (!row) return null;
let params = {};
try { params = JSON.parse(row.params || '{}'); } catch (e) { /* ignore */ }
let tags = [];
try { tags = JSON.parse(row.tags || '[]'); } catch (e) { /* ignore */ }
tags = tags.filter(t => t !== 'bootstrapped');
const device = inflateParams(params);
device._id = row.id;
device._tags = tags;
device._lastInform = row.last_inform;
device._updatedAt = row.updated_at;
device._registered = row.created_at;
device._acs_server_id = 'builtin';
device._acs_server_name = 'Built-in ACS';
// Set GenieACS special _deviceId root property
device._deviceId = {
_SerialNumber: row.serial_number || '',
_Manufacturer: row.manufacturer || '',
_ProductClass: row.product_class || '',
_OUI: row.oui || ''
};
// Set DeviceID structure as strings (GenieACS standard)
device.DeviceID = {
SerialNumber: row.serial_number || '',
Manufacturer: row.manufacturer || '',
ProductClass: row.product_class || '',
OUI: row.oui || ''
};
return device;
}
/** Parse GenieACS-style query filter for SQLite */
function matchesQuery(device, query) {
if (!query || Object.keys(query).length === 0) return true;
for (const [key, condition] of Object.entries(query)) {
if (key === '$or') {
if (!Array.isArray(condition)) return false;
const anyMatch = condition.some(sub => matchesQuery(device, sub));
if (!anyMatch) return false;
continue;
}
if (key === '$and') {
if (!Array.isArray(condition)) return false;
const allMatch = condition.every(sub => matchesQuery(device, sub));
if (!allMatch) return false;
continue;
}
// Get value from nested device object
const parts = key.split('.');
let val = device;
for (const p of parts) {
if (val && typeof val === 'object') {
val = val[p];
} else {
val = undefined;
break;
}
}
if (val && val._value !== undefined) val = val._value;
// Handle condition types
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
if ('$exists' in condition) {
const exists = val !== undefined && val !== null;
if (condition.$exists !== exists) return false;
}
if ('$ne' in condition) {
if (Array.isArray(condition.$ne) && condition.$ne.length === 0) {
if (!Array.isArray(val) || val.length === 0) return false;
} else if (val === condition.$ne) return false;
}
if ('$not' in condition) {
if (condition.$not && typeof condition.$not === 'object' && '$size' in condition.$not) {
if (Array.isArray(val) && val.length === condition.$not.$size) return false;
}
}
} else {
// Direct equality
if (key === '_tags') {
if (!Array.isArray(device._tags) || !device._tags.includes(condition)) return false;
} else if (key === '_id') {
if (device._id !== condition) return false;
} else {
if (String(val) !== String(condition)) return false;
}
}
}
return true;
}
/**
* Create a proxy object that mimics axios interface but operates on local SQLite.
* This is the core of the adapter pattern - all downstream code using
* createAxiosInstance() will transparently use local DB when builtin ACS is active.
*/
function createBuiltinAxiosProxy() {
return {
get: async (url, config = {}) => {
const params = config.params || {};
// GET /devices
if (url === '/devices' || url === '/devices/') {
let rows = [];
try {
const limit = params.limit || 999999;
rows = db.prepare('SELECT * FROM acs_devices ORDER BY last_inform DESC LIMIT ?').all(limit);
} catch (e) {
logger.error(`[BuiltinACS] Error querying devices: ${e.message}`);
}
let devices = rows.map(builtinRowToDevice);
// Apply query filter
if (params.query) {
try {
const queryObj = typeof params.query === 'string' ? JSON.parse(params.query) : params.query;
devices = devices.filter(d => matchesQuery(d, queryObj));
} catch (e) {
logger.debug(`[BuiltinACS] Query parse error: ${e.message}`);
}
}
return { data: devices, status: 200 };
}
// GET /devices/:id
const deviceMatch = url.match(/^\/devices\/(.+)$/);
if (deviceMatch) {
const deviceId = decodeURIComponent(deviceMatch[1]);
const row = db.prepare('SELECT * FROM acs_devices WHERE id = ?').get(deviceId);
if (!row) {
const err = new Error(`Device ${deviceId} not found`);
err.response = { status: 404, data: 'Not found' };
throw err;
}
return { data: builtinRowToDevice(row), status: 200 };
}
return { data: [], status: 200 };
},
post: async (url, data = {}, config = {}) => {
// POST /devices/:id/tasks
const taskMatch = url.match(/^\/devices\/(.+)\/tasks$/);
if (taskMatch) {
const deviceId = decodeURIComponent(taskMatch[1]);
const taskName = data.name || 'unknown';
const payload = JSON.stringify(data);
const now = new Date().toISOString();
try {
// Task 'inform' tidak perlu disimpan sebagai task; cukup memicu Connection Request.
if (taskName === 'inform') {
try {
const acsService = require('../services/acsServerService');
acsService.triggerConnectionRequest(deviceId).catch(() => {});
} catch (e) { /* service not loaded yet */ }
return { data: { _id: 'inform', name: 'inform', device: deviceId }, status: 202 };
}
const result = db.prepare(
'INSERT INTO acs_tasks (device_id, name, payload, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
).run(deviceId, taskName, payload, 'pending', now, now);
logger.info(`[BuiltinACS] Task '${taskName}' created for device ${deviceId}, id=${result.lastInsertRowid}`);
// Trigger connection request asynchronously
try {
const acsService = require('../services/acsServerService');
acsService.triggerConnectionRequest(deviceId).catch(() => {});
} catch (e) { /* service not loaded yet */ }
return { data: { _id: String(result.lastInsertRowid), name: taskName, device: deviceId }, status: 202 };
} catch (e) {
logger.error(`[BuiltinACS] Error creating task: ${e.message}`);
return { data: {}, status: 500 };
}
}
return { data: {}, status: 200 };
},
put: async (url, data = {}, config = {}) => {
// PUT /devices/:id — update tags
const deviceMatch = url.match(/^\/devices\/(.+)$/);
if (deviceMatch) {
const deviceId = decodeURIComponent(deviceMatch[1]);
const now = new Date().toISOString();
if (data._tags) {
try {
db.prepare('UPDATE acs_devices SET tags = ?, updated_at = ? WHERE id = ?')
.run(JSON.stringify(data._tags), now, deviceId);
return { data: {}, status: 200 };
} catch (e) {
logger.error(`[BuiltinACS] Error updating tags: ${e.message}`);
}
}
}
return { data: {}, status: 200 };
},
delete: async (url, config = {}) => {
const deviceMatch = url.match(/^\/devices\/(.+)$/);
if (deviceMatch) {
const deviceId = decodeURIComponent(deviceMatch[1]);
try {
db.prepare('DELETE FROM acs_devices WHERE id = ?').run(deviceId);
db.prepare('DELETE FROM acs_tasks WHERE device_id = ?').run(deviceId);
return { data: {}, status: 200 };
} catch (e) {
logger.error(`[BuiltinACS] Error deleting device: ${e.message}`);
}
}
return { data: {}, status: 200 };
}
};
}
// Import WhatsApp notification function
let sendMonitoringAlert = null;
(async () => {
try {
const whatsappBot = await import('../services/whatsappBot.mjs');
sendMonitoringAlert = whatsappBot.sendMonitoringAlert;
logger.info('[GenieACS] WhatsApp monitoring alert integration loaded');
} catch (error) {
logger.warn('[GenieACS] WhatsApp bot not available for monitoring alerts');
}
})();
// Konfigurasi GenieACS API (Legacy - untuk backward compatibility)
const GENIEACS_URL = process.env.GENIEACS_URL || 'http://localhost:7557';
const GENIEACS_USERNAME = process.env.GENIEACS_USERNAME;
const GENIEACS_PASSWORD = process.env.GENIEACS_PASSWORD;
// Helper: Get all ACS servers from database
function getAllACSServers() {
try {
if (isBuiltinAcsEnabled()) {
return [{
id: 'builtin',
name: 'Built-in ACS',
url: 'local',
status: 'active'
}];
}
const legacyUrl = getSetting('genieacs_url', GENIEACS_URL);
const legacyUser = getSetting('genieacs_username', GENIEACS_USERNAME);
const legacyPass = getSetting('genieacs_password', GENIEACS_PASSWORD);
const servers = [];
// Add legacy server if configured
if (legacyUrl) {
servers.push({
id: 'legacy',
name: 'Default ACS',
url: legacyUrl,
username: legacyUser,
password: legacyPass,
status: 'active'
});
}
// Add servers from database
const dbServers = db.prepare('SELECT * FROM genieacs_servers WHERE status = ?').all('active');
servers.push(...dbServers);
return servers;
} catch (error) {
logger.error(`[GenieACS] Error getting ACS servers: ${error.message}`);
return [];
}
}
// Helper: Get specific ACS server by ID
function getACSServer(serverId) {
if (!serverId) {
if (isBuiltinAcsEnabled()) {
return {
id: 'builtin',
name: 'Built-in ACS',
url: 'local',
status: 'active'
};
}
const legacyUrl = getSetting('genieacs_url', GENIEACS_URL);
const legacyUser = getSetting('genieacs_username', GENIEACS_USERNAME);
const legacyPass = getSetting('genieacs_password', GENIEACS_PASSWORD);
if (legacyUrl) {
return {
id: 'legacy',
name: 'Default ACS',
url: legacyUrl,
username: legacyUser,
password: legacyPass,
status: 'active'
};
}
return null;
}
if (serverId === 'builtin') {
return {
id: 'builtin',
name: 'Built-in ACS',
url: 'local',
status: 'active'
};
}
if (serverId === 'legacy') {
const legacyUrl = getSetting('genieacs_url', GENIEACS_URL);
const legacyUser = getSetting('genieacs_username', GENIEACS_USERNAME);
const legacyPass = getSetting('genieacs_password', GENIEACS_PASSWORD);
if (legacyUrl) {
return {
id: 'legacy',
name: 'Default ACS',
url: legacyUrl,
username: legacyUser,
password: legacyPass,
status: 'active'
};
}
return null;
}
try {
const server = db.prepare('SELECT * FROM genieacs_servers WHERE id = ?').get(serverId);
return server || null;
} catch (error) {
logger.error(`[GenieACS] Error getting ACS server ${serverId}: ${error.message}`);
return null;
}
}
// Helper: Create axios instance for specific server
function createAxiosInstance(server) {
// Built-in ACS: return proxy object that mimics axios but uses SQLite
if (server && (server.id === 'builtin' || server.url === 'local')) {
return createBuiltinAxiosProxy();
}
const config = {
baseURL: server.url.endsWith('/') ? server.url.slice(0, -1) : server.url,
timeout: 30000,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
};
if (server.username && server.password) {
config.auth = {
username: server.username,
password: server.password
};
}
return axios.create(config);
}
// Legacy axios instance (untuk backward compatibility)
const axiosInstance = axios.create({
baseURL: GENIEACS_URL,
timeout: 30000,
auth: {
username: GENIEACS_USERNAME,
password: GENIEACS_PASSWORD
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
// GenieACS API wrapper
const genieacsApi = {
// Get devices from all servers or specific server
async getDevices(serverId = null) {
try {
logger.debug('[GenieACS] Getting all devices...');
const servers = serverId ? [getACSServer(serverId)].filter(Boolean) : getAllACSServers();
const allDevices = [];
for (const server of servers) {
try {
const instance = createAxiosInstance(server);
let response;
try {
response = await instance.get('/devices', {
timeout: 45000
});
} catch (e) {
response = await instance.get('/api/devices', {
timeout: 45000
});
}
const devices = response.data || [];
// Add server info to each device
devices.forEach(device => {
device._acs_server_id = server.id;
device._acs_server_name = server.name;
});
allDevices.push(...devices);
logger.debug(`[GenieACS] Found ${devices.length} devices from ${server.name}`);
} catch (error) {
logger.error(`[GenieACS] Error getting devices from ${server.name}: ${error.message}`);
}
}
logger.debug(`[GenieACS] Total devices found: ${allDevices.length}`);
return allDevices;
} catch (error) {
logger.error(`[GenieACS] Error getting devices: ${error.message}`);
throw error;
}
},
// Find device by phone number across all servers (returns full device data)
async findDeviceByPhoneNumber(phoneNumber, serverId = null) {
try {
logger.debug(`[GenieACS] Finding device with phone number: ${phoneNumber}`);
const servers = serverId ? [getACSServer(serverId)].filter(Boolean) : getAllACSServers();
for (const server of servers) {
try {
const instance = createAxiosInstance(server);
// Don't use projection - get full device data
let response;
try {
response = await instance.get('/devices', {
params: {
'query': JSON.stringify({
'_tags': phoneNumber
})
},
timeout: 15000
});
} catch (e) {
response = await instance.get('/api/devices', {
params: {
'query': JSON.stringify({
'_tags': phoneNumber
})
},
timeout: 15000
});
}
if (response.data && response.data.length > 0) {
const device = response.data[0];
device._acs_server_id = server.id;
device._acs_server_name = server.name;
logger.debug(`[GenieACS] Device found on ${server.name} with full data`);
return device;
}
} catch (error) {
logger.debug(`[GenieACS] Device not found on ${server.name}: ${error.message}`);
}
}
throw new Error(`No device found with phone number: ${phoneNumber}`);
} catch (error) {
logger.error(`[GenieACS] Error finding device with phone number ${phoneNumber}: ${error.message}`);
throw error;
}
},
async getDeviceByPhoneNumber(phoneNumber, serverId = null) {
try {
// findDeviceByPhoneNumber already returns full device data
return await this.findDeviceByPhoneNumber(phoneNumber, serverId);
} catch (error) {
logger.error(`[GenieACS] Error getting device by phone number ${phoneNumber}: ${error.message}`);
throw error;
}
},
async getDevice(deviceId, serverId = null) {
try {
// If serverId provided, use that server
if (serverId) {
const server = getACSServer(serverId);
if (!server) {
throw new Error(`ACS Server ${serverId} not found`);
}
const instance = createAxiosInstance(server);
const response = await instance.get(`/devices/${encodeURIComponent(deviceId)}`);
const device = response.data;
device._acs_server_id = server.id;
device._acs_server_name = server.name;
return device;
}
// Otherwise, search all servers
const servers = getAllACSServers();
for (const server of servers) {
try {
const instance = createAxiosInstance(server);
const response = await instance.get(`/devices/${encodeURIComponent(deviceId)}`);
const device = response.data;
device._acs_server_id = server.id;
device._acs_server_name = server.name;
return device;
} catch (error) {
// Continue to next server
}
}
throw new Error(`Device ${deviceId} not found on any server`);
} catch (error) {
logger.error(`[GenieACS] Error getting device ${deviceId}: ${error.message}`);
throw error;
}
},
async setParameterValues(deviceId, parameters, serverId = null) {
try {
logger.debug(`[GenieACS] Setting parameters for device: ${deviceId}`);
// Format parameter values untuk GenieACS
const parameterValues = [];
for (const [path, value] of Object.entries(parameters)) {
// Handle SSID update
if (path.includes('SSID')) {
parameterValues.push(
["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", value],
["Device.WiFi.SSID.1.SSID", value]
);
}
// Handle WiFi password update
else if (path.includes('Password') || path.includes('KeyPassphrase')) {
parameterValues.push(
["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase", value],
["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase", value],
["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey", value]
);
}
// Handle other parameters
else {
parameterValues.push([path, value]);
}
}
logger.debug(`[GenieACS] Formatted parameter values count: ${parameterValues.length}`);
// Get server instance
const server = serverId ? getACSServer(serverId) : (isBuiltinAcsEnabled() ? getACSServer('builtin') : null);
const instance = server ? createAxiosInstance(server) : axiosInstance;
// Kirim task ke GenieACS
const task = {
name: "setParameterValues",
parameterValues: parameterValues
};
const response = await instance.post(
`/devices/${encodeURIComponent(deviceId)}/tasks`,
task
);
logger.debug('[GenieACS] Parameter update task queued');
// Kirim refresh task
const refreshTask = {
name: "refreshObject",
objectName: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1"
};
const refreshResponse = await instance.post(
`/devices/${encodeURIComponent(deviceId)}/tasks`,
refreshTask
);
logger.debug('[GenieACS] Refresh task queued');
return response.data;
} catch (error) {
logger.error(`[GenieACS] Error setting parameters for device ${deviceId}: ${error.response?.data ? JSON.stringify(error.response.data) : error.message}`);
throw error;
}
},
async reboot(deviceId, serverId = null) {
try {
const server = serverId ? getACSServer(serverId) : (isBuiltinAcsEnabled() ? getACSServer('builtin') : null);
const instance = server ? createAxiosInstance(server) : axiosInstance;
const task = {
name: "reboot",
timestamp: new Date().toISOString()
};
const response = await instance.post(
`/devices/${encodeURIComponent(deviceId)}/tasks`,
task
);
return response.data;
} catch (error) {
logger.error(`[GenieACS] Error rebooting device ${deviceId}: ${error.response?.data ? JSON.stringify(error.response.data) : error.message}`);
throw error;
}
},
async factoryReset(deviceId, serverId = null) {
try {
const server = serverId ? getACSServer(serverId) : (isBuiltinAcsEnabled() ? getACSServer('builtin') : null);
const instance = server ? createAxiosInstance(server) : axiosInstance;
const task = {
name: "factoryReset",
timestamp: new Date().toISOString()
};
const response = await instance.post(
`/devices/${encodeURIComponent(deviceId)}/tasks`,
task
);
return response.data;
} catch (error) {
logger.error(`[GenieACS] Error factory resetting device ${deviceId}: ${error.response?.data ? JSON.stringify(error.response.data) : error.message}`);
throw error;
}
},
async getDeviceParameters(deviceId, parameterNames) {
try {
if (isBuiltinAcsEnabled()) {
const instance = createBuiltinAxiosProxy();
const response = await instance.get(`/devices/${encodeURIComponent(deviceId)}`);
return response.data;
}
const queryString = parameterNames.map(name => `query=${encodeURIComponent(name)}`).join('&');
const response = await axiosInstance.get(`/devices/${encodeURIComponent(deviceId)}?${queryString}`);
return response.data;
} catch (error) {
logger.error(`[GenieACS] Error getting parameters for device ${deviceId}: ${error.response?.data ? JSON.stringify(error.response.data) : error.message}`);
throw error;
}
},
async getDeviceInfo(deviceId) {
try {
logger.debug(`[GenieACS] Getting device info for device ID: ${deviceId}`);
// Built-in ACS mode
if (isBuiltinAcsEnabled()) {
const instance = createBuiltinAxiosProxy();
const response = await instance.get(`/devices/${encodeURIComponent(deviceId)}`);
return response.data;
}
// Mendapatkan device detail
const deviceResponse = await axios.get(`${GENIEACS_URL}/devices/${encodeURIComponent(deviceId)}`, {
auth: {
username: GENIEACS_USERNAME,
password: GENIEACS_PASSWORD
}
});
if (!deviceResponse.data) {
logger.warn('[GenieACS] No device data found');
return null;
}
logger.debug('[GenieACS] Device data retrieved successfully');
return deviceResponse.data;
} catch (error) {
logger.error(`[GenieACS] Error getting device info: ${error.response?.data ? JSON.stringify(error.response.data) : error.message}`);
return null;
}
},
async getVirtualParameters(deviceId) {
try {
logger.debug(`[GenieACS] Getting virtual parameters for device ID: ${deviceId}`);
// Built-in ACS mode: return device data directly (params already stored)
if (isBuiltinAcsEnabled()) {
const instance = createBuiltinAxiosProxy();
try {
const response = await instance.get(`/devices/${encodeURIComponent(deviceId)}`);
return response.data;
} catch (e) {
return null;
}
}
const virtualParams = [
// Serial Number
'InternetGatewayDevice.DeviceInfo.SerialNumber',
'Device.DeviceInfo.SerialNumber',
'VirtualParameters.getSerialNumber',
// Device Uptime
'InternetGatewayDevice.DeviceInfo.UpTime',
'Device.DeviceInfo.UpTime',
'VirtualParameters.getdeviceuptime',
// PPPoE Uptime
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.UpTime',
'Device.PPP.Interface.1.UpTime',
'VirtualParameters.getpppuptime',
// Active Devices
'InternetGatewayDevice.LANDevice.1.Hosts.HostNumberOfEntries',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations',
'Device.Hosts.HostNumberOfEntries',
'VirtualParameters.activedevices',
'VirtualParameters.getactivedevices',
// RX Power
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.XPON.RxPower',
'Device.XPON.Interface.1.RxPower',
'VirtualParameters.RXPower',
'VirtualParameters.redaman',
// PON MAC
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.MACAddress',
'Device.Ethernet.Interface.1.MACAddress',
'VirtualParameters.PonMac',
// WAN IP
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress',
'Device.IP.Interface.1.IPv4Address.1.IPAddress',
'VirtualParameters.WanIP',
// PPP IP
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress',
'Device.PPP.Interface.1.IPCP.LocalIPAddress',
'VirtualParameters.pppIP',
'VirtualParameters.pppoeIP',
// Temperature
'InternetGatewayDevice.DeviceInfo.Temperature',
'Device.DeviceInfo.Temperature',
'VirtualParameters.gettemp'
];
// Menggunakan tasks endpoint untuk mendapatkan parameter values
const response = await axios.post(`${GENIEACS_URL}/tasks`, [{
name: "getParameterValues",
parameterNames: virtualParams,
device: deviceId
}], {
auth: {
username: GENIEACS_USERNAME,
password: GENIEACS_PASSWORD
}
});
logger.debug('[GenieACS] Virtual parameters retrieved successfully');
return response.data;
} catch (error) {
logger.error(`[GenieACS] Error getting virtual parameters: ${error.response?.data ? JSON.stringify(error.response.data) : error.message}`);
return null;
}
},
};
// Fungsi untuk memeriksa nilai RXPower dari semua perangkat
async function monitorRXPower(threshold = -27) {
try {
logger.info(`[RXPower] Memulai pemantauan RXPower dengan threshold ${threshold} dBm`);
// Ambil semua perangkat
const devices = await genieacsApi.getDevices();
logger.info(`[RXPower] Memeriksa RXPower untuk ${devices.length} perangkat...`);
// Ambil data PPPoE dari Mikrotik
logger.debug('[RXPower] Mengambil data PPPoE dari Mikrotik...');
// const conn = await getMikrotikConnection(); // Removed Mikrotik connection
let pppoeSecrets = [];
// if (conn) { // Removed Mikrotik connection
// try {
// // Dapatkan semua PPPoE secret dari Mikrotik
// pppoeSecrets = await conn.write('/ppp/secret/print');
// console.log(`Ditemukan ${pppoeSecrets.length} PPPoE secret`);
// } catch (error) {
// console.error('Error mendapatkan PPPoE secret:', error.message);
// }
// }
const criticalDevices = [];
// Periksa setiap perangkat
for (const device of devices) {
try {
// Dapatkan nilai RXPower
const rxPowerPaths = [
'VirtualParameters.RXPower',
'VirtualParameters.redaman',
'InternetGatewayDevice.WANDevice.1.WANPONInterfaceConfig.RXPower',
'Device.XPON.Interface.1.Stats.RXPower'
];
let rxPower = null;
// Periksa setiap jalur yang mungkin berisi nilai RXPower
for (const path of rxPowerPaths) {
// Ekstrak nilai menggunakan path yang ada di device
if (getRXPowerValue(device, path)) {
rxPower = getRXPowerValue(device, path);
break;
}
}
// Jika rxPower ditemukan dan di bawah threshold
if (rxPower !== null && parseFloat(rxPower) < threshold) {
// Cari PPPoE username dari parameter perangkat (seperti di handleAdminCheckONU)
let pppoeUsername = "Unknown";
const serialNumber = getDeviceSerialNumber(device);
const deviceId = device._id;
const shortDeviceId = deviceId.split('-')[2] || deviceId;
// Ambil PPPoE username dari parameter perangkat
pppoeUsername =
device.InternetGatewayDevice?.WANDevice?.['1']?.WANConnectionDevice?.['1']?.WANPPPConnection?.['1']?.Username?._value ||
device.InternetGatewayDevice?.WANDevice?.['0']?.WANConnectionDevice?.['0']?.WANPPPConnection?.['0']?.Username?._value ||
device.VirtualParameters?.pppoeUsername?._value ||
"Unknown";
// Jika tidak ditemukan dari parameter perangkat, coba cari dari PPPoE secret di Mikrotik
if (pppoeUsername === "Unknown") {
// Coba cari PPPoE secret yang terkait dengan perangkat ini berdasarkan comment
const matchingSecret = pppoeSecrets.find(secret => {
if (!secret.comment) return false;
// Cek apakah serial number atau device ID ada di kolom comment
return (
secret.comment.includes(serialNumber) ||
secret.comment.includes(shortDeviceId)
);
});
if (matchingSecret) {
// Jika ditemukan secret yang cocok, gunakan nama secret sebagai username
pppoeUsername = matchingSecret.name;
logger.debug(`[RXPower] Menemukan PPPoE username ${pppoeUsername} untuk perangkat ${shortDeviceId} dari PPPoE secret`);
}
} else {
logger.debug(`[RXPower] Menemukan PPPoE username ${pppoeUsername} untuk perangkat ${shortDeviceId} dari parameter perangkat`);
}
// Jika masih tidak ditemukan, coba cari dari tag perangkat
if (pppoeUsername === "Unknown" && device._tags && Array.isArray(device._tags)) {
// Cek apakah ada tag yang dimulai dengan "pppoe:" yang berisi username
const pppoeTag = device._tags.find(tag => tag.startsWith('pppoe:'));
if (pppoeTag) {
pppoeUsername = pppoeTag.replace('pppoe:', '');
logger.debug(`[RXPower] Menemukan PPPoE username ${pppoeUsername} untuk perangkat ${shortDeviceId} dari tag`);
} else {
logger.debug(`[RXPower] Tidak menemukan PPPoE username untuk perangkat ${shortDeviceId}, tags: ${JSON.stringify(device._tags)}`);
}
}
const deviceInfo = {
id: device._id,
rxPower,
serialNumber: getDeviceSerialNumber(device),
lastInform: device._lastInform,
pppoeUsername: pppoeUsername
};
criticalDevices.push(deviceInfo);
logger.info(`[RXPower] Perangkat redaman tinggi: ${deviceInfo.id}, RXPower: ${rxPower} dBm, PPPoE: ${pppoeUsername}`);
}
} catch (deviceError) {
logger.error(`[RXPower] Error memeriksa RXPower untuk perangkat ${device._id}: ${deviceError.message || String(deviceError)}`);
}
}
// Jika ada perangkat dengan RXPower di bawah threshold
if (criticalDevices.length > 0) {
// Buat pesan peringatan
let message = `*PERINGATAN: REDAMAN TINGGI*\n\n`;
message += `${criticalDevices.length} perangkat memiliki nilai RX Power lebih buruk dari ${threshold} dBm (semakin negatif = semakin buruk):\n\n`;
criticalDevices.forEach((device, index) => {
message += `${index + 1}. *Device ID:* ${device.id.split('-')[2] || device.id}\n`;
message += ` 📱 *S/N:* ${device.serialNumber}\n`;
message += ` 👤 *PPPoE:* ${device.pppoeUsername}\n`;
message += ` 📡 *RX Power:* ${device.rxPower} dBm\n`;
message += ` 🕒 *Last Inform:* ${new Date(device.lastInform).toLocaleString('id-ID')}\n\n`;
});
message += `⚠️ *Mohon segera dicek untuk menghindari koneksi terputus.*`;
// Kirim notifikasi WhatsApp jika tersedia
if (sendMonitoringAlert) {
try {
const result = await sendMonitoringAlert(message, 'high');
if (result.success) {
logger.info(`[RXPower] Notifikasi WhatsApp terkirim: ${result.message}`);
} else {
logger.warn(`[RXPower] Gagal mengirim notifikasi WhatsApp: ${result.message}`);
}
} catch (error) {
logger.error(`[RXPower] Error mengirim notifikasi WhatsApp: ${error.message}`);
}
} else {
logger.warn('[RXPower] WhatsApp bot tidak tersedia untuk mengirim notifikasi');
}
logger.info(`[RXPower] Pesan peringatan dibuat untuk ${criticalDevices.length} perangkat`);
} else {
logger.info('[RXPower] Tidak ada perangkat dengan nilai RXPower di bawah threshold');
}
return {
success: true,
criticalDevices,
message: `${criticalDevices.length} perangkat memiliki RXPower di atas threshold`
};
} catch (error) {
logger.error(`[RXPower] Error memantau RXPower: ${error.message || String(error)}`);
return {
success: false,
message: `Error memantau RXPower: ${error.message}`,
error
};
}
}
// Helper function untuk mendapatkan nilai RXPower
function getRXPowerValue(device, path) {
try {
// Split path menjadi parts
const parts = path.split('.');
let current = device;
// Navigate through nested properties
for (const part of parts) {
if (!current) return null;
current = current[part];
}
// Check if it's a GenieACS parameter object
if (current && current._value !== undefined) {
return current._value;
}
return null;
} catch (error) {
logger.debug(`[RXPower] Error getting RXPower from path ${path}: ${error.message || String(error)}`);
return null;
}
}
// Helper function untuk mendapatkan serial number
function getDeviceSerialNumber(device) {
try {
const serialPaths = [
'DeviceID.SerialNumber',
'InternetGatewayDevice.DeviceInfo.SerialNumber',
'Device.DeviceInfo.SerialNumber'
];
for (const path of serialPaths) {
const parts = path.split('.');
let current = device;
for (const part of parts) {
if (!current) break;
current = current[part];
}
if (current && current._value !== undefined) {
return current._value;
}
}
// Fallback ke ID perangkat jika serial number tidak ditemukan
if (device._id) {
const parts = device._id.split('-');
if (parts.length >= 3) {
return parts[2];
}
return device._id;
}
return 'Unknown';
} catch (error) {
logger.error('Error getting device serial number:', error);
return 'Unknown';
}
}
// Fungsi untuk memantau perangkat yang tidak aktif (offline)
async function monitorOfflineDevices(thresholdHours = 24) {
try {
logger.info(`[Offline] Memulai pemantauan perangkat offline dengan threshold ${thresholdHours} jam`);
// Ambil semua perangkat
const devices = await genieacsApi.getDevices();
logger.info(`[Offline] Memeriksa status untuk ${devices.length} perangkat...`);
const offlineDevices = [];
const now = new Date();
const thresholdMs = thresholdHours * 60 * 60 * 1000; // Convert jam ke ms
// Periksa setiap perangkat
for (const device of devices) {
try {
if (!device._lastInform) {
logger.info(`[Offline] Perangkat ${device._id} tidak memiliki lastInform`);
continue;
}
const lastInformTime = new Date(device._lastInform).getTime();
const timeDiff = now.getTime() - lastInformTime;
// Jika perangkat belum melakukan inform dalam waktu yang melebihi threshold
if (timeDiff > thresholdMs) {
const deviceInfo = {
id: device._id,
serialNumber: getDeviceSerialNumber(device),
lastInform: device._lastInform,
offlineHours: Math.round(timeDiff / (60 * 60 * 1000) * 10) / 10 // Jam dengan 1 desimal
};
offlineDevices.push(deviceInfo);
logger.info(`[Offline] Perangkat offline: ${deviceInfo.id}, Offline selama: ${deviceInfo.offlineHours} jam`);
}
} catch (deviceError) {
logger.error(`[Offline] Error memeriksa status untuk perangkat ${device._id}: ${deviceError.message || String(deviceError)}`);
}
}
// Jika ada perangkat yang offline
if (offlineDevices.length > 0) {
// Buat pesan peringatan
let message = `*PERINGATAN: PERANGKAT OFFLINE*\n\n`;
message += `${offlineDevices.length} perangkat offline lebih dari ${thresholdHours} jam:\n\n`;
offlineDevices.forEach((device, index) => {
message += `${index + 1}. *Device ID:* ${device.id.split('-')[2] || device.id}\n`;
message += ` 📱 *S/N:* ${device.serialNumber}\n`;
message += ` ⏱️ *Offline:* ${device.offlineHours} jam\n`;
message += ` 🕒 *Last Inform:* ${new Date(device.lastInform).toLocaleString('id-ID')}\n\n`;
});
message += `⚠️ *Mohon segera ditindaklanjuti.*`;
// Kirim notifikasi WhatsApp jika tersedia
if (sendMonitoringAlert) {
try {
const result = await sendMonitoringAlert(message, 'medium');
if (result.success) {
logger.info(`[Offline] Notifikasi WhatsApp terkirim: ${result.message}`);
} else {
logger.warn(`[Offline] Gagal mengirim notifikasi WhatsApp: ${result.message}`);
}
} catch (error) {
logger.error(`[Offline] Error mengirim notifikasi WhatsApp: ${error.message}`);
}
} else {
logger.warn('[Offline] WhatsApp bot tidak tersedia untuk mengirim notifikasi');
}
logger.info(`[Offline] Pesan peringatan perangkat offline terkirim untuk ${offlineDevices.length} perangkat`);
} else {
logger.info('[Offline] Tidak ada perangkat yang offline lebih dari threshold');
}
return {
success: true,
offlineDevices,
message: `${offlineDevices.length} perangkat offline lebih dari ${thresholdHours} jam`
};
} catch (error) {
logger.error(`[Offline] Error memantau perangkat offline: ${error.message || String(error)}`);
return {
success: false,
message: `Error memantau perangkat offline: ${error.message}`,
error
};
}
}
// Jadwalkan monitoring setiap 6 jam
function scheduleMonitoring() {
// Jalankan sekali saat startup (delay lebih lama untuk stabilitas)
setTimeout(async () => {
logger.info('[Monitoring] Menjalankan pemantauan RXPower awal...');
try {
await monitorRXPower();
} catch (error) {
logger.error('[Monitoring] Error pada pemantauan RXPower awal:', error.message);
}
logger.info('[Monitoring] Menjalankan pemantauan perangkat offline awal...');
try {
await monitorOfflineDevices();
} catch (error) {
logger.error('[Monitoring] Error pada pemantauan offline awal:', error.message);
}
// Jadwalkan secara berkala dengan error handling
setInterval(async () => {
logger.info('[Monitoring] Menjalankan pemantauan RXPower terjadwal...');
try {
await monitorRXPower();
} catch (error) {
logger.error('[Monitoring] Error pada pemantauan RXPower:', error.message);
}
}, 6 * 60 * 60 * 1000); // Setiap 6 jam
setInterval(async () => {
logger.info('[Monitoring] Menjalankan pemantauan perangkat offline terjadwal...');
try {
await monitorOfflineDevices();
} catch (error) {
logger.error('[Monitoring] Error pada pemantauan offline:', error.message);
}
}, 12 * 60 * 60 * 1000); // Setiap 12 jam
}, 10 * 60 * 1000); // Mulai 10 menit setelah server berjalan (lebih stabil)
}
// Jalankan penjadwalan monitoring
scheduleMonitoring();
module.exports = {
// Multi-server helpers
getAllACSServers,
getACSServer,
createAxiosInstance,
isBuiltinAcsEnabled,
// GenieACS API methods (now support multi-server)
getDevices: genieacsApi.getDevices,
getDevice: genieacsApi.getDevice,
getDeviceInfo: genieacsApi.getDeviceInfo,
findDeviceByPhoneNumber: genieacsApi.findDeviceByPhoneNumber,
getDeviceByPhoneNumber: genieacsApi.getDeviceByPhoneNumber,
setParameterValues: genieacsApi.setParameterValues,
reboot: genieacsApi.reboot,
factoryReset: genieacsApi.factoryReset,
getVirtualParameters: genieacsApi.getVirtualParameters,
// Monitoring functions (automatically support multi-server)
monitorRXPower,
monitorOfflineDevices
};