Wire CP-78xx probe discovery, relay phone-probe commands, and a chat follow-up message so store desk phones get registration, switch, and provisioning detail alongside DECT and WAN diagnostics. Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
6.6 KiB
JavaScript
184 lines
6.6 KiB
JavaScript
// integrations/cisco-mpp-phone/statusJson.js
|
|
//
|
|
// Parser for MPP Web UI JSON endpoints (Status.json).
|
|
|
|
import { coerceRows, extractIndexed } from './jsonRows.js';
|
|
|
|
/**
|
|
* @param {string|object|Array} raw
|
|
* @returns {object}
|
|
*/
|
|
export function parseStatusJson(raw) {
|
|
const rows = coerceRows(raw);
|
|
const fields = {};
|
|
const lines = [];
|
|
let currentExt = null;
|
|
|
|
for (const row of rows) {
|
|
const name = typeof row?.name === 'string' ? row.name.trim() : '';
|
|
if (!name) continue;
|
|
const value = row?.value == null ? '' : String(row.value).trim();
|
|
|
|
const extMatch = name.match(/^Ext (\d+) Status$/i);
|
|
if (extMatch) {
|
|
currentExt = Number(extMatch[1]);
|
|
lines.push({ index: currentExt });
|
|
continue;
|
|
}
|
|
|
|
if (currentExt && isLineField(name)) {
|
|
const line = lines.find((l) => l.index === currentExt);
|
|
if (line) assignLineField(line, name, value);
|
|
fields[`Ext ${currentExt} ${name}`] = value;
|
|
continue;
|
|
}
|
|
|
|
if (value) fields[name] = value;
|
|
}
|
|
|
|
const device = {
|
|
product: fields['Product Name'] || null,
|
|
mac: normalizeMac(fields['MAC Address']),
|
|
firmware: fields['Software Version'] || fields['Firmware Version'] || null,
|
|
serial: fields['Serial Number'] || null,
|
|
hostname: fields['Host Name'] || null,
|
|
};
|
|
|
|
const network = {
|
|
ipv4: fields['Current IP'] || null,
|
|
netmask: fields['Current Netmask'] || null,
|
|
gateway: fields['Current Gateway'] || null,
|
|
dns1: fields['Primary DNS'] || null,
|
|
dns2: fields['Secondary DNS'] || fields['Secondary DNS'] || null,
|
|
vlan: fields['Operational VLAN ID'] || fields['VLAN ID'] || null,
|
|
connectionType: fields['Connection Type'] || null,
|
|
ipStatus: fields['IP Status'] || null,
|
|
};
|
|
|
|
const rebootHistory = extractIndexed(fields, /^Reboot Reason \d+$/i);
|
|
const normalizedLines = lines
|
|
.filter((l) => l.registrationState || l.lastRegistrationAt || l.lastRegistrationIp)
|
|
.map((l) => ({
|
|
index: l.index,
|
|
registrationState: l.registrationState || null,
|
|
lastRegistrationAt: l.lastRegistrationAt || null,
|
|
lastRegistrationIp: l.lastRegistrationIp || null,
|
|
nextRegistrationInSeconds: l.nextRegistrationInSeconds || null,
|
|
messageWaiting: l.messageWaiting || null,
|
|
hotelingState: l.hotelingState || null,
|
|
}));
|
|
|
|
const primary = normalizedLines.find((l) => l.index === 1) || normalizedLines[0] || null;
|
|
|
|
const phoneStatus = {
|
|
elapsedTime: fields['Elapsed Time'] || null,
|
|
currentTime: fields['Current Time'] || null,
|
|
linkSpeed: fields['SW Port'] || null,
|
|
linkConfig: fields['SW Port Config'] || null,
|
|
upgradeStatus: fields['Upgrade Status'] || null,
|
|
externalIp: fields['External IP'] || null,
|
|
dot1xStatus: fields['Transaction status'] || null,
|
|
dot1xProtocol: fields['Protocol'] || null,
|
|
ledLine1Color: fields['Line 1 LED Color'] || null,
|
|
ledLine1Cadence: fields['Line 1 LED Cadence'] || null,
|
|
messageWaiting: fields['Ext 1 Message Waiting'] || fields['Message Waiting'] || null,
|
|
hotelingState: fields['Ext 1 Hoteling State'] || fields['Hoteling State'] || null,
|
|
sipBytesSent: fields['SIP Bytes Sent'] || null,
|
|
sipBytesRecv: fields['SIP Bytes Recv'] || null,
|
|
vpnConnected: fields['VPN Connected'] || null,
|
|
ipv6Status: fields['IP Status_IPV6'] || null,
|
|
ipv6Address: fields['Current IP_IPV6'] || null,
|
|
tr069Feature: fields['TR-069 Feature'] || null,
|
|
multicastRx: fields['Multicast Rx Pkts'] || null,
|
|
multicastTx: fields['Multicast Tx Pkts'] || null,
|
|
streamingRx: fields['Streaming Rx Pkts'] || null,
|
|
};
|
|
|
|
return {
|
|
fields,
|
|
device,
|
|
network,
|
|
lines: normalizedLines,
|
|
rebootHistory,
|
|
phoneStatus,
|
|
provisioning: {
|
|
latest: fields['Provisioning Status 1'] || null,
|
|
firmwareUpgrade: fields['Firmware Upgrade Status 1'] || null,
|
|
},
|
|
sip: {
|
|
messagesSent: fields['SIP Messages Sent'] || null,
|
|
messagesRecv: fields['SIP Messages Recv'] || null,
|
|
},
|
|
macAddress: device.mac,
|
|
firmware: device.firmware,
|
|
ipAddress: network.ipv4,
|
|
registration: primary?.registrationState || null,
|
|
};
|
|
}
|
|
|
|
export function summarizePhoneHealthFromJson(parsed) {
|
|
const warnings = [];
|
|
const info = [];
|
|
|
|
if (!parsed?.fields || Object.keys(parsed.fields).length === 0) {
|
|
return { healthy: false, warnings: ['Status.json parsed empty or missing'], info };
|
|
}
|
|
|
|
info.push(`${Object.keys(parsed.fields).length} status field(s) from Status.json`);
|
|
|
|
const line1 = parsed.lines?.find((l) => l.index === 1) || parsed.lines?.[0];
|
|
if (line1?.registrationState) {
|
|
const reg = line1.registrationState.toLowerCase();
|
|
if (reg.includes('not registered') || reg.includes('fail')) {
|
|
warnings.push(`Ext ${line1.index} registration: ${line1.registrationState}`);
|
|
} else {
|
|
info.push(`Ext ${line1.index} registration: ${line1.registrationState}`);
|
|
if (line1.lastRegistrationAt) info.push(`last reg: ${line1.lastRegistrationAt}`);
|
|
if (line1.lastRegistrationIp) info.push(`registrar IP: ${line1.lastRegistrationIp}`);
|
|
}
|
|
} else if (parsed.registration) {
|
|
info.push(`registration: ${parsed.registration}`);
|
|
} else {
|
|
warnings.push('no extension registration state in Status.json');
|
|
}
|
|
|
|
if (parsed.device?.firmware) info.push(`firmware: ${parsed.device.firmware}`);
|
|
if (parsed.device?.product) info.push(`product: ${parsed.device.product}`);
|
|
if (parsed.network?.ipv4) info.push(`ip: ${parsed.network.ipv4}`);
|
|
if (parsed.network?.vlan) info.push(`vlan: ${parsed.network.vlan}`);
|
|
if (parsed.rebootHistory?.length) info.push(`latest reboot: ${parsed.rebootHistory[0]}`);
|
|
|
|
return { healthy: warnings.length === 0, warnings, info };
|
|
}
|
|
|
|
function isLineField(name) {
|
|
return [
|
|
'Registration State',
|
|
'Last Registration At',
|
|
'Last Registration IP',
|
|
'Next Registration In Seconds',
|
|
'Mapped SIP Port',
|
|
'Extended Function Status',
|
|
'Message Waiting',
|
|
'Hoteling State',
|
|
].includes(name);
|
|
}
|
|
|
|
function assignLineField(line, name, value) {
|
|
switch (name) {
|
|
case 'Registration State': line.registrationState = value; break;
|
|
case 'Last Registration At': line.lastRegistrationAt = value; break;
|
|
case 'Last Registration IP': line.lastRegistrationIp = value; break;
|
|
case 'Next Registration In Seconds': line.nextRegistrationInSeconds = value; break;
|
|
case 'Message Waiting': line.messageWaiting = value; break;
|
|
case 'Hoteling State': line.hotelingState = value; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
function normalizeMac(raw) {
|
|
if (!raw || typeof raw !== 'string') return null;
|
|
const hex = raw.replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
|
if (hex.length !== 12) return null;
|
|
return hex.match(/../g).join(':');
|
|
}
|