collabSupport/integrations/cisco-mpp-phone/statusJson.js
jmcqueen f7953b8eb5 Add MPP desk phone diagnostics follow-up to /phonestatus via relay.
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>
2026-07-28 18:01:01 -04:00

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(':');
}