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

1332 lines
48 KiB
JavaScript

/**
* Logika GenieACS yang dipakai portal web dan bot WhatsApp.
* Updated to support multi-server GenieACS setup.
*/
const axios = require('axios');
const db = require('../config/database');
const { getSettingsWithCache } = require('../config/settingsManager');
const auditTrail = require('./auditTrailService');
const { logger } = require('../config/logger');
const genieacsApi = require('../config/genieacs');
const mikrotikService = require('./mikrotikService');
// Helper: Search device across all servers (always get full data)
async function searchDeviceAcrossServers(query, fullData = true) {
try {
const servers = genieacsApi.getAllACSServers();
for (const server of servers) {
try {
const instance = genieacsApi.createAxiosInstance(server);
const params = {
query: JSON.stringify(query)
};
// Only add projection if explicitly requesting minimal data
if (!fullData) {
params.projection = '_id,_tags';
}
let response;
try {
response = await instance.get('/devices', {
params,
timeout: 2000
});
} catch (e) {
response = await instance.get('/api/devices', {
params,
timeout: 2000
});
}
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(`[CustomerDevice] Device found on ${server.name}`);
return device;
}
} catch (error) {
logger.debug(`[CustomerDevice] Device not found on ${server.name}: ${error.message}`);
}
}
return null;
} catch (error) {
logger.error(`[CustomerDevice] Error searching device: ${error.message}`);
return null;
}
}
async function findDeviceByTag(tag) {
try {
const query = { $or: [{ _id: tag }, { _tags: tag }] };
// Get full data by default
return await searchDeviceAcrossServers(query, true);
} catch (e) {
logger.error(`[CustomerDevice] Error finding device by tag: ${e.message}`);
return null;
}
}
async function findDeviceByPppoe(pppoeUser) {
try {
const user = String(pppoeUser || '').trim();
if (!user) return null;
const keys = [
'VirtualParameters.pppoeUsername',
'VirtualParameters.pppUsername',
...PPPOE_USER_KEYS
];
const query = { $or: keys.map(k => ({ [k]: user })) };
// Get full data by default
return await searchDeviceAcrossServers(query, true);
} catch (e) {
logger.error(`[CustomerDevice] Error finding device by PPPoE: ${e.message}`);
return null;
}
}
async function fetchFullDevice(tag) {
try {
const query = { $or: [{ _id: tag }, { _tags: tag }] };
// Always get full data
return await searchDeviceAcrossServers(query, true);
} catch (e) {
logger.error(`[CustomerDevice] Error fetching full device: ${e.message}`);
return null;
}
}
async function resolveDeviceToken(input) {
const token = String(input ?? '').replace(/[\r\n\t]+/g, '').trim();
if (!token) return null;
const direct = await findDeviceByTag(token);
if (direct && direct._id) return direct;
const byPppoe = await findDeviceByPppoe(token);
if (byPppoe && byPppoe._id) return byPppoe;
const found = await findDeviceWithTagVariants(token);
if (found && found.device && found.device._id) return found.device;
return null;
}
const parameterPaths = {
serialNumber: [
'DeviceID.SerialNumber',
'InternetGatewayDevice.DeviceInfo.SerialNumber',
'Device.DeviceInfo.SerialNumber'
],
model: [
'DeviceID.ProductClass',
'InternetGatewayDevice.DeviceInfo.ModelName',
'Device.DeviceInfo.ModelName',
'ModelName'
],
softwareVersion: [
'InternetGatewayDevice.DeviceInfo.SoftwareVersion',
'Device.DeviceInfo.SoftwareVersion'
],
ssid: [
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID',
'Device.WiFi.SSID.1.SSID',
'Device.WiFi.SSID.2.SSID'
],
rxPower: [
'VirtualParameters.RXPower',
'VirtualParameters.redaman',
'InternetGatewayDevice.WANDevice.1.WANPONInterfaceConfig.RXPower',
'InternetGatewayDevice.DeviceInfo.XponInterface.RXPower',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANOAM.RXPower',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_HW_OpticalSignal.RXPower',
'InternetGatewayDevice.WANDevice.1.X_GponInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_GponInterfaceConfig.RxPower',
'InternetGatewayDevice.WANDevice.1.X_GponInterafceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_GponInterafceConfig.RxPower',
'InternetGatewayDevice.WANDevice.1.X_ZTE_GponInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_ZTE_GponInterfaceConfig.RxPower',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_ZTE_OpticalSignal.RXPower',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.X_ZTE_OpticalSignal.RxPower',
'InternetGatewayDevice.WANDevice.1.X_HW_GponInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_HW_GponInterfaceConfig.RxPower',
'InternetGatewayDevice.WANDevice.1.X_ZTE-COM_WANPONInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_FH_GponInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_CMCC_EponInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_CMCC_GponInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_CT-COM_EponInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_CT-COM_GponInterfaceConfig.RXPower',
'InternetGatewayDevice.WANDevice.1.X_CU_WANEPONInterfaceConfig.OpticalTransceiver.RXPower',
'Device.Optical.Interface.1.OpticalSignalLevel',
'Device.XPON.Interface.1.Stats.RXPower'
],
pppoeIP: [
'VirtualParameters.pppoeIP',
'VirtualParameters.pppIP',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANIPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANIPConnection.1.ExternalIPAddress',
'Device.PPP.Interface.1.ExternalIPAddress',
'Device.IP.Interface.1.IPv4Address.1.IPAddress'
],
pppUsername: [
'VirtualParameters.pppoeUsername',
'VirtualParameters.pppUsername',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.2.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.3.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.2.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.2.Username',
'Device.PPP.Interface.1.Username',
'Device.PPP.Interface.2.Username',
'Device.PPP.Interface.3.Username'
],
uptime: [
'VirtualParameters.getdeviceuptime',
'InternetGatewayDevice.DeviceInfo.UpTime',
'Device.DeviceInfo.UpTime'
],
userConnected: [
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.TotalAssociations',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AssociatedDeviceNumberOfEntries',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.AssociatedDeviceNumberOfEntries',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.Associations',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.Associations',
'InternetGatewayDevice.LANDevice.1.Hosts.HostNumberOfEntries',
'Device.WiFi.AccessPoint.1.AssociatedDeviceNumberOfEntries',
'Device.WiFi.AccessPoint.2.AssociatedDeviceNumberOfEntries',
'Device.Hosts.HostNumberOfEntries'
]
};
// PPPoE IP search keys matching user's template
const PPPOE_IP_KEYS = [
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.1.WANPPPConnection.2.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.2.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.4.WANPPPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.5.WANPPPConnection.1.ExternalIPAddress',
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANPPPConnection.*.ExternalIPAddress',
'Device.PPP.Interface.1.ExternalIPAddress',
'Device.IP.Interface.1.IPv4Address.1.IPAddress'
];
// PPPoE Username search keys matching user's template
const PPPOE_USER_KEYS = [
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.1.WANPPPConnection.2.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.2.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.2.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.4.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.4.WANPPPConnection.2.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.5.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANPPPConnection.*.Username',
'Device.PPP.Interface.1.Username',
'Device.PPP.Interface.2.Username',
'Device.PPP.Interface.3.Username'
];
function getNestedValue(obj, path) {
try {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (!current) return null;
current = current[part];
}
if (current && typeof current === 'object' && '_value' in current) {
return current._value;
}
if (current && typeof current === 'object' && current.hasOwnProperty('_value')) {
return current._value;
}
return current;
} catch (e) {
return null;
}
}
function getWildcardMatches(device, path) {
const parts = path.split('.');
const results = [];
function recurse(current, index, currentPathParts) {
if (current === undefined || current === null) return;
if (index === parts.length) {
let val = current;
if (typeof current === 'object' && '_value' in current) {
val = current._value;
}
results.push({
path: currentPathParts.join('.'),
value: val
});
return;
}
const part = parts[index];
if (part === '*') {
if (typeof current === 'object') {
for (const key of Object.keys(current)) {
if (!key.startsWith('_')) {
recurse(current[key], index + 1, [...currentPathParts, key]);
}
}
}
} else {
if (typeof current === 'object') {
const targetLower = part.toLowerCase();
for (const key of Object.keys(current)) {
if (key.toLowerCase() === targetLower) {
recurse(current[key], index + 1, [...currentPathParts, key]);
}
}
}
}
}
recurse(device, 0, []);
return results;
}
function getDeviceParameterValue(device, keys, filterFn) {
for (const key of keys) {
const matches = getWildcardMatches(device, key);
for (const match of matches) {
if (filterFn) {
if (filterFn(match.path, match.value, device)) {
return match.value;
}
} else if (match.value !== undefined && match.value !== null && match.value !== '') {
return match.value;
}
}
}
return '';
}
function extractPppoeIp(d) {
const ip = getDeviceParameterValue(d, PPPOE_IP_KEYS, (matchedPath, value, device) => {
if (!value || value === '0.0.0.0' || value === '-') return false;
if (matchedPath.includes('WANPPPConnection.')) {
const connectionTypePath = matchedPath.replace('ExternalIPAddress', 'ConnectionType');
const connTypeMatches = getWildcardMatches(device, connectionTypePath);
if (connTypeMatches.length > 0 && connTypeMatches[0].value === 'bridge') {
return false;
}
}
return true;
});
if (ip) return ip;
if (d._ip && d._ip !== '-' && d._ip !== '0.0.0.0') return d._ip;
return 'N/A';
}
function extractPppoeUser(d) {
const user = getDeviceParameterValue(d, PPPOE_USER_KEYS, (matchedPath, value, device) => {
if (!value || value === '-') return false;
if (matchedPath.includes('WANPPPConnection.')) {
const connectionTypePath = matchedPath.replace('Username', 'ConnectionType');
const connTypeMatches = getWildcardMatches(device, connectionTypePath);
if (connTypeMatches.length > 0 && connTypeMatches[0].value === 'PPPoE_Bridged') {
return false;
}
}
return true;
});
return user || 'N/A';
}
function formatUptime(seconds) {
if (!seconds || seconds === 'N/A' || seconds === '-') return seconds || 'N/A';
if (typeof seconds === 'string' && (seconds.includes('d') || seconds.includes(':')) && isNaN(seconds)) {
return seconds;
}
const totalSecs = parseInt(seconds, 10);
if (isNaN(totalSecs)) return seconds || 'N/A';
const days = Math.floor(totalSecs / 86400);
const rem = totalSecs % 86400;
let hrs = Math.floor(rem / 3600);
if (hrs < 10) hrs = "0" + hrs;
const rem2 = rem % 3600;
let mins = Math.floor(rem2 / 60);
if (mins < 10) mins = "0" + mins;
let secs = rem2 % 60;
if (secs < 10) secs = "0" + secs;
return days + "d " + hrs + ":" + mins + ":" + secs;
}
function formatDeviceTimestamp(value) {
if (!value) return '-';
const d = new Date(value);
if (isNaN(d.getTime())) return '-';
return d.toLocaleString('id-ID');
}
function formatRelativeTime(value) {
if (!value) return '-';
const ts = new Date(value).getTime();
if (!Number.isFinite(ts)) return '-';
const diffMs = Date.now() - ts;
if (diffMs < 0) return 'baru saja';
const sec = Math.floor(diffMs / 1000);
if (sec < 60) return `${sec} detik lalu`;
const min = Math.floor(sec / 60);
if (min < 60) return `${min} menit lalu`;
const hour = Math.floor(min / 60);
if (hour < 24) return `${hour} jam lalu`;
const day = Math.floor(hour / 24);
return `${day} hari lalu`;
}
function collectRefreshObjects(device) {
const objects = [];
const isTr181 = !!device?.Device;
if (isTr181) {
// TR-181 Device - validate before pushing
if (device?.Device?.Hosts?.Host) {
objects.push('Device.Hosts.Host');
}
if (device?.Device?.WiFi?.AccessPoint?.['1']) {
objects.push('Device.WiFi.AccessPoint.1.AssociatedDevice');
}
if (device?.Device?.WiFi?.AccessPoint?.['2']) {
objects.push('Device.WiFi.AccessPoint.2.AssociatedDevice');
}
} else {
// TR-098 Device (InternetGatewayDevice) - validate before pushing
// FIX for Error 9005: Only query objects that exist in device cache
// Some devices (e.g. ZTE GM220-S XPON) don't have LANDevice/WiFi support
if (device?.InternetGatewayDevice?.LANDevice?.['1']?.Hosts?.Host) {
objects.push('InternetGatewayDevice.LANDevice.1.Hosts.Host');
}
if (device?.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['1']?.AssociatedDevice) {
objects.push('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AssociatedDevice');
}
if (device?.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['5']) {
objects.push('InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.AssociatedDevice');
}
}
return Array.from(new Set(objects));
}
function getBuiltinSyncState(deviceId) {
const empty = {
syncInProgress: false,
syncPendingCount: 0,
syncLastQueueAt: '',
syncLastQueueLabel: '-',
syncStatusLabel: 'Idle'
};
if (!deviceId) return empty;
try {
const pending = db.prepare(
`SELECT COUNT(*) AS count, MAX(created_at) AS last_queue_at
FROM acs_tasks
WHERE device_id = ?
AND name IN ('refreshObject', 'getParameterValues', 'getParameterNames')
AND status IN ('pending', 'in_progress')`
).get(deviceId);
const count = Number(pending?.count || 0);
const lastQueueAt = String(pending?.last_queue_at || '');
return {
syncInProgress: count > 0,
syncPendingCount: count,
syncLastQueueAt: lastQueueAt,
syncLastQueueLabel: formatDeviceTimestamp(lastQueueAt),
syncStatusLabel: count > 0 ? `Sinkronisasi berjalan (${count})` : 'Idle'
};
} catch (e) {
logger.debug(`[CustomerDevice] Failed reading ACS sync state for ${deviceId}: ${e.message}`);
return empty;
}
}
const PPPOE_UPTIME_KEYS = [
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Uptime',
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.1.WANPPPConnection.2.Uptime',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.1.Uptime',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.2.WANPPPConnection.2.Uptime',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.3.WANPPPConnection.1.Uptime',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.4.WANPPPConnection.1.Uptime',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.5.WANPPPConnection.1.Uptime',
'InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANPPPConnection.*.Uptime',
'Device.PPP.Interface.1.UpTime'
];
function extractPppoeUptime(d) {
let uptimeVal = getDeviceParameterValue(d, PPPOE_UPTIME_KEYS, (matchedPath, value, device) => {
if (value === undefined || value === null || value === '' || value === '-') return false;
if (matchedPath.toLowerCase().includes('wanpppconnection')) {
const connTypePath = matchedPath.substring(0, matchedPath.toLowerCase().lastIndexOf('.uptime')) + '.ConnectionType';
const connTypeMatches = getWildcardMatches(device, connTypePath);
if (connTypeMatches.length > 0 && connTypeMatches[0].value === 'PPPoE_Bridged') {
return false;
}
}
return true;
});
if (!uptimeVal || uptimeVal === '-') {
const UPTIME_PATHS = [
'VirtualParameters.getdeviceuptime',
'InternetGatewayDevice.DeviceInfo.UpTime',
'Device.DeviceInfo.UpTime'
];
for (const path of UPTIME_PATHS) {
const val = getNestedValue(d, path);
if (val && val !== '-' && val !== '') {
uptimeVal = val;
break;
}
}
}
if (uptimeVal) {
return formatUptime(uptimeVal);
}
return 'N/A';
}
function getParameterWithPaths(device, paths) {
let values = [];
for (const p of paths) {
const parts = p.split('.');
let value = device;
for (const part of parts) {
if (value && typeof value === 'object' && part in value) {
value = value[part];
if (value && value._value !== undefined) value = value._value;
} else {
value = undefined;
break;
}
}
if (value !== undefined && value !== null && value !== '' && value !== 'N/A') {
const isIpPath = p.toLowerCase().includes('ipaddress') || p.toLowerCase().includes('pppoeip') || p.toLowerCase().includes('pppip') || p.toLowerCase().includes('pppusername') || p.toLowerCase().includes('pppoeusername');
if (isIpPath && String(value) === '0.0.0.0') {
continue;
}
const isCountParam = p.includes('TotalAssociations') ||
p.includes('AssociatedDeviceNumberOfEntries') ||
p.includes('HostNumberOfEntries');
if (isCountParam) {
// Ensure we push a number
const val = (typeof value === 'object' && value._value !== undefined) ? value._value : value;
values.push(parseInt(val) || 0);
} else {
// If it's still an object, try to get _value or stringify it
if (typeof value === 'object') {
if (value._value !== undefined) return String(value._value);
return 'N/A'; // Don't return raw object
}
return String(value);
}
}
}
if (values.length > 0) {
// If it's a count parameter, sum them up (for dual band)
return values.reduce((a, b) => a + b, 0);
}
return 'N/A';
}
function expandTagCandidates(input) {
const t = String(input || '').trim();
if (!t) return [];
if (/^\d+$/.test(t)) {
const d = t.replace(/\D/g, '');
const set = new Set([d]);
if (d.startsWith('62') && d.length > 2) set.add('0' + d.slice(2));
if (d.startsWith('0')) set.add('62' + d.slice(1));
return [...set];
}
return [t];
}
/** Coba beberapa varian tag (62/0 untuk nomor) sampai device ketemu */
async function findDeviceWithTagVariants(input) {
for (const c of expandTagCandidates(input)) {
const dev = await findDeviceByTag(c);
if (dev) return { device: dev, canonicalTag: c };
}
return null;
}
/** Nomor dari JID WhatsApp @s.whatsapp.net */
function phoneFromPnJid(jid) {
if (!jid || typeof jid !== 'string') return null;
const [user, host] = jid.split('@');
if (!user || host !== 's.whatsapp.net') return null;
return user.replace(/\D/g, '') || null;
}
function mapDeviceData(device, tag, isPppoeActive = false) {
if (!device) return null;
const ssid = getParameterWithPaths(device, parameterPaths.ssid);
const ssidDisplay = ssid === 'N/A' ? '-' : ssid;
const lastInformRaw =
device?._lastInform ||
device?.Events?.Inform ||
device?.InternetGatewayDevice?.DeviceInfo?.['1']?.LastInform?._value ||
'';
const lastInform = formatDeviceTimestamp(lastInformRaw);
const lastSyncRaw = device?._updatedAt || lastInformRaw || '';
const lastSync = formatDeviceTimestamp(lastSyncRaw);
const syncState = getBuiltinSyncState(device?._id);
let status = 'Unknown';
if (device?._lastInform) {
const diffMs = Date.now() - new Date(device._lastInform).getTime();
status = diffMs < 15 * 60 * 1000 ? 'Online' : 'Offline';
} else if (device?.Events?.Inform) {
const diffMs = Date.now() - new Date(device.Events.Inform).getTime();
status = diffMs < 15 * 60 * 1000 ? 'Online' : 'Offline';
}
if (status !== 'Online' && isPppoeActive) {
status = 'Online';
}
let connectedUsers = [];
try {
const hosts = device?.InternetGatewayDevice?.LANDevice?.['1']?.Hosts?.Host || device?.Device?.Hosts?.Host;
if (hosts && typeof hosts === 'object') {
for (const key in hosts) {
if (!isNaN(key)) {
const entry = hosts[key];
connectedUsers.push({
hostname: typeof entry?.HostName === 'object' ? entry?.HostName?._value || '-' : entry?.HostName || '-',
ip: typeof entry?.IPAddress === 'object' ? entry?.IPAddress?._value || '-' : entry?.IPAddress || '-',
mac: typeof entry?.MACAddress === 'object' ? entry?.MACAddress?._value || '-' : entry?.MACAddress || '-',
iface: typeof entry?.InterfaceType === 'object' ? entry?.InterfaceType?._value || '-' : entry?.InterfaceType || entry?.Interface || '-',
status: (
entry?.Active?._value === 'true' ||
entry?.Active?._value === '1' ||
entry?.Active?._value === 1 ||
entry?.Active === true ||
entry?.Active === '1' ||
entry?.Active === 1 ||
String(entry?.Active || '').toLowerCase() === 'online'
) ? 'Online' : 'Offline'
});
}
}
}
const assocCandidates = [
device?.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['1']?.AssociatedDevice,
device?.InternetGatewayDevice?.LANDevice?.['1']?.WLANConfiguration?.['5']?.AssociatedDevice,
device?.Device?.WiFi?.AccessPoint?.['1']?.AssociatedDevice,
device?.Device?.WiFi?.AccessPoint?.['2']?.AssociatedDevice
];
for (const assoc of assocCandidates) {
if (!assoc || typeof assoc !== 'object') continue;
for (const key in assoc) {
if (key === '_value' || key === '_type' || key === '_timestamp') continue;
if (isNaN(key)) continue;
const entry = assoc[key];
const mac =
(typeof entry?.MACAddress === 'object' ? entry?.MACAddress?._value : entry?.MACAddress) ||
(typeof entry?.AssociatedDeviceMACAddress === 'object' ? entry?.AssociatedDeviceMACAddress?._value : entry?.AssociatedDeviceMACAddress) ||
'-';
const ip =
(typeof entry?.IPAddress === 'object' ? entry?.IPAddress?._value : entry?.IPAddress) ||
(typeof entry?.IP === 'object' ? entry?.IP?._value : entry?.IP) ||
'-';
const hostname =
(typeof entry?.HostName === 'object' ? entry?.HostName?._value : entry?.HostName) ||
(typeof entry?.DeviceName === 'object' ? entry?.DeviceName?._value : entry?.DeviceName) ||
'-';
const ssidName =
(typeof entry?.SSID === 'object' ? entry?.SSID?._value : entry?.SSID) ||
(typeof entry?.WLANConfiguration === 'object' ? entry?.WLANConfiguration?._value : entry?.WLANConfiguration) ||
'';
if (!mac || mac === '-') continue;
connectedUsers.push({
hostname: hostname || '-',
ip: ip || '-',
mac: mac || '-',
iface: ssidName ? `WiFi ${ssidName}` : 'WiFi',
status: 'Online'
});
}
}
} catch (e) {}
try {
const seen = new Set();
connectedUsers = connectedUsers.filter((u) => {
const mac = String(u?.mac || '').toUpperCase();
const ip = String(u?.ip || '');
const key = mac ? `${mac}|${ip}` : ip;
if (!key) return false;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
} catch {}
let rxPower = getParameterWithPaths(device, parameterPaths.rxPower);
if (rxPower !== 'N/A' && rxPower !== '-' && rxPower !== '') {
const num = parseFloat(rxPower);
if (!isNaN(num) && num > 0) {
const dbVal = 30 + (Math.log10(num * Math.pow(10, -7)) * 10);
rxPower = (Math.ceil(dbVal * 100) / 100).toFixed(2);
}
}
const pppoeIP = extractPppoeIp(device);
const pppoeUsername = extractPppoeUser(device);
const uptimeRaw = getParameterWithPaths(device, parameterPaths.uptime);
let totalAssociations = getParameterWithPaths(device, parameterPaths.userConnected);
// Fallback: If N/A or 0, count from connectedUsers list (LAN + WLAN)
if ((totalAssociations === 'N/A' || totalAssociations === 0 || totalAssociations === '0') && connectedUsers.length > 0) {
totalAssociations = connectedUsers.filter(u => u.status === 'Online').length;
}
if (totalAssociations === 'N/A' || totalAssociations === '-' || totalAssociations === '' || totalAssociations === null || totalAssociations === undefined) {
totalAssociations = 0;
}
if (typeof totalAssociations === 'string' && /^\d+$/.test(totalAssociations)) {
totalAssociations = parseInt(totalAssociations, 10);
}
function formatUptime(seconds) {
if (!seconds || seconds === 'N/A' || seconds === '-') return seconds || 'N/A';
if (typeof seconds === 'string' && (seconds.includes('d') || seconds.includes(':')) && isNaN(seconds)) {
return seconds;
}
const totalSecs = parseInt(seconds, 10);
if (isNaN(totalSecs)) return seconds || 'N/A';
const days = Math.floor(totalSecs / 86400);
const rem = totalSecs % 86400;
let hrs = Math.floor(rem / 3600);
if (hrs < 10) hrs = "0" + hrs;
const rem2 = rem % 3600;
let mins = Math.floor(rem2 / 60);
if (mins < 10) mins = "0" + mins;
let secs = rem2 % 60;
if (secs < 10) secs = "0" + secs;
return days + "d " + hrs + ":" + mins + ":" + secs;
}
const uptime = formatUptime(uptimeRaw);
const pppoeUptime = extractPppoeUptime(device);
const serialNumber = getParameterWithPaths(device, parameterPaths.serialNumber);
const productClass = getParameterWithPaths(device, parameterPaths.model);
const softwareVersion = getParameterWithPaths(device, parameterPaths.softwareVersion);
const model = productClass;
let lokasi = device?._tags || '-';
if (Array.isArray(lokasi)) lokasi = lokasi.join(', ');
// Suhu ONT (dari parameter ACS bila tersedia)
let temperature = '-';
try {
const _db = require('../config/database');
const _trow = _db.prepare('SELECT params FROM acs_devices WHERE id = ?').get(device._id);
const _tflat = _trow && _trow.params ? JSON.parse(_trow.params) : null;
if (_tflat) {
const _tkey = Object.keys(_tflat).find(k => /temp/i.test(k));
if (_tkey != null) {
let _tv = _tflat[_tkey];
if (_tv && typeof _tv === 'object') _tv = (_tv._value != null ? _tv._value : _tv.value);
const _tn = Number(_tv);
if (Number.isFinite(_tn) && _tn > 0) temperature = (_tn > 200 ? (_tn / 100).toFixed(1) : String(_tn)) + '°C';
}
}
} catch (e) {}
return {
phone: tag,
ssid: ssidDisplay,
temperature,
status,
lastInform,
lastInformRaw: lastInformRaw || '',
lastInformAgo: formatRelativeTime(lastInformRaw),
lastSync,
lastSyncRaw: lastSyncRaw || '',
lastSyncAgo: formatRelativeTime(lastSyncRaw),
syncInProgress: !!syncState.syncInProgress,
syncPendingCount: Number(syncState.syncPendingCount || 0),
syncLastQueueAt: syncState.syncLastQueueAt || '',
syncLastQueueLabel: syncState.syncLastQueueLabel || '-',
syncStatusLabel: syncState.syncStatusLabel || 'Idle',
connectedUsers,
rxPower: rxPower === 'N/A' ? '-' : rxPower,
pppoeIP: pppoeIP === 'N/A' ? '-' : pppoeIP,
pppoeUsername: pppoeUsername === 'N/A' ? '-' : pppoeUsername,
pppoeUptime: pppoeUptime === 'N/A' ? '-' : pppoeUptime,
serialNumber: serialNumber === 'N/A' ? '-' : serialNumber,
productClass: productClass === 'N/A' ? '-' : productClass,
lokasi,
softwareVersion: softwareVersion === 'N/A' ? '-' : softwareVersion,
model: model === 'N/A' ? '-' : model,
uptime: uptime === 'N/A' ? '-' : uptime,
totalAssociations
};
}
async function getCustomerDeviceData(tag) {
const base = await resolveDeviceToken(tag);
if (!base || !base._id) return null;
const device = await fetchFullDevice(base._id);
let isPppoeActive = false;
try {
const pppoeUser = extractPppoeUser(device);
if (pppoeUser && pppoeUser !== 'N/A' && pppoeUser !== '-') {
const activeSessionsMap = await mikrotikService.getActivePppoeSessionsMap().catch(() => new Map());
if (activeSessionsMap.has(pppoeUser.toLowerCase())) {
isPppoeActive = true;
}
}
} catch (e) {}
return mapDeviceData(device, tag, isPppoeActive);
}
function fallbackCustomer(tag) {
return {
phone: tag,
ssid: '-',
status: 'Tidak ditemukan',
lastInform: '-',
lastInformAgo: '-',
lastSync: '-',
lastSyncAgo: '-',
syncInProgress: false,
syncPendingCount: 0,
syncLastQueueLabel: '-',
syncStatusLabel: 'Idle',
connectedUsers: [],
rxPower: '-',
pppoeIP: '-',
pppoeUsername: '-',
pppoeUptime: '-',
serialNumber: '-',
productClass: '-',
lokasi: '-',
softwareVersion: '-',
model: '-',
uptime: '-',
totalAssociations: '-'
};
}
async function updateSSID(tag, newSSID, actor = null) {
try {
const ssidClean = String(newSSID ?? '').replace(/[\r\n\t]+/g, '').trim();
if (!ssidClean) { logger.warn('[updateSSID] SSID kosong, dibatalkan.'); return false; }
const device = await resolveDeviceToken(tag);
if (!device) return false;
const deviceId = encodeURIComponent(device._id);
// Gunakan server yang sesuai
const server = device._acs_server_id ? genieacsApi.getACSServer(device._acs_server_id) : genieacsApi.getACSServer('legacy');
if (!server) return false;
const instance = genieacsApi.createAxiosInstance(server);
const tasksUrl = `/devices/${deviceId}/tasks`;
const parameterValues = [];
// Check supported paths in DB
const db = require('../config/database');
const row = db.prepare('SELECT params FROM acs_devices WHERE id = ?').get(device._id);
const flatParams = row && row.params ? JSON.parse(row.params) : null;
if (flatParams) {
// SSID 2.4G paths
const paths24G = [
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID',
'Device.WiFi.SSID.1.SSID'
];
paths24G.forEach(p => {
if (flatParams[p] !== undefined) {
parameterValues.push([p, ssidClean, 'xsd:string']);
}
});
// SSID 5G paths
const paths5G = [
'Device.WiFi.SSID.2.SSID'
];
for (const idx of [5, 6, 7, 8]) {
paths5G.push(`InternetGatewayDevice.LANDevice.1.WLANConfiguration.${idx}.SSID`);
}
paths5G.forEach(p => {
if (flatParams[p] !== undefined) {
parameterValues.push([p, `${ssidClean}-5G`, 'xsd:string']);
}
});
}
// Fallback if no parameters match or device not bootstrapped yet
if (parameterValues.length === 0) {
parameterValues.push(
['InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID', ssidClean, 'xsd:string'],
['Device.WiFi.SSID.1.SSID', ssidClean, 'xsd:string']
);
}
let ok = false;
try {
await instance.post(tasksUrl, {
name: 'setParameterValues',
parameterValues: parameterValues
}, { timeout: 20000 });
ok = true;
} catch (e) {
logger.error(`[updateSSID] Failed to set SSID: ${e.message}`);
}
// Refresh + inform dijalankan di latar belakang agar respons cepat (tidak menahan UI)
if (ok) {
instance.post(tasksUrl, { name: 'refreshObject', objectName: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration' }, { timeout: 12000 }).catch(() => {});
instance.post(tasksUrl, { name: 'inform' }, { timeout: 12000 }).catch(() => {});
logger.info(`[updateSSID] Set SSID dikirim untuk ${tag}; refresh & inform berjalan di latar belakang`);
}
// Catat audit trail jika berhasil
if (ok && actor) {
auditTrail.logAuditTrail({
action: 'UPDATE_SSID',
entity_type: 'device',
entity_id: tag,
actor_type: actor.type || 'unknown',
actor_id: actor.id || null,
actor_name: actor.name || null,
details: {
oldSSID: device._id || 'unknown',
newSSID: newSSID
},
ip_address: actor.ip || null,
user_agent: actor.userAgent || null
});
}
return ok;
} catch (e) {
return false;
}
}
async function updatePassword(tag, newPassword, actor = null) {
try {
const pwRaw = String(newPassword ?? '');
const pw = pwRaw.replace(/[\r\n\t]+/g, '').trim();
if (pw.length < 8) {
logger.warn(`[updatePassword] Password too short for tag ${tag}`);
return false;
}
const device = await resolveDeviceToken(tag);
if (!device) {
logger.warn(`[updatePassword] Device not found for tag ${tag}`);
return false;
}
const deviceId = encodeURIComponent(device._id);
// Gunakan server yang sesuai
const server = device._acs_server_id ? genieacsApi.getACSServer(device._acs_server_id) : genieacsApi.getACSServer('legacy');
if (!server) return false;
const instance = genieacsApi.createAxiosInstance(server);
const tasksUrl = `/devices/${deviceId}/tasks`;
logger.info(`[updatePassword] Setting password for device ${deviceId}, tag ${tag}`);
const parameterValues = [];
// Check supported paths in DB
const db = require('../config/database');
const row = db.prepare('SELECT params FROM acs_devices WHERE id = ?').get(device._id);
const flatParams = row && row.params ? JSON.parse(row.params) : null;
if (flatParams) {
// 2.4G password paths
const paths24G = [
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase',
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey',
'Device.WiFi.AccessPoint.1.Security.KeyPassphrase',
'Device.WiFi.AccessPoint.1.Security.PreSharedKey'
];
paths24G.forEach(p => {
if (flatParams[p] !== undefined) {
parameterValues.push([p, pw, 'xsd:string']);
}
});
// 5G password paths
const paths5G = [
'Device.WiFi.AccessPoint.2.Security.KeyPassphrase',
'Device.WiFi.AccessPoint.2.Security.PreSharedKey'
];
for (const idx of [5, 6, 7, 8]) {
paths5G.push(
`InternetGatewayDevice.LANDevice.1.WLANConfiguration.${idx}.KeyPassphrase`,
`InternetGatewayDevice.LANDevice.1.WLANConfiguration.${idx}.PreSharedKey.1.KeyPassphrase`,
`InternetGatewayDevice.LANDevice.1.WLANConfiguration.${idx}.PreSharedKey.1.PreSharedKey`
);
}
paths5G.forEach(p => {
if (flatParams[p] !== undefined) {
parameterValues.push([p, pw, 'xsd:string']);
}
});
}
// Fallback if no parameters match or device not bootstrapped yet
if (parameterValues.length === 0) {
parameterValues.push(
['InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase', pw, 'xsd:string'],
['InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase', pw, 'xsd:string'],
['InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey', pw, 'xsd:string'],
['Device.WiFi.AccessPoint.1.Security.KeyPassphrase', pw, 'xsd:string']
);
}
let ok = false;
try {
await instance.post(tasksUrl, {
name: 'setParameterValues',
parameterValues: parameterValues
}, { timeout: 20000 });
ok = true;
} catch (e) {
logger.error(`[updatePassword] Failed to set password: ${e.message}`);
}
// Refresh di latar belakang agar respons cepat
instance.post(tasksUrl, { name: 'refreshObject', objectName: 'InternetGatewayDevice.LANDevice.1.WLANConfiguration' }, { timeout: 12000 }).catch(() => {});
// Skip Device.WiFi.AccessPoint refresh karena tidak semua ONU support (CIOT tidak support)
// Catat audit trail jika berhasil
if (ok && actor) {
auditTrail.logAuditTrail({
action: 'UPDATE_PASSWORD',
entity_type: 'device',
entity_id: tag,
actor_type: actor.type || 'unknown',
actor_id: actor.id || null,
actor_name: actor.name || null,
details: {
device_id: deviceId
},
ip_address: actor.ip || null,
user_agent: actor.userAgent || null
});
}
return ok;
} catch (e) {
logger.error(`[updatePassword] Error: ${e.message}`, e.response?.data || '');
return false;
}
}
async function requestRefresh(tag, actor = null) {
try {
const device = await resolveDeviceToken(tag);
if (!device || !device._id) {
return { ok: false, message: 'Perangkat tidak ditemukan.' };
}
const server = device._acs_server_id
? genieacsApi.getACSServer(device._acs_server_id)
: genieacsApi.getACSServer('legacy');
if (!server) {
return { ok: false, message: 'Server ACS tidak ditemukan.' };
}
if (server.id === 'builtin') {
try {
const pending = db.prepare(
`SELECT COUNT(*) AS c
FROM acs_tasks
WHERE device_id = ?
AND name IN ('refreshObject', 'getParameterValues', 'getParameterNames')
AND status IN ('pending', 'in_progress')`
).get(device._id);
if (pending && pending.c > 0) {
return { ok: true, message: 'Sinkronisasi ONU masih berjalan. Mohon tunggu sebentar.' };
}
} catch (e) {
logger.debug(`[CustomerDevice] Unable to check pending ACS tasks for ${device._id}: ${e.message}`);
}
}
const instance = genieacsApi.createAxiosInstance(server);
const tasksUrl = `/devices/${encodeURIComponent(device._id)}/tasks`;
const refreshObjects = collectRefreshObjects(device);
for (const objectName of refreshObjects) {
await instance.post(tasksUrl, { name: 'refreshObject', objectName }, { timeout: 15000 });
}
if (actor) {
auditTrail.logAuditTrail({
action: 'REFRESH_DEVICE',
entity_type: 'device',
entity_id: tag,
actor_type: actor.type || 'unknown',
actor_id: actor.id || null,
actor_name: actor.name || null,
details: {
device_id: device._id,
refreshObjects
},
ip_address: actor.ip || null,
user_agent: actor.userAgent || null
});
}
return {
ok: true,
message: `Sinkronisasi ONU dimulai. ${refreshObjects.length} jalur TR-069 dipoll.`,
deviceId: device._id,
acsServerId: server.id
};
} catch (e) {
logger.error(`[CustomerDevice] Error requesting refresh for ${tag}: ${e.message}`);
return { ok: false, message: 'Gagal memulai sinkronisasi ONU.' };
}
}
async function requestReboot(tag, actor = null) {
const device = await resolveDeviceToken(tag);
if (!device || !device._id) return { ok: false, message: 'Perangkat tidak ditemukan.' };
const server = device._acs_server_id ? genieacsApi.getACSServer(device._acs_server_id) : genieacsApi.getACSServer('legacy');
if (!server) return { ok: false, message: 'Server ACS tidak ditemukan.' };
const instance = genieacsApi.createAxiosInstance(server);
try {
await instance.post(
`/devices/${encodeURIComponent(device._id)}/tasks`,
{ name: 'reboot', timestamp: new Date().toISOString() }
);
// Catat audit trail jika berhasil
if (actor) {
auditTrail.logAuditTrail({
action: 'REBOOT_DEVICE',
entity_type: 'device',
entity_id: tag,
actor_type: actor.type || 'unknown',
actor_id: actor.id || null,
actor_name: actor.name || null,
details: {
device_id: device._id
},
ip_address: actor.ip || null,
user_agent: actor.userAgent || null
});
}
return { ok: true, message: 'Perintah reboot terkirim. Tunggu beberapa menit hingga ONU online.' };
} catch (e) {
return { ok: false, message: 'Gagal mengirim reboot ke GenieACS.' };
}
}
/** Daftar perangkat yang punya minimal satu tag (untuk admin WA). */
async function listDevicesWithTags(limit = 250) {
const servers = genieacsApi.getAllACSServers();
const queries = [
{ _tags: { $exists: true, $ne: [] } },
{ _tags: { $exists: true, $not: { $size: 0 } } },
{ '_tags.0': { $exists: true } }
];
const projection = [
'_id',
'_tags',
'_lastInform',
'DeviceID.SerialNumber',
'VirtualParameters.pppoeUsername',
'VirtualParameters.pppUsername',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username',
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username._value'
].join(',');
let allDevices = [];
const maxLimit = Math.max(1, Math.min(parseInt(limit, 10) || 250, 500));
for (const server of servers) {
let found = false;
for (const query of queries) {
try {
const instance = genieacsApi.createAxiosInstance(server);
let response;
try {
response = await instance.get(`/devices`, {
params: {
query: JSON.stringify(query),
limit: maxLimit,
projection
},
timeout: 45000
});
} catch (e) {
response = await instance.get(`/api/devices`, {
params: {
query: JSON.stringify(query),
limit: maxLimit,
projection
},
timeout: 45000
});
}
const rows = Array.isArray(response.data) ? response.data : [];
if (rows.length > 0) {
rows.forEach(d => {
d._acs_server_id = server.id;
d._acs_server_name = server.name;
});
allDevices.push(...rows);
found = true;
break;
}
} catch (e) {
/* coba query alternatif */
}
}
}
if (allDevices.length > 0) {
return { ok: true, devices: allDevices.slice(0, limit) };
}
return { ok: false, devices: [], message: 'Gagal mengambil daftar dari GenieACS.' };
}
/** Mengambil semua perangkat tanpa melihat tag. */
async function listAllDevices(limit = 999999, acsId = null) {
let servers = genieacsApi.getAllACSServers();
if (acsId && acsId !== 'all') {
servers = servers.filter(s => String(s.id) === String(acsId));
}
let allDevices = [];
let lastError = null;
// Query servers in parallel using Promise.allSettled
const promises = servers.map(async (server) => {
try {
const instance = genieacsApi.createAxiosInstance(server);
const params = {
limit,
projection: '_id,_tags,_lastInform,DeviceID.SerialNumber,VirtualParameters,InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username,InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.2.Username,InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress,Device.PPP.Interface.1.Username,Device.PPP.Interface.1.ExternalIPAddress,InternetGatewayDevice.DeviceInfo.ModelName,InternetGatewayDevice.DeviceInfo.SoftwareVersion,InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID,InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations,InternetGatewayDevice.LANDevice.1.WLANConfiguration.5.TotalAssociations,InternetGatewayDevice.LANDevice.1.Hosts.HostNumberOfEntries,Device.WiFi.AccessPoint.1.AssociatedDeviceNumberOfEntries,Device.Hosts.HostNumberOfEntries,InternetGatewayDevice.LANDevice.1.Hosts.Host,Device.Hosts.Host'
};
let response;
try {
response = await instance.get(`/devices`, { params, timeout: 8000 });
} catch (e) {
response = await instance.get(`/api/devices`, { params, timeout: 8000 });
}
const rows = Array.isArray(response.data) ? response.data : [];
rows.forEach(d => {
d._acs_server_id = server.id;
d._acs_server_name = server.name;
});
return rows;
} catch (e) {
logger.error(`[CustomerDevice] Error listing devices on ${server.name}: ${e.message}`);
throw e;
}
});
const results = await Promise.allSettled(promises);
results.forEach((r) => {
if (r.status === 'fulfilled') {
allDevices.push(...r.value);
} else {
lastError = r.reason;
}
});
if (allDevices.length > 0 || !lastError) {
return { ok: true, devices: allDevices.slice(0, limit) };
}
return { ok: false, devices: [], message: 'Gagal mengambil daftar dari GenieACS: ' + (lastError ? lastError.message : 'Unknown error') };
}
async function updateCustomerTag(oldTag, newTag) {
const device = await findDeviceByTag(oldTag);
if (!device || !device._id) return { ok: false, message: 'Perangkat tidak ditemukan.' };
const server = device._acs_server_id ? genieacsApi.getACSServer(device._acs_server_id) : genieacsApi.getACSServer('legacy');
if (!server) return { ok: false, message: 'Server ACS tidak ditemukan.' };
const instance = genieacsApi.createAxiosInstance(server);
try {
const tags = Array.isArray(device._tags) ? device._tags.filter((t) => t !== oldTag) : [];
tags.push(newTag);
await instance.put(
`/devices/${encodeURIComponent(device._id)}`,
{ _id: device._id, _tags: tags }
);
return { ok: true };
} catch (e) {
return { ok: false, message: 'Gagal mengubah tag.' };
}
}
module.exports = {
findDeviceByTag,
findDeviceByPppoe,
fetchFullDevice,
resolveDeviceToken,
mapDeviceData,
extractPppoeUser,
getCustomerDeviceData,
fallbackCustomer,
requestRefresh,
updateSSID,
updatePassword,
requestReboot,
updateCustomerTag,
listDevicesWithTags,
listAllDevices,
expandTagCandidates,
findDeviceWithTagVariants,
phoneFromPnJid
};