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 = require('../services/waNotifyService').notifyAdmin; 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 };