Improve /dectstatus with handset RF context and cleaner base cards.
Surface handset registrations and RSSI, tighten reboot health to 7 days, and consolidate Base / Handsets & RF / Network & RTP sections. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
90a56c4640
commit
2b8c4e06aa
26 changed files with 3128 additions and 134 deletions
|
|
@ -4,8 +4,7 @@
|
|||
// on-prem relay agent. Complements the compact
|
||||
// follow-up that /phonestatus already posts:
|
||||
// this is the full status.xml dump (device,
|
||||
// firmware, reboot log, network, RTP, security,
|
||||
// emergency numbers) plus chat-only action cards
|
||||
// firmware, reboot log, network, RTP) plus chat-only action cards
|
||||
// for reboot / force-reboot / factory-reset.
|
||||
//
|
||||
// Chat surface
|
||||
|
|
@ -30,6 +29,7 @@ import { discoverDectBases } from '../services/dectDiscovery.js';
|
|||
import { collectAll, execAction } from '../services/dectCollectorService.js';
|
||||
import { getDectRelayHub } from '../services/dectRelayHub.js';
|
||||
import { renderDectStatusMarkdown } from '../services/renderers/dectStatusRenderer.js';
|
||||
import { buildHandsetContext } from '../services/dectStatus/buildHandsetContext.js';
|
||||
import { pendingDectActions } from '../utils/pendingDectActions.js';
|
||||
import { extractRequester, describeRequester } from '../utils/requester.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
|
@ -144,10 +144,12 @@ export async function handleDectStatus(bot, trigger) {
|
|||
);
|
||||
|
||||
const results = await collectAll(targets);
|
||||
const handsetCtx = buildHandsetContext(phoneData);
|
||||
const md = renderDectStatusMarkdown(results, {
|
||||
storeNum,
|
||||
relay: relayStatus,
|
||||
discoveryWarnings,
|
||||
handsetCtx,
|
||||
});
|
||||
await bot.say('markdown', md || 'No data available.');
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const SHORT_HELP = {
|
|||
// AV / phones
|
||||
avstatus: 'AV / device status for a store (alias: /wostatus)',
|
||||
phonestatus: 'DECT + IP phone status for a store (plus 7d WAN follow-up)',
|
||||
dectstatus: 'Full DECT basestation dump via relay (reboot / factory-reset cards)',
|
||||
dectstatus: 'Full DECT base dump via relay (handsets, RSSI, reboot cards)',
|
||||
voicediag: 'Deep voice diagnostic: Webex Calling features + SD-WAN quality with fix cards',
|
||||
callreport: 'Daily call digest: correlated CDR calls + per-call Prisma WAN (store, email, or phone)',
|
||||
calltest: 'Twilio voice path test (store AA or direct dial, 60s listen)',
|
||||
|
|
@ -80,7 +80,8 @@ const LONG_HELP = {
|
|||
],
|
||||
notes: [
|
||||
'Aliases: `/dect`.',
|
||||
'Pulls the full `status.xml` dump from every reachable DBS-210 base at the store via the on-prem DECT relay agent (device, firmware, reboot log, network stats, RTP, security, emergency numbers, health verdict).',
|
||||
'Pulls the full `status.xml` dump from every reachable DBS-210 base at the store via the on-prem DECT relay agent (device, firmware, handsets & RF, reboot log, network stats, RTP, health verdict).',
|
||||
'Also shows **Webex handset registrations** per base (extension, last registration) and **RF signal (RSSI)** from the base when handsets are registered.',
|
||||
'Optional second arg filters to one base by IP, MAC, or name substring.',
|
||||
'Chat-only action cards per reachable base: **Reboot**, **Force Reboot**, **Factory Reset**. Each requires a confirm click; outcomes are audited under `dect:audit`.',
|
||||
'Requires `DECT_RELAY_AGENT_TOKEN` on the bot and a live `dect-relay-agent` in the data center. When the relay is offline the command still reports discovery results and the offline state.',
|
||||
|
|
|
|||
|
|
@ -121,10 +121,30 @@ All frames are JSON, one per WebSocket message.
|
|||
{ "type": "hello",
|
||||
"agentVersion": "0.1.0",
|
||||
"hostname": "dc-dect-relay-01",
|
||||
"capabilities": ["collect","reboot","force-reboot","reboot-chain",
|
||||
"capabilities": ["collect","collect-raw","reboot","force-reboot","reboot-chain",
|
||||
"force-reboot-chain","factory-reset","reconfigure-tree"] }
|
||||
```
|
||||
|
||||
**Bot → Agent (collect with raw XML for fixture capture):**
|
||||
```json
|
||||
{ "id": "cmd_<uuid>", "type": "collect", "baseIp": "10.4.11.87", "includeRaw": true }
|
||||
```
|
||||
|
||||
Legacy agents may also accept `{ "type": "collect-raw", "baseIp": "..." }`.
|
||||
|
||||
**Agent → Bot (collect + includeRaw reply):**
|
||||
```json
|
||||
{ "id": "cmd_<uuid>", "ok": true, "elapsedMs": 812,
|
||||
"result": { "rawXml": "<Status>...</Status>", "byteLength": 12345,
|
||||
"sectionInventory": [...], "parsed": { ... }, "verdict": { ... } } }
|
||||
```
|
||||
|
||||
**Capture script (from repo root, bot running with relay connected):**
|
||||
```bash
|
||||
node scripts/fetchDectStatusXml.js 782 --save-dir tests/fixtures/dect/
|
||||
# Optional: --base 10.4.11.87 --url https://your-bot-host/CollabSupport
|
||||
```
|
||||
|
||||
**Bot → Agent (command):**
|
||||
```json
|
||||
{ "id": "cmd_<uuid>", "type": "collect", "baseIp": "10.4.11.87" }
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
parseStatusXml,
|
||||
summarizeBaseHealth,
|
||||
} from '../integrations/cisco-dect/statusXml.js';
|
||||
import { inventoryStatusXml } from '../integrations/cisco-dect/statusXmlInventory.js';
|
||||
|
||||
// ─── Config ─────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -49,9 +50,10 @@ const CFG = {
|
|||
reconnectMaxMs: Number(process.env.DECT_RELAY_RECONNECT_MAX_MS) || 30_000,
|
||||
};
|
||||
|
||||
const AGENT_VERSION = '0.1.0';
|
||||
const AGENT_VERSION = '0.2.0';
|
||||
const CAPABILITIES = [
|
||||
'collect',
|
||||
'collect-raw',
|
||||
'reboot',
|
||||
'force-reboot',
|
||||
'reboot-chain',
|
||||
|
|
@ -247,7 +249,8 @@ async function dispatch(cmd) {
|
|||
});
|
||||
|
||||
switch (cmd.type) {
|
||||
case 'collect': {
|
||||
case 'collect':
|
||||
case 'collect-raw': {
|
||||
const started = Date.now();
|
||||
const resp = await client.get('/admin/status.xml');
|
||||
const elapsedMs = Date.now() - started;
|
||||
|
|
@ -256,9 +259,22 @@ async function dispatch(cmd) {
|
|||
err.code = 'BASE_BAD_STATUS';
|
||||
throw err;
|
||||
}
|
||||
const parsed = parseStatusXml(resp.data);
|
||||
const rawXml = resp.data;
|
||||
const parsed = parseStatusXml(rawXml);
|
||||
const verdict = summarizeBaseHealth(parsed);
|
||||
return { parsed, verdict, fetchedInMs: elapsedMs };
|
||||
const base = { parsed, verdict, fetchedInMs: elapsedMs };
|
||||
const wantRaw = cmd.includeRaw === true
|
||||
|| cmd.includeRaw === 'true'
|
||||
|| cmd.type === 'collect-raw';
|
||||
if (wantRaw) {
|
||||
return {
|
||||
...base,
|
||||
rawXml,
|
||||
byteLength: Buffer.byteLength(rawXml, 'utf8'),
|
||||
sectionInventory: inventoryStatusXml(rawXml),
|
||||
};
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
// All mutating actions are one-shot GETs (see integrations/cisco-
|
||||
|
|
|
|||
24
index.js
24
index.js
|
|
@ -169,6 +169,30 @@ app.get('/phone/devices/build/:storeNumber', dataAuthGate, async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
import { captureDectRawXml } from './services/dectStatus/captureRawXml.js';
|
||||
app.get('/api/dect/raw-xml/:storeNumber', dataAuthGate, async (req, res) => {
|
||||
const storeNumber = (req.params.storeNumber || '').trim();
|
||||
const baseFilter = (req.query.base || req.query.ip || req.query.mac || '').trim();
|
||||
|
||||
try {
|
||||
const result = await captureDectRawXml(storeNumber, { baseFilter: baseFilter || undefined });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
const code = err?.code || 'CAPTURE_FAILED';
|
||||
const status = code === 'INVALID_STORE' ? 400
|
||||
: code === 'BASE_NOT_FOUND' ? 404
|
||||
: code === 'RELAY_NOT_CONFIGURED' ? 503
|
||||
: 500;
|
||||
logger('dect:capture', `raw-xml failed for store ${storeNumber}: ${err.message}`, 'error');
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
code,
|
||||
message: err.message,
|
||||
knownBases: err.knownBases || undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/av/device/:storeNumber/:identifier', dataAuthGate, async (req, res) => {
|
||||
const result = await getShapedDeviceData(req.params.storeNumber, req.params.identifier);
|
||||
res.json(result);
|
||||
|
|
|
|||
|
|
@ -172,6 +172,10 @@ export function parseStatusXml(xml) {
|
|||
const emergency = extractEmergencyNumbers(sys.Emergency_Calls);
|
||||
const deviceFwu = extractDeviceFwu(sys.Device_FWU_Info);
|
||||
const rssi = extractRssiList(sys.RSSI_List);
|
||||
const devicePresence = extractDevicePresence(sys.Device_Presence);
|
||||
const sipIdentityStatus = extractSipIdentityStatus(sys.SIP_Identity_Status);
|
||||
const devices = extractDeviceInformation(root);
|
||||
const perRpnStats = extractPerRpnStatistics(stats);
|
||||
|
||||
return {
|
||||
device: {
|
||||
|
|
@ -236,13 +240,16 @@ export function parseStatusXml(xml) {
|
|||
},
|
||||
emergencyNumbers: emergency,
|
||||
rssi,
|
||||
devicePresence,
|
||||
sipIdentityStatus,
|
||||
devices,
|
||||
perRpnStats,
|
||||
features: {
|
||||
pushToTalk: (str(sys.Push_To_Talk) || '').toLowerCase() === 'on',
|
||||
},
|
||||
// The Statistics/Header_Line_Idx tag is a CSV schema descriptor
|
||||
// for a companion (per-RPN) statistics section we haven't
|
||||
// captured yet. Kept as raw so a future collector can align to
|
||||
// it without reparsing here.
|
||||
// for companion per-RPN statistics rows (see perRpnStats).
|
||||
statisticsHeader: str(stats.Header_Line_Idx),
|
||||
_rawStatisticsHeader: str(stats.Header_Line_Idx),
|
||||
};
|
||||
}
|
||||
|
|
@ -255,9 +262,27 @@ export function parseStatusXml(xml) {
|
|||
// shows up as "unknown" instead of being silently reclassified.
|
||||
const KNOWN_REBOOT_CODES = new Map([
|
||||
[21, { key: 'normal', severity: 'info', label: 'Normal reboot (admin- or firmware-initiated)' }],
|
||||
[43, { key: 'unexpected', severity: 'warn', label: 'Unexpected reboot' }],
|
||||
[80, { key: 'power-loss', severity: 'warn', label: 'Power loss — mains interruption or PoE glitch' }],
|
||||
]);
|
||||
|
||||
/** Reboot reason codes that affect health when seen within the warn window. */
|
||||
const REBOOT_HEALTH_WARN_CODES = new Set([43, 80]);
|
||||
const REBOOT_HEALTH_WARN_DAYS = 7;
|
||||
|
||||
function rebootEntryAgeMs(entry) {
|
||||
if (!entry?.at) return null;
|
||||
const t = Date.parse(entry.at);
|
||||
return Number.isFinite(t) ? Date.now() - t : null;
|
||||
}
|
||||
|
||||
function isRecentRebootConcern(entry, maxAgeMs = REBOOT_HEALTH_WARN_DAYS * 86400000) {
|
||||
if (!REBOOT_HEALTH_WARN_CODES.has(entry?.reasonCode)) return false;
|
||||
const ageMs = rebootEntryAgeMs(entry);
|
||||
if (ageMs == null) return false;
|
||||
return ageMs >= 0 && ageMs <= maxAgeMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a pure-function health verdict from a parsed status object.
|
||||
* No I/O. Returns { healthy, warnings, info } where warnings is an
|
||||
|
|
@ -277,12 +302,14 @@ export function summarizeBaseHealth(parsed) {
|
|||
warnings.push(`Base rebooted very recently (uptime ${Math.round(uptimeSec / 60)} min)`);
|
||||
}
|
||||
|
||||
// Reboot log — surface any power-loss in the last 6 boots (that's
|
||||
// literally as far back as the device remembers).
|
||||
const powerLosses = (parsed.rebootLog || []).filter((r) => r.reasonCode === 80);
|
||||
if (powerLosses.length > 0) {
|
||||
// Reboot log — power-loss or unexpected reboot within the last 7 days.
|
||||
const badReboots = (parsed.rebootLog || []).filter((entry) => isRecentRebootConcern(entry));
|
||||
if (badReboots.length > 0) {
|
||||
const latest = badReboots[0];
|
||||
const label = latest.reasonName || `code ${latest.reasonCode}`;
|
||||
warnings.push(
|
||||
`${powerLosses.length} recent power-loss reboot(s); most recent at ${powerLosses[0].at}`,
|
||||
`${badReboots.length} concerning reboot(s) in the last ${REBOOT_HEALTH_WARN_DAYS} days ` +
|
||||
`(latest: ${label} at ${latest.at})`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -362,11 +389,23 @@ function normalizeMac(mac) {
|
|||
* We normalize to a role token + keep the flavor text.
|
||||
*/
|
||||
function parseMultiCell(raw) {
|
||||
if (!raw) return { role: null, raw: null };
|
||||
const s = String(raw);
|
||||
const m = s.match(/^([A-Za-z]+)/);
|
||||
if (!raw) return { role: null, state: null, raw: null };
|
||||
const s = String(raw).trim();
|
||||
|
||||
const leadRole = s.match(/^(Primary|Secondary|Unchained)\b/i);
|
||||
if (leadRole) {
|
||||
return { role: leadRole[1].toLowerCase(), state: null, raw: s };
|
||||
}
|
||||
|
||||
const leadState = s.match(/^([A-Za-z]+)/);
|
||||
const state = leadState ? leadState[1].toLowerCase() : null;
|
||||
|
||||
const trailRole = s.match(/\)\s+(Primary|Secondary)\b/i);
|
||||
const role = trailRole ? trailRole[1].toLowerCase() : null;
|
||||
|
||||
return {
|
||||
role: m ? m[1].toLowerCase() : null, // "unchained" | "primary" | "secondary"
|
||||
role,
|
||||
state: role && state && role !== state ? state : null,
|
||||
raw: s,
|
||||
};
|
||||
}
|
||||
|
|
@ -377,10 +416,16 @@ function parseMultiCell(raw) {
|
|||
*/
|
||||
function parseOperatingSeconds(v) {
|
||||
if (!v) return null;
|
||||
const m = String(v).match(/(\d+):(\d+):(\d+)/);
|
||||
const s = String(v);
|
||||
const daysMatch = s.match(/(\d+)\s+Days?\s+(\d+):(\d+):(\d+)/i);
|
||||
if (daysMatch) {
|
||||
const [, d, h, mi, sec] = daysMatch;
|
||||
return Number(d) * 86400 + Number(h) * 3600 + Number(mi) * 60 + Number(sec);
|
||||
}
|
||||
const m = s.match(/(\d+):(\d+):(\d+)/);
|
||||
if (!m) return null;
|
||||
const [, h, mi, s] = m;
|
||||
return Number(h) * 3600 + Number(mi) * 60 + Number(s);
|
||||
const [, h, mi, sec] = m;
|
||||
return Number(h) * 3600 + Number(mi) * 60 + Number(sec);
|
||||
}
|
||||
|
||||
function collectRebootLog(rebootLogNode) {
|
||||
|
|
@ -430,10 +475,209 @@ function extractDeviceFwu(node) {
|
|||
|
||||
function extractRssiList(node) {
|
||||
if (!node || typeof node !== 'object') return [];
|
||||
// RSSI_List can be empty (as in our sample) or contain child
|
||||
// <RPN_X> entries. Whatever's here, we surface raw and let a
|
||||
// future parser refine once we've seen a populated example.
|
||||
const keys = Object.keys(node);
|
||||
if (keys.length === 0) return [];
|
||||
return keys.map((k) => ({ key: k, value: node[k] }));
|
||||
const out = [];
|
||||
|
||||
for (const entry of extractKeyedEntries(node, /^RPN_/)) {
|
||||
out.push({ ...parseRssiEntry(entry.key, entry.value), raw: entry.value });
|
||||
}
|
||||
|
||||
for (const entry of extractKeyedEntries(node, /^RSSI_Line_/)) {
|
||||
const parsed = parseRssiLineText(entry.value);
|
||||
if (parsed) out.push({ ...parsed, raw: entry.value });
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RSSI_List child text. Shapes seen in the field:
|
||||
* "MAC:6CAB05A1B2C3; RSSI:-58 dBm" (lab / synthetic)
|
||||
* "RSSI for RPN:4 is -49 [dBm]" (store 933 live capture)
|
||||
*/
|
||||
function parseRssiEntry(key, value) {
|
||||
const rpnMatch = key.match(/^RPN_(\d+)$/i);
|
||||
const rpn = rpnMatch ? rpnMatch[1] : null;
|
||||
const text = String(value || '');
|
||||
const macMatch = text.match(/MAC:\s*([0-9A-Fa-f:]+)/i);
|
||||
const rssiMatch = text.match(/RSSI:\s*(-?\d+)/i)
|
||||
|| text.match(/(-?\d+)\s*dBm/i);
|
||||
return {
|
||||
rpn,
|
||||
mac: macMatch ? normalizeMac(macMatch[1]) : null,
|
||||
rssiDbm: rssiMatch ? Number(rssiMatch[1]) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function parseRssiLineText(text) {
|
||||
const m = String(text || '').match(/RPN:(\d+)\s+is\s+(-?\d+)/i);
|
||||
if (!m) return null;
|
||||
return { rpn: m[1], rssiDbm: Number(m[2]), mac: null };
|
||||
}
|
||||
|
||||
function extractDevicePresence(node) {
|
||||
if (!node || typeof node !== 'object') return [];
|
||||
const rpnEntries = extractKeyedEntries(node, /^RPN_/).map((e) => ({
|
||||
key: e.key,
|
||||
present: /present/i.test(e.value),
|
||||
raw: e.value,
|
||||
}));
|
||||
if (rpnEntries.length > 0) return rpnEntries;
|
||||
|
||||
return extractKeyedEntries(node, /^Device_Line_/).map((e) => {
|
||||
const m = e.value.match(
|
||||
/Device:([^\s]+)\s+(present|absent)\s+at\s+Extension\s+(\d+)/i,
|
||||
);
|
||||
return {
|
||||
key: e.key,
|
||||
deviceType: m?.[1] || null,
|
||||
present: m ? /present/i.test(m[2]) : null,
|
||||
extension: m?.[3] || null,
|
||||
raw: e.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function extractSipIdentityStatus(node) {
|
||||
if (!node || typeof node !== 'object') return [];
|
||||
|
||||
const uaEntries = Object.entries(node)
|
||||
.filter(([k]) => /^SIP_UA_Id_/i.test(k))
|
||||
.sort(([a], [b]) => Number(a.match(/\d+$/)[0]) - Number(b.match(/\d+$/)[0]));
|
||||
|
||||
if (uaEntries.length > 0) {
|
||||
return uaEntries.map(([key, val]) => {
|
||||
if (typeof val !== 'object' || val == null) {
|
||||
return { key, raw: String(val) };
|
||||
}
|
||||
return {
|
||||
key,
|
||||
sipIdx: str(val.SIP_Idx),
|
||||
serverName: str(val.Server_Name),
|
||||
sipUri: str(val.SIP_URI),
|
||||
status: str(val.Status),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return extractKeyedEntries(node, /^Line_/).map((e) => ({
|
||||
key: e.key,
|
||||
status: e.value,
|
||||
raw: e.value,
|
||||
}));
|
||||
}
|
||||
|
||||
function extractDeviceInformation(root) {
|
||||
const di = (typeof root === 'object' && root.Device_Information) || {};
|
||||
if (!di || typeof di !== 'object') return [];
|
||||
|
||||
return Object.entries(di)
|
||||
.filter(([k]) => /^Device_Idx\d+$/i.test(k))
|
||||
.sort(([a], [b]) => Number(a.match(/\d+$/)[0]) - Number(b.match(/\d+$/)[0]))
|
||||
.map(([key, dev]) => parseDeviceIdxEntry(key, dev))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseDeviceIdxEntry(key, dev) {
|
||||
if (!dev || typeof dev !== 'object') return null;
|
||||
const indexMatch = key.match(/(\d+)$/);
|
||||
const index = indexMatch ? Number(indexMatch[1]) : null;
|
||||
const battery = dev.Battery_RSSI_Info || {};
|
||||
const location = dev.Location_Info || {};
|
||||
const dect = dev.DECT_Info || {};
|
||||
const sipAccount = findSipAccountDisplay(dev.SIP_Account);
|
||||
|
||||
const rssiRaw = str(battery.RSSI);
|
||||
const rssiMatch = rssiRaw?.match(/(-?\d+)/);
|
||||
|
||||
const batteryRaw = str(battery.Battery_level);
|
||||
const batteryMatch = batteryRaw?.match(/(\d+)/);
|
||||
|
||||
return {
|
||||
index,
|
||||
deviceType: str(dev.Device_Type),
|
||||
ipei: str(dev.Ipei),
|
||||
firmware: str(dev.SW_Version),
|
||||
displayName: sipAccount?.displayName || null,
|
||||
sipNumber: sipAccount?.number || null,
|
||||
sipState: sipAccount?.state || null,
|
||||
rssiDbm: rssiMatch ? Number(rssiMatch[1]) : null,
|
||||
batteryPercent: batteryMatch ? Number(batteryMatch[1]) : null,
|
||||
registeredRpn: str(location.RPN),
|
||||
lockedRpn: str(location.Locked_RPN),
|
||||
dectState: str(dect.DECT_State),
|
||||
fwuProgress: str(dev.FWU_Progress),
|
||||
};
|
||||
}
|
||||
|
||||
function findSipAccountDisplay(sipAccountNode) {
|
||||
if (!sipAccountNode || typeof sipAccountNode !== 'object') return null;
|
||||
for (const val of Object.values(sipAccountNode)) {
|
||||
if (!val || typeof val !== 'object') continue;
|
||||
const displayName = str(val.Display_Name);
|
||||
const number = str(val.Number);
|
||||
const state = str(val.State);
|
||||
if (displayName || number) {
|
||||
return { displayName, number, state };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractKeyedEntries(node, keyPattern) {
|
||||
if (!node || typeof node !== 'object') return [];
|
||||
return Object.entries(node)
|
||||
.filter(([k]) => keyPattern.test(k))
|
||||
.sort(([a], [b]) => {
|
||||
const na = Number((a.match(/\d+$/) || [0])[0]);
|
||||
const nb = Number((b.match(/\d+$/) || [0])[0]);
|
||||
return na - nb;
|
||||
})
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value: typeof value === 'string' ? value.trim() : String(value ?? ''),
|
||||
}))
|
||||
.filter((e) => e.value);
|
||||
}
|
||||
|
||||
function extractPerRpnStatistics(statsNode) {
|
||||
if (!statsNode || typeof statsNode !== 'object') return [];
|
||||
const header = str(statsNode.Header_Line_Idx) || '';
|
||||
const columns = header.split(',').map((c) => c.trim()).filter(Boolean);
|
||||
|
||||
const lines = Object.entries(statsNode)
|
||||
.filter(([k]) => /^Statistics_Line_\d+$/.test(k) || /^Value_Line_\d+$/.test(k))
|
||||
.sort(([a], [b]) => Number(a.match(/\d+$/)[0]) - Number(b.match(/\d+$/)[0]))
|
||||
.map(([, v]) => (typeof v === 'string' ? v.trim() : ''));
|
||||
|
||||
return lines.filter(Boolean).map((line) => {
|
||||
const parts = parseStatisticsCsvLine(line);
|
||||
const row = { raw: line };
|
||||
if (columns.length === parts.length) {
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const col = columns[i].toLowerCase();
|
||||
if (/^rpn$/i.test(col)) row.rpn = parts[i].replace(/^'|'$/g, '');
|
||||
else if (/mac/i.test(col)) row.mac = normalizeMac(parts[i]);
|
||||
else if (/^op\[s\]/i.test(col)) row.opSeconds = numOrNull(parts[i]);
|
||||
else if (/^dt\[s\]/i.test(col)) row.dtSeconds = numOrNull(parts[i]);
|
||||
else if (/call cnt/i.test(col)) row.callCount = numOrNull(parts[i]);
|
||||
}
|
||||
} else if (parts.length >= 4) {
|
||||
row.rpn = parts[0].replace(/^'|'$/g, '');
|
||||
row.mac = normalizeMac(parts[1]);
|
||||
row.opSeconds = numOrNull(parts[2]);
|
||||
row.dtSeconds = numOrNull(parts[3]);
|
||||
if (parts.length >= 5) row.callCount = numOrNull(parts[4]);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
function parseStatisticsCsvLine(line) {
|
||||
const parts = [];
|
||||
const re = /'([^']*)'|([^,]+)/g;
|
||||
let m;
|
||||
while ((m = re.exec(line)) !== null) {
|
||||
parts.push((m[1] != null ? m[1] : m[2]).trim());
|
||||
}
|
||||
return parts.length > 0 ? parts : line.split(',').map((p) => p.trim());
|
||||
}
|
||||
|
|
|
|||
91
integrations/cisco-dect/statusXmlInventory.js
Normal file
91
integrations/cisco-dect/statusXmlInventory.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// integrations/cisco-dect/statusXmlInventory.js
|
||||
//
|
||||
// Walk a parsed status.xml tree and list every leaf path with a short
|
||||
// preview. Used by collect-raw tooling to spot populated sections
|
||||
// (RSSI_List, Device_Presence, SIP_Identity_Status, etc.) without
|
||||
// hand-reading XML.
|
||||
|
||||
import { xmlToObject } from './statusXml.js';
|
||||
|
||||
const HANDSET_RELATED = /rssi|presence|sip_identity|device_line|statistics|rpn|mac/i;
|
||||
|
||||
/**
|
||||
* @typedef {object} SectionInventoryEntry
|
||||
* @property {string} path dot-separated path from root
|
||||
* @property {boolean} isEmpty true when leaf is absent or whitespace-only
|
||||
* @property {string|null} preview truncated leaf value (null for empty containers)
|
||||
* @property {boolean} handsetRelated heuristic flag for operator summaries
|
||||
*/
|
||||
|
||||
/**
|
||||
* Build a flat inventory of leaf paths from raw status.xml text.
|
||||
*
|
||||
* @param {string} xml
|
||||
* @returns {SectionInventoryEntry[]}
|
||||
*/
|
||||
export function inventoryStatusXml(xml) {
|
||||
if (typeof xml !== 'string' || !xml.trim()) return [];
|
||||
const tree = xmlToObject(xml);
|
||||
const entries = [];
|
||||
walk(tree, '', entries);
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize non-empty handset-related paths for console output.
|
||||
*
|
||||
* @param {SectionInventoryEntry[]} inventory
|
||||
* @returns {{ nonEmpty: number, handsetRelated: number, paths: string[] }}
|
||||
*/
|
||||
export function summarizeInventory(inventory) {
|
||||
const list = Array.isArray(inventory) ? inventory : [];
|
||||
const nonEmpty = list.filter((e) => !e.isEmpty);
|
||||
const handsetPaths = nonEmpty
|
||||
.filter((e) => e.handsetRelated)
|
||||
.map((e) => e.path);
|
||||
return {
|
||||
nonEmpty: nonEmpty.length,
|
||||
handsetRelated: handsetPaths.length,
|
||||
paths: handsetPaths,
|
||||
};
|
||||
}
|
||||
|
||||
function walk(node, prefix, out) {
|
||||
if (node == null) {
|
||||
pushLeaf(prefix, '', out);
|
||||
return;
|
||||
}
|
||||
if (typeof node === 'string') {
|
||||
pushLeaf(prefix, node, out);
|
||||
return;
|
||||
}
|
||||
if (typeof node !== 'object') {
|
||||
pushLeaf(prefix, String(node), out);
|
||||
return;
|
||||
}
|
||||
const keys = Object.keys(node);
|
||||
if (keys.length === 0) {
|
||||
pushLeaf(prefix, '', out);
|
||||
return;
|
||||
}
|
||||
for (const key of keys) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
walk(node[key], path, out);
|
||||
}
|
||||
}
|
||||
|
||||
function pushLeaf(path, value, out) {
|
||||
const str = value == null ? '' : String(value).trim();
|
||||
const isEmpty = str === '';
|
||||
out.push({
|
||||
path,
|
||||
isEmpty,
|
||||
preview: isEmpty ? null : truncate(str, 120),
|
||||
handsetRelated: HANDSET_RELATED.test(path),
|
||||
});
|
||||
}
|
||||
|
||||
function truncate(s, max) {
|
||||
if (s.length <= max) return s;
|
||||
return `${s.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
// the Jira poller in Phase 3) owns presentation.
|
||||
|
||||
import { getDectRelayHub, RelayErrorCodes } from './dectRelayHub.js';
|
||||
import { parseStatusXml, summarizeBaseHealth } from '../integrations/cisco-dect/statusXml.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const LOG_SCOPE = 'dect:collector';
|
||||
|
|
@ -65,6 +66,119 @@ export async function collectAll(bases, opts = {}) {
|
|||
* command's individual "refresh this base" flow — collectAll uses it
|
||||
* internally.
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} BaseCollectRawResult
|
||||
* @property {BaseTarget} base
|
||||
* @property {boolean} ok
|
||||
* @property {string|null} rawXml
|
||||
* @property {number|null} byteLength
|
||||
* @property {object|null} data
|
||||
* @property {object|null} verdict
|
||||
* @property {Array|null} sectionInventory
|
||||
* @property {number|null} elapsedMs
|
||||
* @property {object|null} error
|
||||
*/
|
||||
|
||||
/**
|
||||
* Run `collect-raw` against every base (includes raw XML + inventory).
|
||||
*
|
||||
* @param {BaseTarget[]} bases
|
||||
* @param {object} [opts]
|
||||
* @returns {Promise<BaseCollectRawResult[]>}
|
||||
*/
|
||||
export async function collectRawAll(bases, opts = {}) {
|
||||
const list = Array.isArray(bases) ? bases : [];
|
||||
if (list.length === 0) return [];
|
||||
const hub = opts.hub || getDectRelayHub();
|
||||
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
||||
|
||||
logger(LOG_SCOPE, `Fanning out collect-raw() to ${list.length} base(s)`, 'debug');
|
||||
|
||||
const results = await Promise.all(list.map((base) => collectRawOne(hub, base, timeoutMs)));
|
||||
|
||||
const okCount = results.filter((r) => r.ok).length;
|
||||
logger(LOG_SCOPE, `Collect-raw finished: ${okCount}/${list.length} succeeded`, 'debug');
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function collectRawOne(hub, base, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
if (!base?.ip) {
|
||||
return {
|
||||
base, ok: false, rawXml: null, byteLength: null, data: null, verdict: null,
|
||||
sectionInventory: null, elapsedMs: null,
|
||||
error: { code: 'NO_IP', message: 'base has no IP address' },
|
||||
};
|
||||
}
|
||||
const started = Date.now();
|
||||
try {
|
||||
let result;
|
||||
let elapsedMs;
|
||||
try {
|
||||
({ result, elapsedMs } = await hub.collectRaw(base.ip, { timeoutMs }));
|
||||
} catch (err) {
|
||||
if (err?.code === 'UNKNOWN_COMMAND' || /unknown command type/i.test(err?.message || '')) {
|
||||
({ result, elapsedMs } = await hub.collectRawLegacy(base.ip, { timeoutMs }));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result?.rawXml) {
|
||||
return {
|
||||
base,
|
||||
ok: false,
|
||||
rawXml: null,
|
||||
byteLength: null,
|
||||
data: result?.parsed || null,
|
||||
verdict: result?.verdict || null,
|
||||
sectionInventory: null,
|
||||
elapsedMs: elapsedMs ?? (Date.now() - started),
|
||||
error: {
|
||||
code: 'AGENT_OUTDATED',
|
||||
message: 'Relay agent returned parsed status but not raw XML',
|
||||
hint:
|
||||
'Redeploy dect-relay-agent on the DC host (separate from the bot container). ' +
|
||||
'From the repo root: ./dect-relay-agent/bundle.sh, then install on the DC host ' +
|
||||
'and docker compose restart dect-relay-agent. Agent v0.2.0+ supports includeRaw on collect.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
base,
|
||||
ok: true,
|
||||
rawXml: result.rawXml,
|
||||
byteLength: result.byteLength ?? null,
|
||||
data: result.parsed || null,
|
||||
verdict: result.verdict || null,
|
||||
sectionInventory: result.sectionInventory || null,
|
||||
elapsedMs: elapsedMs ?? (Date.now() - started),
|
||||
error: null,
|
||||
};
|
||||
} catch (err) {
|
||||
const code = err?.code || 'UNKNOWN';
|
||||
const hint = hintFor(code)
|
||||
|| (/unknown command type/i.test(err?.message || '')
|
||||
? 'Redeploy dect-relay-agent on the DC host — the bot container does not run the relay agent.'
|
||||
: null);
|
||||
return {
|
||||
base,
|
||||
ok: false,
|
||||
rawXml: null,
|
||||
byteLength: null,
|
||||
data: null,
|
||||
verdict: null,
|
||||
sectionInventory: null,
|
||||
elapsedMs: Date.now() - started,
|
||||
error: {
|
||||
code,
|
||||
message: err?.message || String(err),
|
||||
hint,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectOne(hub, base, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
||||
if (!base?.ip) {
|
||||
return {
|
||||
|
|
@ -74,12 +188,33 @@ export async function collectOne(hub, base, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|||
}
|
||||
const started = Date.now();
|
||||
try {
|
||||
const { result, elapsedMs } = await hub.collect(base.ip, { timeoutMs });
|
||||
let result;
|
||||
let elapsedMs;
|
||||
try {
|
||||
({ result, elapsedMs } = await hub.collectRaw(base.ip, { timeoutMs }));
|
||||
} catch (err) {
|
||||
if (err?.code === 'UNKNOWN_COMMAND' || /unknown command type/i.test(err?.message || '')) {
|
||||
try {
|
||||
({ result, elapsedMs } = await hub.collectRawLegacy(base.ip, { timeoutMs }));
|
||||
} catch {
|
||||
({ result, elapsedMs } = await hub.collect(base.ip, { timeoutMs }));
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer bot-side parsing so handset/RSSI fields stay current even when
|
||||
// the DC relay agent ships an older statusXml.js.
|
||||
const data = result?.rawXml
|
||||
? parseStatusXml(result.rawXml)
|
||||
: (result?.parsed || result || null);
|
||||
|
||||
return {
|
||||
base,
|
||||
ok: true,
|
||||
data: result?.parsed || result || null,
|
||||
verdict: result?.verdict || null,
|
||||
data,
|
||||
verdict: data ? summarizeBaseHealth(data) : (result?.verdict || null),
|
||||
elapsedMs: elapsedMs ?? (Date.now() - started),
|
||||
error: null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export function discoverDectBases(phoneStatus) {
|
|||
mac,
|
||||
ip,
|
||||
name,
|
||||
webexId: base.id || null,
|
||||
// Track where the IP came from — useful in logs if a base is
|
||||
// reachable via one source but not the other.
|
||||
source: base.meraki?.ip ? 'meraki' : 'webex',
|
||||
|
|
|
|||
|
|
@ -370,6 +370,16 @@ export class DectRelayHub {
|
|||
return this.rpc({ type: 'collect', baseIp }, opts);
|
||||
}
|
||||
|
||||
/** Fetch status.xml including raw XML + section inventory (capture tooling). */
|
||||
collectRaw(baseIp, opts) {
|
||||
return this.rpc({ type: 'collect', baseIp, includeRaw: true }, opts);
|
||||
}
|
||||
|
||||
/** Legacy alias — some agents only expose collect-raw as a distinct type. */
|
||||
collectRawLegacy(baseIp, opts) {
|
||||
return this.rpc({ type: 'collect-raw', baseIp }, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: execute one of the mutating actions the agent
|
||||
* exposes (reboot / force-reboot / reboot-chain / force-reboot-chain
|
||||
|
|
|
|||
70
services/dectStatus/buildHandsetContext.js
Normal file
70
services/dectStatus/buildHandsetContext.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// services/dectStatus/buildHandsetContext.js
|
||||
//
|
||||
// Group Webex DECT handset inventory for /dectstatus rendering.
|
||||
|
||||
/**
|
||||
* @param {object|null} phoneData collectPhoneStatus() output
|
||||
* @returns {object}
|
||||
*/
|
||||
export function buildHandsetContext(phoneData) {
|
||||
const basestations = Array.isArray(phoneData?.dectBasestations)
|
||||
? phoneData.dectBasestations
|
||||
: [];
|
||||
const handsets = Array.isArray(phoneData?.dectHandsets)
|
||||
? phoneData.dectHandsets
|
||||
: [];
|
||||
|
||||
const handsetsByWebexId = new Map();
|
||||
const linesRegisteredByWebexId = new Map();
|
||||
const webexIdByMac = new Map();
|
||||
|
||||
for (const base of basestations) {
|
||||
if (base.id) {
|
||||
handsetsByWebexId.set(base.id, []);
|
||||
linesRegisteredByWebexId.set(base.id, base.linesRegistered ?? null);
|
||||
}
|
||||
const mac = normalizeMacLoose(base.mac);
|
||||
if (base.id && mac) webexIdByMac.set(mac, base.id);
|
||||
}
|
||||
|
||||
const unassignedHandsets = [];
|
||||
|
||||
for (const h of handsets) {
|
||||
const baseId = h.baseStationId || null;
|
||||
if (baseId && handsetsByWebexId.has(baseId)) {
|
||||
handsetsByWebexId.get(baseId).push(h);
|
||||
} else if (!baseId) {
|
||||
unassignedHandsets.push(h);
|
||||
} else {
|
||||
// Assigned to a base id we don't know — treat as unassigned for display.
|
||||
unassignedHandsets.push(h);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handsetsByWebexId,
|
||||
linesRegisteredByWebexId,
|
||||
webexIdByMac,
|
||||
unassignedHandsets,
|
||||
dectNetwork: phoneData?.dectNetwork || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handsets registered to a base (by Webex base id).
|
||||
*
|
||||
* @param {object} handsetCtx buildHandsetContext() output
|
||||
* @param {string|null} webexId
|
||||
* @returns {object[]}
|
||||
*/
|
||||
export function handsetsForBase(handsetCtx, webexId) {
|
||||
if (!webexId || !handsetCtx?.handsetsByWebexId) return [];
|
||||
return handsetCtx.handsetsByWebexId.get(webexId) || [];
|
||||
}
|
||||
|
||||
function normalizeMacLoose(mac) {
|
||||
if (!mac || typeof mac !== 'string') return null;
|
||||
const hex = mac.replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
||||
if (hex.length !== 12) return null;
|
||||
return hex.match(/../g).join(':');
|
||||
}
|
||||
93
services/dectStatus/captureRawXml.js
Normal file
93
services/dectStatus/captureRawXml.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// services/dectStatus/captureRawXml.js
|
||||
//
|
||||
// Fetch raw status.xml from store DECT bases via the on-prem relay.
|
||||
// Used by GET /api/dect/raw-xml/:storeNumber and scripts/fetchDectStatusXml.js.
|
||||
|
||||
import { collectPhoneStatus } from '../phoneService.js';
|
||||
import { discoverDectBases } from '../dectDiscovery.js';
|
||||
import { collectRawAll } from '../dectCollectorService.js';
|
||||
import { getDectRelayHub } from '../dectRelayHub.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
const LOG_SCOPE = 'dect:capture';
|
||||
|
||||
/**
|
||||
* @param {string} storeNum
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.baseFilter] optional IP or MAC filter
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
export async function captureDectRawXml(storeNum, opts = {}) {
|
||||
const store = String(storeNum || '').trim();
|
||||
if (!store || !/^\d{2,4}$/.test(store)) {
|
||||
const err = new Error('storeNum must be a 2–4 digit store number');
|
||||
err.code = 'INVALID_STORE';
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!process.env.DECT_RELAY_AGENT_TOKEN) {
|
||||
const err = new Error('DECT_RELAY_AGENT_TOKEN is not configured on this bot');
|
||||
err.code = 'RELAY_NOT_CONFIGURED';
|
||||
throw err;
|
||||
}
|
||||
|
||||
let relay = null;
|
||||
try {
|
||||
relay = getDectRelayHub().status();
|
||||
} catch (err) {
|
||||
logger(LOG_SCOPE, `Relay status unavailable: ${err.message}`, 'warn');
|
||||
relay = { connected: false };
|
||||
}
|
||||
|
||||
const phoneData = await collectPhoneStatus(store);
|
||||
const { bases, warnings: discoveryWarnings } = discoverDectBases(phoneData || {});
|
||||
|
||||
let targets = bases;
|
||||
const baseFilter = (opts.baseFilter || '').trim();
|
||||
if (baseFilter) {
|
||||
targets = filterBases(bases, baseFilter);
|
||||
if (targets.length === 0) {
|
||||
const err = new Error(`No discovered base matched "${baseFilter}" for store ${store}`);
|
||||
err.code = 'BASE_NOT_FOUND';
|
||||
err.knownBases = bases.map((b) => ({ ip: b.ip, mac: b.mac, name: b.name }));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const results = await collectRawAll(targets);
|
||||
|
||||
return {
|
||||
storeNum: store,
|
||||
relay,
|
||||
discoveryWarnings,
|
||||
bases: results.map((r) => ({
|
||||
mac: r.base?.mac || null,
|
||||
ip: r.base?.ip || null,
|
||||
webexId: r.base?.webexId || null,
|
||||
name: r.base?.name || null,
|
||||
ok: r.ok,
|
||||
byteLength: r.byteLength,
|
||||
rawXml: r.rawXml,
|
||||
sectionInventory: r.sectionInventory,
|
||||
parsed: r.data,
|
||||
verdict: r.verdict,
|
||||
elapsedMs: r.elapsedMs,
|
||||
error: r.error,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function filterBases(bases, raw) {
|
||||
const needle = String(raw).trim().toLowerCase();
|
||||
const needleMac = needle.replace(/[^0-9a-f]/g, '');
|
||||
return bases.filter((b) => {
|
||||
if (b.ip && b.ip.toLowerCase() === needle) return true;
|
||||
if (b.mac) {
|
||||
const macHex = b.mac.replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
||||
if (macHex === needleMac && needleMac.length === 12) return true;
|
||||
if (b.mac.toLowerCase() === needle) return true;
|
||||
}
|
||||
if (b.name && b.name.toLowerCase().includes(needle)) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
50
services/dectStatus/matchHandsetByMac.js
Normal file
50
services/dectStatus/matchHandsetByMac.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// services/dectStatus/matchHandsetByMac.js
|
||||
//
|
||||
// Correlate base-local RSSI / statistics rows with Webex handset records.
|
||||
|
||||
import { normalizeMac } from '../dectDiscovery.js';
|
||||
|
||||
/**
|
||||
* @param {object|null} handset Webex handset record
|
||||
* @param {object} rssiRow parsed RSSI entry from status.xml
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function handsetMatchesRssiRow(handset, rssiRow) {
|
||||
const hMac = normalizeMac(handset?.mac);
|
||||
const rMac = normalizeMac(rssiRow?.mac);
|
||||
return !!(hMac && rMac && hMac === rMac);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Webex handset matching an RSSI row (by MAC).
|
||||
*
|
||||
* @param {object} rssiRow
|
||||
* @param {object[]} handsets
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export function findHandsetForRssiRow(rssiRow, handsets) {
|
||||
const list = Array.isArray(handsets) ? handsets : [];
|
||||
return list.find((h) => handsetMatchesRssiRow(h, rssiRow)) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build display rows merging RSSI with optional Webex handset metadata.
|
||||
*
|
||||
* @param {object[]} rssiRows parsed status.rssi
|
||||
* @param {object[]} handsets Webex handsets for this base
|
||||
* @returns {object[]}
|
||||
*/
|
||||
export function mergeRssiWithHandsets(rssiRows, handsets) {
|
||||
const rows = Array.isArray(rssiRows) ? rssiRows : [];
|
||||
const list = Array.isArray(handsets) ? handsets : [];
|
||||
return rows.map((row) => {
|
||||
const handset = findHandsetForRssiRow(row, list);
|
||||
return {
|
||||
rpn: row.rpn,
|
||||
mac: row.mac,
|
||||
rssiDbm: row.rssiDbm,
|
||||
handsetName: handset?.name || null,
|
||||
extension: handset?.extension || null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -1,15 +1,11 @@
|
|||
// src/services/renderers/dectStatusRenderer.js
|
||||
//
|
||||
// Full CLI-style DECT base dump for `/dectstatus`. Complements the
|
||||
// compact follow-up from renderDectDiagnosticsMarkdown (phonestatus):
|
||||
// that one is "exceptions only"; this one is the complete status.xml
|
||||
// picture (device, firmware, reboot log, network, RTP, security,
|
||||
// emergency numbers, health verdict).
|
||||
//
|
||||
// Input is the same collectAll() result array used by the compact
|
||||
// renderer. Pure — no I/O, no env.
|
||||
// compact follow-up from renderDectDiagnosticsMarkdown (phonestatus).
|
||||
|
||||
import { formatDisplayTime } from '../../utils/time.js';
|
||||
import { formatDisplayTime, simpleTimeAgo } from '../../utils/time.js';
|
||||
import { handsetsForBase } from '../dectStatus/buildHandsetContext.js';
|
||||
import { mergeRssiWithHandsets } from '../dectStatus/matchHandsetByMac.js';
|
||||
|
||||
/**
|
||||
* @param {Array} results collectAll() output
|
||||
|
|
@ -18,6 +14,7 @@ import { formatDisplayTime } from '../../utils/time.js';
|
|||
* @param {boolean} [opts.footer=true]
|
||||
* @param {object} [opts.relay] optional hub.status() snapshot
|
||||
* @param {Array} [opts.discoveryWarnings]
|
||||
* @param {object} [opts.handsetCtx] buildHandsetContext() output
|
||||
* @returns {string}
|
||||
*/
|
||||
export function renderDectStatusMarkdown(results, opts = {}) {
|
||||
|
|
@ -26,11 +23,18 @@ export function renderDectStatusMarkdown(results, opts = {}) {
|
|||
footer = true,
|
||||
relay = null,
|
||||
discoveryWarnings = [],
|
||||
handsetCtx = null,
|
||||
} = opts;
|
||||
const list = Array.isArray(results) ? results : [];
|
||||
|
||||
const lines = [`**DECT Status — Store ${storeNum}**`, ''];
|
||||
|
||||
if (handsetCtx?.dectNetwork) {
|
||||
const net = handsetCtx.dectNetwork;
|
||||
lines.push(`_DECT network: ${net.name || '—'} (assigned handsets: ${net.handsetsCount ?? '—'})_`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (relay) {
|
||||
if (relay.connected) {
|
||||
const agent = relay.agent?.hostname || relay.agent?.version || 'connected';
|
||||
|
|
@ -51,6 +55,7 @@ export function renderDectStatusMarkdown(results, opts = {}) {
|
|||
lines.push(`- ${who}: ${w.reason}`);
|
||||
}
|
||||
}
|
||||
lines.push(...renderUnassignedHandsets(handsetCtx));
|
||||
if (footer) {
|
||||
lines.push('');
|
||||
lines.push(`_Pulled at ${formatDisplayTime()}._`);
|
||||
|
|
@ -60,9 +65,11 @@ export function renderDectStatusMarkdown(results, opts = {}) {
|
|||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (i > 0) lines.push('', '---', '');
|
||||
lines.push(...renderOneBaseFull(list[i]));
|
||||
lines.push(...renderOneBaseFull(list[i], handsetCtx));
|
||||
}
|
||||
|
||||
lines.push(...renderUnassignedHandsets(handsetCtx));
|
||||
|
||||
if (discoveryWarnings.length > 0) {
|
||||
lines.push('', '**Discovery notes (skipped bases):**');
|
||||
for (const w of discoveryWarnings) {
|
||||
|
|
@ -82,10 +89,11 @@ export function renderDectStatusMarkdown(results, opts = {}) {
|
|||
return lines.join('\n').trim();
|
||||
}
|
||||
|
||||
function renderOneBaseFull(r) {
|
||||
function renderOneBaseFull(r, handsetCtx) {
|
||||
const label = r.base?.name || `Basestation ${r.base?.mac || '?'}`;
|
||||
const ip = r.base?.ip || '?';
|
||||
const mac = r.base?.mac || '?';
|
||||
const webexId = r.base?.webexId || null;
|
||||
const lines = [];
|
||||
|
||||
if (!r.ok) {
|
||||
|
|
@ -94,6 +102,7 @@ function renderOneBaseFull(r) {
|
|||
lines.push(`- Collect failed: ${r.error?.message || 'unknown error'}`);
|
||||
if (r.error?.hint) lines.push(`- _${r.error.hint}_`);
|
||||
if (r.elapsedMs != null) lines.push(`- Elapsed: ${r.elapsedMs}ms`);
|
||||
lines.push(...renderHandsetsAndRfSection({}, handsetCtx, webexId));
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
|
@ -101,98 +110,248 @@ function renderOneBaseFull(r) {
|
|||
const verdict = r.verdict || {};
|
||||
const icon = verdict.healthy ? '✅' : '⚠️';
|
||||
lines.push(`${icon} **${label}**`);
|
||||
lines.push(...renderHealthNotes(verdict));
|
||||
|
||||
// ── Device ──
|
||||
lines.push('', '**Device**');
|
||||
row(lines, 'Model', p.device?.model);
|
||||
row(lines, 'System type', p.device?.systemType);
|
||||
row(lines, 'Unit', [p.device?.unitName, p.device?.unitIndex].filter(Boolean).join(' · ') || null);
|
||||
row(lines, 'MAC', p.device?.macAddress || mac);
|
||||
row(lines, 'IP', p.device?.ipAddress || ip);
|
||||
row(lines, 'RFPI', p.device?.rfpiAddress);
|
||||
row(lines, 'RF band', p.device?.rfBand);
|
||||
row(lines, 'Multi-cell', p.multiCell?.role || p.multiCell?.raw);
|
||||
row(lines, 'Base status', p.baseStatus);
|
||||
row(lines, 'Conflict', p.conflictInfo);
|
||||
lines.push(...renderHandsetsAndRfSection(p, handsetCtx, webexId));
|
||||
lines.push(...renderBaseSection(p, { mac, ip, elapsedMs: r.elapsedMs }));
|
||||
lines.push(...renderRebootLogSection(p.rebootLog));
|
||||
lines.push(...renderTrafficSection(p.network, p.rtp));
|
||||
|
||||
// ── Firmware ──
|
||||
lines.push('', '**Firmware**');
|
||||
row(lines, 'Version', p.firmware?.version);
|
||||
row(lines, 'Update server', p.firmware?.updateServer);
|
||||
row(lines, 'Update path', p.firmware?.updatePath);
|
||||
return lines;
|
||||
}
|
||||
|
||||
// ── Time ──
|
||||
lines.push('', '**Time / uptime**');
|
||||
row(lines, 'Local time', p.time?.currentLocalTime);
|
||||
row(lines, 'Uptime', p.time?.operatingTime);
|
||||
if (r.elapsedMs != null) row(lines, 'Collect latency', `${r.elapsedMs}ms`);
|
||||
function renderHealthNotes(verdict) {
|
||||
const warnings = verdict.warnings || [];
|
||||
const info = verdict.info || [];
|
||||
if (warnings.length === 0 && info.length === 0) return [];
|
||||
|
||||
// ── Reboot log ──
|
||||
lines.push('', '**Reboot log** (newest first)');
|
||||
const log = Array.isArray(p.rebootLog) ? p.rebootLog : [];
|
||||
const lines = [];
|
||||
for (const w of warnings) lines.push(`- ⚠️ ${w}`);
|
||||
for (const i of info) lines.push(`- ℹ️ ${i}`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
function renderBaseSection(p, { mac, ip, elapsedMs }) {
|
||||
const lines = ['', '**Base**'];
|
||||
const modelBits = [p.device?.model, p.device?.systemType].filter(Boolean);
|
||||
if (modelBits.length) lines.push(`- ${modelBits.join(' · ')}`);
|
||||
|
||||
const identityBits = [
|
||||
p.device?.macAddress || mac,
|
||||
p.device?.ipAddress || ip,
|
||||
p.device?.rfpiAddress,
|
||||
p.device?.rfBand ? `${p.device.rfBand} band` : null,
|
||||
].filter(Boolean);
|
||||
if (identityBits.length) lines.push(`- ${identityBits.join(' · ')}`);
|
||||
|
||||
const unitBits = [p.device?.unitName, p.device?.unitIndex].filter(Boolean);
|
||||
const statusBits = [
|
||||
formatMultiCell(p.multiCell),
|
||||
p.baseStatus,
|
||||
p.conflictInfo,
|
||||
].filter(Boolean);
|
||||
const opsBits = [...unitBits, ...statusBits].filter(Boolean);
|
||||
if (opsBits.length) lines.push(`- ${opsBits.join(' · ')}`);
|
||||
|
||||
const fw = p.firmware?.version;
|
||||
const uptimeBits = [
|
||||
p.time?.operatingTime ? `uptime ${p.time.operatingTime}` : null,
|
||||
p.time?.currentLocalTime ? `local ${p.time.currentLocalTime}` : null,
|
||||
elapsedMs != null ? `collect ${elapsedMs}ms` : null,
|
||||
].filter(Boolean);
|
||||
const fwLine = [fw, ...uptimeBits].filter(Boolean);
|
||||
if (fwLine.length) lines.push(`- FW ${fwLine.join(' · ')}`);
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function renderRebootLogSection(rebootLog) {
|
||||
const log = Array.isArray(rebootLog) ? rebootLog : [];
|
||||
const lines = ['', '**Reboot log** (newest first)'];
|
||||
if (log.length === 0) {
|
||||
lines.push('- _(none)_');
|
||||
} else {
|
||||
return lines;
|
||||
}
|
||||
|
||||
for (const entry of log) {
|
||||
if (entry.unrecognized) {
|
||||
lines.push(`- ??? ${entry.raw || ''}`);
|
||||
continue;
|
||||
}
|
||||
const tag = entry.reasonCode === 80 ? '⚡' : '•';
|
||||
const tag = (entry.reasonCode === 80 || entry.reasonCode === 43) ? '⚡' : '•';
|
||||
lines.push(
|
||||
`- ${tag} #${entry.sequence} ${entry.at} **${entry.reasonName}** (${entry.reasonCode}) fw=${entry.firmwareAtBoot || '?'}`,
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function renderTrafficSection(net, rtp) {
|
||||
const lines = [];
|
||||
const hasNet = net && Object.values(net).some((v) => v != null);
|
||||
const hasRtp = rtp && Object.values(rtp).some((v) => v != null);
|
||||
if (!hasNet && !hasRtp) return lines;
|
||||
|
||||
lines.push('', '**Network & RTP** (since boot)');
|
||||
if (hasNet) {
|
||||
lines.push(`- **TX:** ${formatNetCounters(net, 'tx')}`);
|
||||
lines.push(`- **RX:** ${formatNetCounters(net, 'rx')}`);
|
||||
}
|
||||
|
||||
// ── Network ──
|
||||
lines.push('', '**Network stats** (since boot)');
|
||||
row(lines, 'Tx packets', p.network?.txPackets);
|
||||
row(lines, 'Tx dropped', p.network?.txDropped);
|
||||
row(lines, 'Tx errors', p.network?.txErrors);
|
||||
row(lines, 'Rx packets', p.network?.rxPackets);
|
||||
row(lines, 'Rx dropped', p.network?.rxDropped);
|
||||
row(lines, 'Rx errors', p.network?.rxErrors);
|
||||
row(lines, 'Rx broadcasts', p.network?.rxBroadcasts);
|
||||
|
||||
// ── RTP ──
|
||||
lines.push('', '**RTP**');
|
||||
row(lines, 'Total since boot', p.rtp?.total);
|
||||
row(lines, 'Current active', p.rtp?.current);
|
||||
row(lines, 'Current local', p.rtp?.currentLocal);
|
||||
row(lines, 'Current relay', p.rtp?.currentRelay);
|
||||
|
||||
// ── Security ──
|
||||
lines.push('', '**Security**');
|
||||
row(
|
||||
lines,
|
||||
'Custom CA',
|
||||
p.security?.customCa?.installed
|
||||
? (p.security.customCa.info || 'installed')
|
||||
: 'Not installed',
|
||||
if (hasRtp) {
|
||||
lines.push(
|
||||
`- **RTP:** ${netVal(rtp.total)} total · ${netVal(rtp.current)} active · ` +
|
||||
`${netVal(rtp.currentLocal)} local · ${netVal(rtp.currentRelay)} relay`,
|
||||
);
|
||||
row(lines, '802.1X protocol', p.security?.dot1x?.protocol);
|
||||
row(lines, '802.1X status', p.security?.dot1x?.transactionStatus);
|
||||
|
||||
// ── Emergency ──
|
||||
const emerg = Array.isArray(p.emergencyNumbers) ? p.emergencyNumbers : [];
|
||||
lines.push('', '**Emergency numbers**');
|
||||
lines.push(emerg.length ? `- ${emerg.join(', ')}` : '- _(none configured)_');
|
||||
|
||||
// ── Verdict ──
|
||||
lines.push('', '**Health verdict**');
|
||||
lines.push(`- healthy: **${verdict.healthy ? 'YES' : 'NO'}**`);
|
||||
for (const w of verdict.warnings || []) {
|
||||
lines.push(`- ⚠️ ${w}`);
|
||||
}
|
||||
for (const i of verdict.info || []) {
|
||||
lines.push(`- ℹ️ ${i}`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
const NET_COUNTER_FIELDS = {
|
||||
tx: [
|
||||
['txPackets', 'pkts'],
|
||||
['txBlocked', 'blocked'],
|
||||
['txDropped', 'dropped'],
|
||||
['txErrors', 'errors'],
|
||||
['txBroadcasts', 'bcast'],
|
||||
],
|
||||
rx: [
|
||||
['rxPackets', 'pkts'],
|
||||
['rxBlocked', 'blocked'],
|
||||
['rxDropped', 'dropped'],
|
||||
['rxErrors', 'errors'],
|
||||
['rxBroadcasts', 'bcast'],
|
||||
],
|
||||
};
|
||||
|
||||
function formatNetCounters(net, direction) {
|
||||
return NET_COUNTER_FIELDS[direction]
|
||||
.map(([key, label]) => `${netVal(net[key])} ${label}`)
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
function netVal(v) {
|
||||
return Number.isFinite(v) ? v : 0;
|
||||
}
|
||||
|
||||
function formatMultiCell(multiCell) {
|
||||
if (!multiCell) return null;
|
||||
const role = multiCell.role ? capitalize(multiCell.role) : null;
|
||||
const state = multiCell.state ? multiCell.state : null;
|
||||
if (role && state) return `multi-cell ${role} (${state})`;
|
||||
if (role) return `multi-cell ${role}`;
|
||||
return multiCell.raw ? `multi-cell ${multiCell.raw}` : null;
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Webex handset inventory + base status.xml handset/RSSI/presence/SIP data.
|
||||
*/
|
||||
function renderHandsetsAndRfSection(parsed, handsetCtx, webexId) {
|
||||
const lines = ['', '**Handsets & RF**'];
|
||||
let hasDetail = false;
|
||||
|
||||
const linesReg = webexId && handsetCtx?.linesRegisteredByWebexId
|
||||
? handsetCtx.linesRegisteredByWebexId.get(webexId)
|
||||
: null;
|
||||
if (linesReg != null) {
|
||||
lines.push(`- Webex lines registered on base: **${linesReg}**`);
|
||||
hasDetail = true;
|
||||
}
|
||||
|
||||
const registered = handsetsForBase(handsetCtx, webexId);
|
||||
const devices = Array.isArray(parsed?.devices) ? parsed.devices : [];
|
||||
const coveredMacs = new Set(
|
||||
devices.map((d) => normalizeMacLoose(d.mac)).filter(Boolean),
|
||||
);
|
||||
|
||||
for (const h of registered) {
|
||||
const idx = h.index != null ? `${h.index}-` : '';
|
||||
const reg = h.lastRegistrationTime
|
||||
? simpleTimeAgo(h.lastRegistrationTime)
|
||||
: '—';
|
||||
const macBit = h.mac && h.mac !== '—' ? ` · ${h.mac}` : '';
|
||||
lines.push(
|
||||
`- **Webex** ${idx}${h.name || 'Handset'} (ext ${h.extension || '—'}) · last reg ${reg}${macBit}`,
|
||||
);
|
||||
hasDetail = true;
|
||||
}
|
||||
|
||||
for (const d of devices) {
|
||||
const who = d.displayName || `Handset ${d.index ?? '?'}`;
|
||||
const rssi = d.rssiDbm != null ? `${d.rssiDbm} dBm` : '?';
|
||||
const bat = d.batteryPercent != null ? ` · battery ${d.batteryPercent}%` : '';
|
||||
const rpn = d.lockedRpn || d.registeredRpn;
|
||||
const rpnBit = rpn ? ` · ${rpn}` : '';
|
||||
const sip = d.sipState ? ` · SIP ${d.sipState}` : '';
|
||||
lines.push(`- **Base** ${who} (${d.deviceType || '?'}) · **${rssi}**${bat}${rpnBit}${sip}`);
|
||||
hasDetail = true;
|
||||
}
|
||||
|
||||
const rssiRows = Array.isArray(parsed?.rssi) ? parsed.rssi : [];
|
||||
const merged = mergeRssiWithHandsets(rssiRows, registered);
|
||||
for (const row of merged) {
|
||||
if (row.mac && coveredMacs.has(normalizeMacLoose(row.mac))) continue;
|
||||
const rssi = row.rssiDbm != null ? `${row.rssiDbm} dBm` : '?';
|
||||
const who = row.handsetName
|
||||
? `${row.handsetName} (ext ${row.extension || '—'})`
|
||||
: (row.mac || `RPN ${row.rpn ?? '?'}`);
|
||||
lines.push(`- **RSSI** ${who}: **${rssi}**`);
|
||||
hasDetail = true;
|
||||
}
|
||||
|
||||
const presence = Array.isArray(parsed?.devicePresence) ? parsed.devicePresence : [];
|
||||
for (const p of presence) {
|
||||
if (p.extension != null) {
|
||||
const state = p.present === true ? 'present' : (p.present === false ? 'absent' : p.raw);
|
||||
lines.push(`- **Presence** ext ${p.extension} (${p.deviceType || '?'}) · ${state}`);
|
||||
hasDetail = true;
|
||||
continue;
|
||||
}
|
||||
if (p.present != null) {
|
||||
lines.push(`- **Presence** ${p.key}: ${p.present ? 'present' : 'absent'}`);
|
||||
hasDetail = true;
|
||||
}
|
||||
}
|
||||
|
||||
const sip = Array.isArray(parsed?.sipIdentityStatus) ? parsed.sipIdentityStatus : [];
|
||||
for (const s of sip) {
|
||||
const status = s.status || s.value || '?';
|
||||
const lineMatch = s.key?.match(/^Line_(\d+)$/i);
|
||||
const who = s.sipIdx != null
|
||||
? `line ${s.sipIdx}`
|
||||
: (lineMatch ? `line ${lineMatch[1]}` : s.key);
|
||||
const server = s.serverName ? ` (${s.serverName})` : '';
|
||||
lines.push(`- **SIP** ${who}${server}: **${status}**`);
|
||||
hasDetail = true;
|
||||
}
|
||||
|
||||
if (!hasDetail && registered.length === 0) {
|
||||
lines.push('- _(no handset or RF data from Webex or base status.xml)_');
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function row(lines, label, value) {
|
||||
if (value == null || value === '') return;
|
||||
lines.push(`- **${label}:** ${value}`);
|
||||
function normalizeMacLoose(mac) {
|
||||
if (!mac) return null;
|
||||
const hex = String(mac).replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
||||
return hex.length === 12 ? hex : null;
|
||||
}
|
||||
|
||||
function renderUnassignedHandsets(handsetCtx) {
|
||||
const unassigned = handsetCtx?.unassignedHandsets || [];
|
||||
if (unassigned.length === 0) return [];
|
||||
|
||||
const lines = ['', '**Unassigned handsets** (Webex)'];
|
||||
for (const h of unassigned) {
|
||||
const idx = h.index != null ? `${h.index}-` : '';
|
||||
const reg = h.lastRegistrationTime
|
||||
? simpleTimeAgo(h.lastRegistrationTime)
|
||||
: '—';
|
||||
lines.push(`- **${idx}${h.name || 'Handset'}** (ext ${h.extension || '—'}) · last reg ${reg}`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,10 +89,20 @@ test('discoverDectBases: happy path — one Meraki-enriched base on 10.x', () =>
|
|||
mac: '6c:ab:05:f6:28:19',
|
||||
ip: '10.4.11.87',
|
||||
name: 'Basestation A',
|
||||
webexId: null,
|
||||
source: 'meraki',
|
||||
});
|
||||
});
|
||||
|
||||
test('discoverDectBases: passes through Webex base id as webexId', () => {
|
||||
const result = discoverDectBases(fixture({
|
||||
dectBasestations: [
|
||||
base({ id: 'webex-base-99', meraki: { ip: '10.4.11.87' } }),
|
||||
],
|
||||
}));
|
||||
assert.equal(result.bases[0].webexId, 'webex-base-99');
|
||||
});
|
||||
|
||||
test('discoverDectBases: prefers Meraki IP over Webex IP', () => {
|
||||
const result = discoverDectBases(fixture({
|
||||
dectBasestations: [
|
||||
|
|
|
|||
34
tests/dectStatus.buildHandsetContext.test.js
Normal file
34
tests/dectStatus.buildHandsetContext.test.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { buildHandsetContext, handsetsForBase } from '../services/dectStatus/buildHandsetContext.js';
|
||||
import { mergeRssiWithHandsets } from '../services/dectStatus/matchHandsetByMac.js';
|
||||
|
||||
test('buildHandsetContext: groups handsets by base and tracks unassigned', () => {
|
||||
const ctx = buildHandsetContext({
|
||||
dectNetwork: { name: 'Store DECT', handsetsCount: 3 },
|
||||
dectBasestations: [
|
||||
{ id: 'base-1', mac: '6c:ab:05:f6:28:19', linesRegistered: 2 },
|
||||
{ id: 'base-2', mac: '6c:ab:05:aa:bb:cc', linesRegistered: 0 },
|
||||
],
|
||||
dectHandsets: [
|
||||
{ id: 'h1', name: 'Handset A', extension: '1001', baseStationId: 'base-1', mac: '6c:ab:05:a1:b2:c3' },
|
||||
{ id: 'h2', name: 'Handset B', extension: '1002', baseStationId: 'base-1', mac: '6c:ab:05:d4:e5:f6' },
|
||||
{ id: 'h3', name: 'Floater', extension: '1003', baseStationId: null, mac: '6c:ab:05:11:22:33' },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(handsetsForBase(ctx, 'base-1').length, 2);
|
||||
assert.equal(ctx.unassignedHandsets.length, 1);
|
||||
assert.equal(ctx.linesRegisteredByWebexId.get('base-1'), 2);
|
||||
});
|
||||
|
||||
test('mergeRssiWithHandsets: joins by MAC', () => {
|
||||
const merged = mergeRssiWithHandsets(
|
||||
[{ rpn: '00', mac: '6c:ab:05:a1:b2:c3', rssiDbm: -58 }],
|
||||
[{ name: 'Handset A', extension: '1001', mac: '6C:AB:05:A1:B2:C3' }],
|
||||
);
|
||||
assert.equal(merged[0].handsetName, 'Handset A');
|
||||
assert.equal(merged[0].extension, '1001');
|
||||
assert.equal(merged[0].rssiDbm, -58);
|
||||
});
|
||||
740
tests/fixtures/dect/status-933-f635de.inventory.json
vendored
Normal file
740
tests/fixtures/dect/status-933-f635de.inventory.json
vendored
Normal file
|
|
@ -0,0 +1,740 @@
|
|||
[
|
||||
{
|
||||
"path": "Status.System_Information.Released_Build",
|
||||
"isEmpty": false,
|
||||
"preview": "Yes",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Multi_Cell",
|
||||
"isEmpty": false,
|
||||
"preview": "Ready(TXT_STATE_KEEP_ALIVE) Primary",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Phone_Type",
|
||||
"isEmpty": false,
|
||||
"preview": "IPDECT-V2 (DBS-210-3PC)",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.System_Type",
|
||||
"isEmpty": false,
|
||||
"preview": "Generic SIP (RFC 3261)",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Unit_Name",
|
||||
"isEmpty": false,
|
||||
"preview": "SME VoIP",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Unit_Index",
|
||||
"isEmpty": false,
|
||||
"preview": "Base Idx:0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RF_Band",
|
||||
"isEmpty": false,
|
||||
"preview": "US",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Conflict_Info",
|
||||
"isEmpty": false,
|
||||
"preview": "No Conflict",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Current_Local_Time",
|
||||
"isEmpty": false,
|
||||
"preview": "27-Jul-2026 15:51:57",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Operating_Time",
|
||||
"isEmpty": false,
|
||||
"preview": "39 Days 04:36:56 (H:M:S)",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RFPI_Address",
|
||||
"isEmpty": false,
|
||||
"preview": "135098C1; RPN:00",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.MAC_Address",
|
||||
"isEmpty": false,
|
||||
"preview": "6cab05f635de",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.IP_Address",
|
||||
"isEmpty": false,
|
||||
"preview": "10.52.38.149",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Product_Configuration",
|
||||
"isEmpty": false,
|
||||
"preview": "0000",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Firmware_Version",
|
||||
"isEmpty": false,
|
||||
"preview": "IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Firmware_URL.Update_Server_Address",
|
||||
"isEmpty": false,
|
||||
"preview": "https://cisco.sipflash.com",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Firmware_URL.Path",
|
||||
"isEmpty": false,
|
||||
"preview": "dms/dbS210",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-06-18 11:09:04 (126) Power Loss (80) Firmware Version 05-01-03-0101-09",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_2",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-05-31 06:44:34 (125) Power Loss (80) Firmware Version 05-01-03-0101-09",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_3",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-05-28 00:49:01 (124) Forced Reboot (81) Firmware Version 05-01-02-0101-05",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_4",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-05-10 07:11:22 (123) Power Loss (80) Firmware Version 05-01-02-0101-05",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_5",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-04-12 09:21:45 (122) Power Loss (80) Firmware Version 05-01-02-0101-05",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_6",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-02-17 00:21:42 (121) Power Loss (80) Firmware Version 05-01-02-0101-05",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Base_Station_Status",
|
||||
"isEmpty": false,
|
||||
"preview": "Idle",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Custom_CA_Status.Custom_CA_Provisioning_Status",
|
||||
"isEmpty": false,
|
||||
"preview": "N/A",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Custom_CA_Status.Custom_CA_Info",
|
||||
"isEmpty": false,
|
||||
"preview": "Not Installed",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Dot1x_Authentication.Transaction_status",
|
||||
"isEmpty": false,
|
||||
"preview": "Unavailable",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Dot1x_Authentication.Protocol",
|
||||
"isEmpty": false,
|
||||
"preview": "N/A",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RSSI_List.RSSI_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "RSSI for RPN:4 is -47 [dBm]",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Total_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "4",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Max_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "-1",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Time_In_Max_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "49710 days 06:28:15 [H:M:S]",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Current_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Current_Local_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Current_Relay_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Remote_Relay_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Current_Recording",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_Presence.Device_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "Device:6825 present at Extension 1",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_Presence.Device_Line_2",
|
||||
"isEmpty": false,
|
||||
"preview": "Device:6825 present at Extension 2",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Base_Station",
|
||||
"isEmpty": false,
|
||||
"preview": "Base type:DBS-210-3PC - Required Version:501 Required Branch:309",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Line_0",
|
||||
"isEmpty": false,
|
||||
"preview": "Device type:6825 - Required Version:501 Required Branch:308 Language Pack:6825_default",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "Device type:6825-RGD - Required Version:501 Required Branch:308 Language Pack:6825-RGD_default",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Line_2",
|
||||
"isEmpty": false,
|
||||
"preview": "Device type:6823 - Required Version:501 Required Branch:308 Language Pack:6823_default",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Line_3",
|
||||
"isEmpty": false,
|
||||
"preview": "Device type:RPT-110-3PC - Required Version:501 Required Branch:303",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Push_To_Talk",
|
||||
"isEmpty": false,
|
||||
"preview": "Off",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_1",
|
||||
"isEmpty": false,
|
||||
"preview": "911",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_2",
|
||||
"isEmpty": false,
|
||||
"preview": "1911",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_3",
|
||||
"isEmpty": false,
|
||||
"preview": "933",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_4",
|
||||
"isEmpty": false,
|
||||
"preview": "No Number set!",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_5",
|
||||
"isEmpty": false,
|
||||
"preview": "No Number set!",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_4.SIP_Idx",
|
||||
"isEmpty": false,
|
||||
"preview": "1",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_4.Server_Id",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_4.SIP_URI",
|
||||
"isEmpty": false,
|
||||
"preview": "jdph354xak_HH_9768C5EF4172_2@31134724.cisco-bcld.com",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_4.Server_Name",
|
||||
"isEmpty": false,
|
||||
"preview": "BroadCloud",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_4.Status",
|
||||
"isEmpty": false,
|
||||
"preview": "OK",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_5.SIP_Idx",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_5.Server_Id",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_5.SIP_URI",
|
||||
"isEmpty": false,
|
||||
"preview": "jdph354xak@31134724.cisco-bcld.com",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_5.Server_Name",
|
||||
"isEmpty": false,
|
||||
"preview": "BroadCloud",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status.SIP_UA_Id_5.Status",
|
||||
"isEmpty": false,
|
||||
"preview": "Registering",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Ipei",
|
||||
"isEmpty": false,
|
||||
"preview": "0329908355",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Device_Type",
|
||||
"isEmpty": false,
|
||||
"preview": "6825",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.SW_Version",
|
||||
"isEmpty": false,
|
||||
"preview": "05-01-03-0101-08",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.FWU_Progress",
|
||||
"isEmpty": false,
|
||||
"preview": "FWU Complete",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Location_Info.Time_Date",
|
||||
"isEmpty": false,
|
||||
"preview": "27-Jul-2026 19:33:27",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Location_Info.RPN",
|
||||
"isEmpty": false,
|
||||
"preview": "Registered@RPN:00",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Location_Info.Locked_RPN",
|
||||
"isEmpty": false,
|
||||
"preview": "Locked@RPN:00",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Battery_RSSI_Info.Time_Date",
|
||||
"isEmpty": false,
|
||||
"preview": "12:33 [mm:ss]",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Battery_RSSI_Info.Battery_level",
|
||||
"isEmpty": false,
|
||||
"preview": "81 [%]",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Battery_RSSI_Info.RSSI",
|
||||
"isEmpty": false,
|
||||
"preview": "-76 [dBm]",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Hs_Config_Update_Time",
|
||||
"isEmpty": false,
|
||||
"preview": "N/A",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Tracking_Info.Tracking_State",
|
||||
"isEmpty": false,
|
||||
"preview": "Tracking Activated",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Tracking_Info.Time_Date",
|
||||
"isEmpty": false,
|
||||
"preview": "27-Jul-2026 19:33:27",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.DECT_Info.DECT_State",
|
||||
"isEmpty": false,
|
||||
"preview": "Present",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.DECT_Info.Call_Instance.MmInstance",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.DECT_Info.Call_Instance.CissInstance",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.DECT_Info.Call_Instance.CcOutInstance",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.DECT_Info.Call_Instance.CcIncInstance",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Push_To_Talk",
|
||||
"isEmpty": false,
|
||||
"preview": "Off",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Reset_Info.Time_Date",
|
||||
"isEmpty": false,
|
||||
"preview": "N/A",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.Reset_Info.Number",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.SIP_Account.SIP_IDX1.Index_of_Relation",
|
||||
"isEmpty": false,
|
||||
"preview": "SIP account with Idx 1 is linked to device Idx 1",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.SIP_Account.SIP_IDX1.Display_Name",
|
||||
"isEmpty": false,
|
||||
"preview": "1-50933",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.SIP_Account.SIP_IDX1.Number",
|
||||
"isEmpty": false,
|
||||
"preview": "jdph354xak",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx1.SIP_Account.SIP_IDX1.State",
|
||||
"isEmpty": false,
|
||||
"preview": "Not in use",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Ipei",
|
||||
"isEmpty": false,
|
||||
"preview": "032990831B",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Device_Type",
|
||||
"isEmpty": false,
|
||||
"preview": "6825",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.SW_Version",
|
||||
"isEmpty": false,
|
||||
"preview": "05-01-03-0101-08",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.FWU_Progress",
|
||||
"isEmpty": false,
|
||||
"preview": "FWU Complete",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Location_Info.Time_Date",
|
||||
"isEmpty": false,
|
||||
"preview": "27-Jul-2026 19:39:02",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Location_Info.RPN",
|
||||
"isEmpty": false,
|
||||
"preview": "Registered@RPN:00",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Location_Info.Locked_RPN",
|
||||
"isEmpty": false,
|
||||
"preview": "Locked@RPN:04",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Battery_RSSI_Info.Time_Date",
|
||||
"isEmpty": false,
|
||||
"preview": "53:38 [mm:ss]",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Battery_RSSI_Info.Battery_level",
|
||||
"isEmpty": false,
|
||||
"preview": "98 [%]",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Battery_RSSI_Info.RSSI",
|
||||
"isEmpty": false,
|
||||
"preview": "-76 [dBm]",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Hs_Config_Update_Time",
|
||||
"isEmpty": false,
|
||||
"preview": "N/A",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Tracking_Info.Tracking_State",
|
||||
"isEmpty": false,
|
||||
"preview": "Tracking Activated",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Tracking_Info.Time_Date",
|
||||
"isEmpty": false,
|
||||
"preview": "27-Jul-2026 19:39:02",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.DECT_Info.DECT_State",
|
||||
"isEmpty": false,
|
||||
"preview": "Present",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.DECT_Info.Call_Instance.MmInstance",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.DECT_Info.Call_Instance.CissInstance",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.DECT_Info.Call_Instance.CcOutInstance",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.DECT_Info.Call_Instance.CcIncInstance",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Push_To_Talk",
|
||||
"isEmpty": false,
|
||||
"preview": "Off",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Reset_Info.Time_Date",
|
||||
"isEmpty": false,
|
||||
"preview": "N/A",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.Reset_Info.Number",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.SIP_Account.SIP_IDX2.Index_of_Relation",
|
||||
"isEmpty": false,
|
||||
"preview": "SIP account with Idx 2 is linked to device Idx 2",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.SIP_Account.SIP_IDX2.Display_Name",
|
||||
"isEmpty": false,
|
||||
"preview": "2-50933",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.SIP_Account.SIP_IDX2.Number",
|
||||
"isEmpty": false,
|
||||
"preview": "jdph354xak_HH_9768C5EF4172_2",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information.Device_Idx2.SIP_Account.SIP_IDX2.State",
|
||||
"isEmpty": false,
|
||||
"preview": "Not in use",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Packets",
|
||||
"isEmpty": false,
|
||||
"preview": "3518964",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Blocked",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Dropped",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Errors",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Broadcasts",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Packets",
|
||||
"isEmpty": false,
|
||||
"preview": "4940939",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Blocked",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Dropped",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Errors",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Broadcasts",
|
||||
"isEmpty": false,
|
||||
"preview": "6239",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Header_Line_Idx",
|
||||
"isEmpty": false,
|
||||
"preview": "RPN, MAC-Addr, OP[s], DT[s], Call Cnt, Call Drop, Call Failed, Duration[s], Active Calls, Max Calls, Busy, Busy Duratio…",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Value_Line_0",
|
||||
"isEmpty": false,
|
||||
"preview": "'00','6CAB05F635DE',3386184,5238180,942,1,8,78353,0,4,0,0,255,0,0,892,0,0,0,35,0,0,285,94,7662,1,1,0,24,0,0,90,109,74,1…",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Value_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "'00','6CAB05F63642',3386126,5238247,160,0,0,12875,0,2,0,0,1,1,8,146,0,0,0,2,0,0,30,10,167,2,2,2,25,0,0,60,118,72,105,52…",
|
||||
"handsetRelated": true
|
||||
}
|
||||
]
|
||||
169
tests/fixtures/dect/status-933-f635de.parsed.json
vendored
Normal file
169
tests/fixtures/dect/status-933-f635de.parsed.json
vendored
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
{
|
||||
"parsed": {
|
||||
"device": {
|
||||
"model": "IPDECT-V2 (DBS-210-3PC)",
|
||||
"systemType": "Generic SIP (RFC 3261)",
|
||||
"unitName": "SME VoIP",
|
||||
"unitIndex": "Base Idx:0",
|
||||
"rfBand": "US",
|
||||
"productConfiguration": "0000",
|
||||
"macAddress": "6c:ab:05:f6:35:de",
|
||||
"ipAddress": "10.52.38.149",
|
||||
"rfpiAddress": "135098C1; RPN:00",
|
||||
"releasedBuild": true
|
||||
},
|
||||
"firmware": {
|
||||
"version": "IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29",
|
||||
"updateServer": "https://cisco.sipflash.com",
|
||||
"updatePath": "dms/dbS210",
|
||||
"requiredFor": {
|
||||
"6823": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "308",
|
||||
"languagePack": "6823_default",
|
||||
"_sourceKey": "Device_Line_2"
|
||||
},
|
||||
"6825": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "308",
|
||||
"languagePack": "6825_default",
|
||||
"_sourceKey": "Device_Line_0"
|
||||
},
|
||||
"DBS-210-3PC": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "309",
|
||||
"languagePack": null,
|
||||
"_sourceKey": "Device_Base_Station"
|
||||
},
|
||||
"6825-RGD": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "308",
|
||||
"languagePack": "6825-RGD_default",
|
||||
"_sourceKey": "Device_Line_1"
|
||||
},
|
||||
"RPT-110-3PC": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "303",
|
||||
"languagePack": null,
|
||||
"_sourceKey": "Device_Line_3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"currentLocalTime": "27-Jul-2026 15:51:57",
|
||||
"operatingTime": "39 Days 04:36:56 (H:M:S)",
|
||||
"operatingTimeSeconds": 16616
|
||||
},
|
||||
"multiCell": {
|
||||
"role": "ready",
|
||||
"raw": "Ready(TXT_STATE_KEEP_ALIVE) Primary"
|
||||
},
|
||||
"baseStatus": "idle",
|
||||
"conflictInfo": "No Conflict",
|
||||
"security": {
|
||||
"customCa": {
|
||||
"provisioningStatus": "N/A",
|
||||
"info": "Not Installed",
|
||||
"installed": false
|
||||
},
|
||||
"dot1x": {
|
||||
"transactionStatus": "Unavailable",
|
||||
"protocol": "N/A"
|
||||
}
|
||||
},
|
||||
"rebootLog": [
|
||||
{
|
||||
"at": "2026-06-18T11:09:04",
|
||||
"sequence": 126,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-03-0101-09",
|
||||
"raw": "2026-06-18 11:09:04 (126) Power Loss (80) Firmware Version 05-01-03-0101-09"
|
||||
},
|
||||
{
|
||||
"at": "2026-05-31T06:44:34",
|
||||
"sequence": 125,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-03-0101-09",
|
||||
"raw": "2026-05-31 06:44:34 (125) Power Loss (80) Firmware Version 05-01-03-0101-09"
|
||||
},
|
||||
{
|
||||
"at": "2026-05-28T00:49:01",
|
||||
"sequence": 124,
|
||||
"reasonName": "Forced Reboot",
|
||||
"reasonCode": 81,
|
||||
"firmwareAtBoot": "05-01-02-0101-05",
|
||||
"raw": "2026-05-28 00:49:01 (124) Forced Reboot (81) Firmware Version 05-01-02-0101-05"
|
||||
},
|
||||
{
|
||||
"at": "2026-05-10T07:11:22",
|
||||
"sequence": 123,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-02-0101-05",
|
||||
"raw": "2026-05-10 07:11:22 (123) Power Loss (80) Firmware Version 05-01-02-0101-05"
|
||||
},
|
||||
{
|
||||
"at": "2026-04-12T09:21:45",
|
||||
"sequence": 122,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-02-0101-05",
|
||||
"raw": "2026-04-12 09:21:45 (122) Power Loss (80) Firmware Version 05-01-02-0101-05"
|
||||
},
|
||||
{
|
||||
"at": "2026-02-17T00:21:42",
|
||||
"sequence": 121,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-02-0101-05",
|
||||
"raw": "2026-02-17 00:21:42 (121) Power Loss (80) Firmware Version 05-01-02-0101-05"
|
||||
}
|
||||
],
|
||||
"rtp": {
|
||||
"total": 4,
|
||||
"max": -1,
|
||||
"current": 0,
|
||||
"currentLocal": 0,
|
||||
"currentRelay": 0,
|
||||
"remoteRelay": 0,
|
||||
"currentRecording": 0,
|
||||
"timeInMaxRtp": "49710 days 06:28:15 [H:M:S]"
|
||||
},
|
||||
"network": {
|
||||
"txPackets": 3518964,
|
||||
"txBlocked": 0,
|
||||
"txDropped": 0,
|
||||
"txErrors": 0,
|
||||
"txBroadcasts": 0,
|
||||
"rxPackets": 4940939,
|
||||
"rxBlocked": 0,
|
||||
"rxDropped": 0,
|
||||
"rxErrors": 0,
|
||||
"rxBroadcasts": 6239
|
||||
},
|
||||
"emergencyNumbers": [
|
||||
"911",
|
||||
"1911",
|
||||
"933"
|
||||
],
|
||||
"rssi": [],
|
||||
"devicePresence": [],
|
||||
"sipIdentityStatus": [],
|
||||
"perRpnStats": [],
|
||||
"features": {
|
||||
"pushToTalk": false
|
||||
},
|
||||
"statisticsHeader": "RPN, MAC-Addr, OP[s], DT[s], Call Cnt, Call Drop, Call Failed, Duration[s], Active Calls, Max Calls, Busy, Busy Duration[s], Min Latency[ms], Avg Latency[ms], Max Latency[ms], G711U, G711A, G729, G722, G726, OPUS, BV32, Handovers, Failed Handovers, SIP reg failed, Handset Removed, Searching, RcHeapFree Running, New SyncSource, LAN Sync Lost, LAN Primary Lost, Freq[0][0], Freq[0][1], Freq[0][2], Freq[0][3], Freq[0][4], Freq[0][5], Freq[0][6], Freq[0][7], Freq[0][8], Freq[0][9], Freq[0][10], Freq[0][11], Freq[1][0], Freq[1][1], Freq[1][2], Freq[1][3], Freq[1][4], Freq[1][5], Freq[1][6], Freq[1][7], Freq[1][8], Freq[1][9], Freq[1][10], Freq[1][11], Freq[2][0], Freq[2][1], Freq[2][2], Freq[2][3], Freq[2][4], Freq[2][5], Freq[2][6], Freq[2][7], Freq[2][8], Freq[2][9], Freq[2][10], Freq[2][11], Freq[3][0], Freq[3][1], Freq[3][2], Freq[3][3], Freq[3][4], Freq[3][5], Freq[3][6], Freq[3][7], Freq[3][8], Freq[3][9], Freq[3][10], Freq[3][11], Freq[4][0], Freq[4][1], Freq[4][2], Freq[4][3], Freq[4][4], Freq[4][5], Freq[4][6], Freq[4][7], Freq[4][8], Freq[4][9], Freq[4][10], Freq[4][11], Freq[5][0], Freq[5][1], Freq[5][2], Freq[5][3], Freq[5][4], Freq[5][5], Freq[5][6], Freq[5][7], Freq[5][8], Freq[5][9], Freq[5][10], Freq[5][11], Freq[6][0], Freq[6][1], Freq[6][2], Freq[6][3], Freq[6][4], Freq[6][5], Freq[6][6], Freq[6][7], Freq[6][8], Freq[6][9], Freq[6][10], Freq[6][11], Freq[7][0], Freq[7][1], Freq[7][2], Freq[7][3], Freq[7][4], Freq[7][5], Freq[7][6], Freq[7][7], Freq[7][8], Freq[7][9], Freq[7][10], Freq[7][11], Freq[8][0], Freq[8][1], Freq[8][2], Freq[8][3], Freq[8][4], Freq[8][5], Freq[8][6], Freq[8][7], Freq[8][8], Freq[8][9], Freq[8][10], Freq[8][11], Freq[9][0], Freq[9][1], Freq[9][2], Freq[9][3], Freq[9][4], Freq[9][5], Freq[9][6], Freq[9][7], Freq[9][8], Freq[9][9], Freq[9][10], Freq[9][11], R-Idx[0], R-OP[0][s], R-Busy[0], R-Busy Duration[0][s], Max Calls[0], Searching[0], Recovery[0], New SyncSource[0], Wide Band[0], Narrow Band[0], R-Idx[1], R-OP[1][s], R-Busy[1], R-Busy Duration[1][s], Max Calls[1], Searching[1], Recovery[1], New SyncSource[1], Wide Band[1], Narrow Band[1], R-Idx[2], R-OP[2][s], R-Busy[2], R-Busy Duration[2][s], Max Calls[2], Searching[2], Recovery[2], New SyncSource[2], Wide Band[2], Narrow Band[2]",
|
||||
"_rawStatisticsHeader": "RPN, MAC-Addr, OP[s], DT[s], Call Cnt, Call Drop, Call Failed, Duration[s], Active Calls, Max Calls, Busy, Busy Duration[s], Min Latency[ms], Avg Latency[ms], Max Latency[ms], G711U, G711A, G729, G722, G726, OPUS, BV32, Handovers, Failed Handovers, SIP reg failed, Handset Removed, Searching, RcHeapFree Running, New SyncSource, LAN Sync Lost, LAN Primary Lost, Freq[0][0], Freq[0][1], Freq[0][2], Freq[0][3], Freq[0][4], Freq[0][5], Freq[0][6], Freq[0][7], Freq[0][8], Freq[0][9], Freq[0][10], Freq[0][11], Freq[1][0], Freq[1][1], Freq[1][2], Freq[1][3], Freq[1][4], Freq[1][5], Freq[1][6], Freq[1][7], Freq[1][8], Freq[1][9], Freq[1][10], Freq[1][11], Freq[2][0], Freq[2][1], Freq[2][2], Freq[2][3], Freq[2][4], Freq[2][5], Freq[2][6], Freq[2][7], Freq[2][8], Freq[2][9], Freq[2][10], Freq[2][11], Freq[3][0], Freq[3][1], Freq[3][2], Freq[3][3], Freq[3][4], Freq[3][5], Freq[3][6], Freq[3][7], Freq[3][8], Freq[3][9], Freq[3][10], Freq[3][11], Freq[4][0], Freq[4][1], Freq[4][2], Freq[4][3], Freq[4][4], Freq[4][5], Freq[4][6], Freq[4][7], Freq[4][8], Freq[4][9], Freq[4][10], Freq[4][11], Freq[5][0], Freq[5][1], Freq[5][2], Freq[5][3], Freq[5][4], Freq[5][5], Freq[5][6], Freq[5][7], Freq[5][8], Freq[5][9], Freq[5][10], Freq[5][11], Freq[6][0], Freq[6][1], Freq[6][2], Freq[6][3], Freq[6][4], Freq[6][5], Freq[6][6], Freq[6][7], Freq[6][8], Freq[6][9], Freq[6][10], Freq[6][11], Freq[7][0], Freq[7][1], Freq[7][2], Freq[7][3], Freq[7][4], Freq[7][5], Freq[7][6], Freq[7][7], Freq[7][8], Freq[7][9], Freq[7][10], Freq[7][11], Freq[8][0], Freq[8][1], Freq[8][2], Freq[8][3], Freq[8][4], Freq[8][5], Freq[8][6], Freq[8][7], Freq[8][8], Freq[8][9], Freq[8][10], Freq[8][11], Freq[9][0], Freq[9][1], Freq[9][2], Freq[9][3], Freq[9][4], Freq[9][5], Freq[9][6], Freq[9][7], Freq[9][8], Freq[9][9], Freq[9][10], Freq[9][11], R-Idx[0], R-OP[0][s], R-Busy[0], R-Busy Duration[0][s], Max Calls[0], Searching[0], Recovery[0], New SyncSource[0], Wide Band[0], Narrow Band[0], R-Idx[1], R-OP[1][s], R-Busy[1], R-Busy Duration[1][s], Max Calls[1], Searching[1], Recovery[1], New SyncSource[1], Wide Band[1], Narrow Band[1], R-Idx[2], R-OP[2][s], R-Busy[2], R-Busy Duration[2][s], Max Calls[2], Searching[2], Recovery[2], New SyncSource[2], Wide Band[2], Narrow Band[2]"
|
||||
},
|
||||
"verdict": {
|
||||
"healthy": false,
|
||||
"warnings": [
|
||||
"5 recent power-loss reboot(s); most recent at 2026-06-18T11:09:04"
|
||||
],
|
||||
"info": []
|
||||
},
|
||||
"fetchedAt": "2026-07-27T19:51:57.077Z"
|
||||
}
|
||||
190
tests/fixtures/dect/status-933-f635de.xml
vendored
Normal file
190
tests/fixtures/dect/status-933-f635de.xml
vendored
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Status>
|
||||
<System_Information>
|
||||
<Released_Build>Yes</Released_Build>
|
||||
<Multi_Cell>Ready(TXT_STATE_KEEP_ALIVE) Primary</Multi_Cell>
|
||||
<Phone_Type>IPDECT-V2 (DBS-210-3PC)</Phone_Type>
|
||||
<System_Type>Generic SIP (RFC 3261)</System_Type>
|
||||
<Unit_Name>SME VoIP</Unit_Name>
|
||||
<Unit_Index>Base Idx:0</Unit_Index>
|
||||
<RF_Band>US</RF_Band>
|
||||
<Conflict_Info>No Conflict</Conflict_Info>
|
||||
<Current_Local_Time>27-Jul-2026 15:51:57</Current_Local_Time>
|
||||
<Operating_Time>39 Days 04:36:56 (H:M:S)</Operating_Time>
|
||||
<RFPI_Address>135098C1; RPN:00</RFPI_Address>
|
||||
<MAC_Address>6cab05f635de</MAC_Address>
|
||||
<IP_Address>10.52.38.149</IP_Address>
|
||||
<Product_Configuration>0000</Product_Configuration>
|
||||
<Firmware_Version>IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29</Firmware_Version>
|
||||
<Firmware_URL>
|
||||
<Update_Server_Address>https://cisco.sipflash.com</Update_Server_Address>
|
||||
<Path>dms/dbS210</Path>
|
||||
</Firmware_URL>
|
||||
<Reboot_Log>
|
||||
<Reboot_Line_1>2026-06-18 11:09:04 (126) Power Loss (80) Firmware Version 05-01-03-0101-09</Reboot_Line_1>
|
||||
<Reboot_Line_2>2026-05-31 06:44:34 (125) Power Loss (80) Firmware Version 05-01-03-0101-09</Reboot_Line_2>
|
||||
<Reboot_Line_3>2026-05-28 00:49:01 (124) Forced Reboot (81) Firmware Version 05-01-02-0101-05</Reboot_Line_3>
|
||||
<Reboot_Line_4>2026-05-10 07:11:22 (123) Power Loss (80) Firmware Version 05-01-02-0101-05</Reboot_Line_4>
|
||||
<Reboot_Line_5>2026-04-12 09:21:45 (122) Power Loss (80) Firmware Version 05-01-02-0101-05</Reboot_Line_5>
|
||||
<Reboot_Line_6>2026-02-17 00:21:42 (121) Power Loss (80) Firmware Version 05-01-02-0101-05</Reboot_Line_6>
|
||||
</Reboot_Log>
|
||||
<Base_Station_Status>Idle</Base_Station_Status>
|
||||
<Custom_CA_Status>
|
||||
<Custom_CA_Provisioning_Status>N/A</Custom_CA_Provisioning_Status>
|
||||
<Custom_CA_Info>Not Installed</Custom_CA_Info>
|
||||
</Custom_CA_Status>
|
||||
<Dot1x_Authentication>
|
||||
<Transaction_status>Unavailable</Transaction_status>
|
||||
<Protocol>N/A</Protocol>
|
||||
</Dot1x_Authentication>
|
||||
<RSSI_List>
|
||||
<RSSI_Line_1>RSSI for RPN:4 is -47 [dBm]</RSSI_Line_1>
|
||||
</RSSI_List>
|
||||
<RTP_Usage>
|
||||
<Total_RTP>4</Total_RTP>
|
||||
<Max_RTP>-1</Max_RTP>
|
||||
<Time_In_Max_RTP>49710 days 06:28:15 [H:M:S]</Time_In_Max_RTP>
|
||||
<Current_RTP>0</Current_RTP>
|
||||
<Current_Local_RTP>0</Current_Local_RTP>
|
||||
<Current_Relay_RTP>0</Current_Relay_RTP>
|
||||
<Remote_Relay_RTP>0</Remote_Relay_RTP>
|
||||
<Current_Recording>0</Current_Recording>
|
||||
</RTP_Usage>
|
||||
<Device_Presence>
|
||||
<Device_Line_1>Device:6825 present at Extension 1</Device_Line_1>
|
||||
<Device_Line_2>Device:6825 present at Extension 2</Device_Line_2>
|
||||
</Device_Presence>
|
||||
<Device_FWU_Info>
|
||||
<Device_Base_Station>Base type:DBS-210-3PC - Required Version:501 Required Branch:309</Device_Base_Station>
|
||||
<Device_Line_0>Device type:6825 - Required Version:501 Required Branch:308 Language Pack:6825_default</Device_Line_0>
|
||||
<Device_Line_1>Device type:6825-RGD - Required Version:501 Required Branch:308 Language Pack:6825-RGD_default</Device_Line_1>
|
||||
<Device_Line_2>Device type:6823 - Required Version:501 Required Branch:308 Language Pack:6823_default</Device_Line_2>
|
||||
<Device_Line_3>Device type:RPT-110-3PC - Required Version:501 Required Branch:303</Device_Line_3>
|
||||
</Device_FWU_Info>
|
||||
<Push_To_Talk>Off</Push_To_Talk>
|
||||
<Emergency_Calls>
|
||||
<Emergency_Number_1>911</Emergency_Number_1>
|
||||
<Emergency_Number_2>1911</Emergency_Number_2>
|
||||
<Emergency_Number_3>933</Emergency_Number_3>
|
||||
<Emergency_Number_4>No Number set!</Emergency_Number_4>
|
||||
<Emergency_Number_5>No Number set!</Emergency_Number_5>
|
||||
</Emergency_Calls>
|
||||
<SIP_Identity_Status>
|
||||
<SIP_UA_Id_4>
|
||||
<SIP_Idx>1</SIP_Idx>
|
||||
<Server_Id>0</Server_Id>
|
||||
<SIP_URI>jdph354xak_HH_9768C5EF4172_2@31134724.cisco-bcld.com</SIP_URI>
|
||||
<Server_Name>BroadCloud</Server_Name>
|
||||
<Status>OK</Status>
|
||||
</SIP_UA_Id_4>
|
||||
<SIP_UA_Id_5>
|
||||
<SIP_Idx>0</SIP_Idx>
|
||||
<Server_Id>0</Server_Id>
|
||||
<SIP_URI>jdph354xak@31134724.cisco-bcld.com</SIP_URI>
|
||||
<Server_Name>BroadCloud</Server_Name>
|
||||
<Status>Registering</Status>
|
||||
</SIP_UA_Id_5>
|
||||
</SIP_Identity_Status>
|
||||
</System_Information>
|
||||
<Device_Information>
|
||||
<Device_Idx1>
|
||||
<Ipei>0329908355</Ipei>
|
||||
<Device_Type>6825</Device_Type>
|
||||
<SW_Version>05-01-03-0101-08</SW_Version>
|
||||
<FWU_Progress>FWU Complete</FWU_Progress>
|
||||
<Location_Info>
|
||||
<Time_Date>27-Jul-2026 19:33:27</Time_Date>
|
||||
<RPN>Registered@RPN:00</RPN>
|
||||
<Locked_RPN>Locked@RPN:00</Locked_RPN>
|
||||
</Location_Info>
|
||||
<Battery_RSSI_Info>
|
||||
<Time_Date>12:33 [mm:ss]</Time_Date>
|
||||
<Battery_level>81 [%]</Battery_level>
|
||||
<RSSI>-76 [dBm]</RSSI>
|
||||
</Battery_RSSI_Info>
|
||||
<Hs_Config_Update_Time>N/A</Hs_Config_Update_Time><Tracking_Info>
|
||||
<Tracking_State>Tracking Activated</Tracking_State>
|
||||
<Time_Date>27-Jul-2026 19:33:27</Time_Date>
|
||||
</Tracking_Info>
|
||||
<DECT_Info>
|
||||
<DECT_State>Present</DECT_State>
|
||||
<Call_Instance><MmInstance>0</MmInstance>
|
||||
<CissInstance>0</CissInstance>
|
||||
<CcOutInstance>0</CcOutInstance>
|
||||
<CcIncInstance>0</CcIncInstance>
|
||||
</Call_Instance>
|
||||
</DECT_Info>
|
||||
<Push_To_Talk>Off</Push_To_Talk>
|
||||
<Reset_Info>
|
||||
<Time_Date>N/A</Time_Date>
|
||||
<Number>0</Number>
|
||||
</Reset_Info>
|
||||
<SIP_Account>
|
||||
<SIP_IDX1>
|
||||
<Index_of_Relation>SIP account with Idx 1 is linked to device Idx 1</Index_of_Relation>
|
||||
<Display_Name>1-50933</Display_Name>
|
||||
<Number>jdph354xak</Number>
|
||||
<State>Not in use</State>
|
||||
</SIP_IDX1>
|
||||
</SIP_Account>
|
||||
</Device_Idx1>
|
||||
<Device_Idx2>
|
||||
<Ipei>032990831B</Ipei>
|
||||
<Device_Type>6825</Device_Type>
|
||||
<SW_Version>05-01-03-0101-08</SW_Version>
|
||||
<FWU_Progress>FWU Complete</FWU_Progress>
|
||||
<Location_Info>
|
||||
<Time_Date>27-Jul-2026 19:39:02</Time_Date>
|
||||
<RPN>Registered@RPN:00</RPN>
|
||||
<Locked_RPN>Locked@RPN:04</Locked_RPN>
|
||||
</Location_Info>
|
||||
<Battery_RSSI_Info>
|
||||
<Time_Date>53:38 [mm:ss]</Time_Date>
|
||||
<Battery_level>98 [%]</Battery_level>
|
||||
<RSSI>-76 [dBm]</RSSI>
|
||||
</Battery_RSSI_Info>
|
||||
<Hs_Config_Update_Time>N/A</Hs_Config_Update_Time><Tracking_Info>
|
||||
<Tracking_State>Tracking Activated</Tracking_State>
|
||||
<Time_Date>27-Jul-2026 19:39:02</Time_Date>
|
||||
</Tracking_Info>
|
||||
<DECT_Info>
|
||||
<DECT_State>Present</DECT_State>
|
||||
<Call_Instance><MmInstance>0</MmInstance>
|
||||
<CissInstance>0</CissInstance>
|
||||
<CcOutInstance>0</CcOutInstance>
|
||||
<CcIncInstance>0</CcIncInstance>
|
||||
</Call_Instance>
|
||||
</DECT_Info>
|
||||
<Push_To_Talk>Off</Push_To_Talk>
|
||||
<Reset_Info>
|
||||
<Time_Date>N/A</Time_Date>
|
||||
<Number>0</Number>
|
||||
</Reset_Info>
|
||||
<SIP_Account>
|
||||
<SIP_IDX2>
|
||||
<Index_of_Relation>SIP account with Idx 2 is linked to device Idx 2</Index_of_Relation>
|
||||
<Display_Name>2-50933</Display_Name>
|
||||
<Number>jdph354xak_HH_9768C5EF4172_2</Number>
|
||||
<State>Not in use</State>
|
||||
</SIP_IDX2>
|
||||
</SIP_Account>
|
||||
</Device_Idx2>
|
||||
</Device_Information>
|
||||
<Statistics>
|
||||
<Network_Statistics>
|
||||
<Tx_Packets>3518964</Tx_Packets>
|
||||
<Tx_Blocked>0</Tx_Blocked>
|
||||
<Tx_Dropped>0</Tx_Dropped>
|
||||
<Tx_Errors>0</Tx_Errors>
|
||||
<Tx_Broadcasts>0</Tx_Broadcasts>
|
||||
<Rx_Packets>4940939</Rx_Packets>
|
||||
<Rx_Blocked>0</Rx_Blocked>
|
||||
<Rx_Dropped>0</Rx_Dropped>
|
||||
<Rx_Errors>0</Rx_Errors>
|
||||
<Rx_Broadcasts>6239</Rx_Broadcasts>
|
||||
</Network_Statistics>
|
||||
<Header_Line_Idx>RPN, MAC-Addr, OP[s], DT[s], Call Cnt, Call Drop, Call Failed, Duration[s], Active Calls, Max Calls, Busy, Busy Duration[s], Min Latency[ms], Avg Latency[ms], Max Latency[ms], G711U, G711A, G729, G722, G726, OPUS, BV32, Handovers, Failed Handovers, SIP reg failed, Handset Removed, Searching, RcHeapFree Running, New SyncSource, LAN Sync Lost, LAN Primary Lost, Freq[0][0], Freq[0][1], Freq[0][2], Freq[0][3], Freq[0][4], Freq[0][5], Freq[0][6], Freq[0][7], Freq[0][8], Freq[0][9], Freq[0][10], Freq[0][11], Freq[1][0], Freq[1][1], Freq[1][2], Freq[1][3], Freq[1][4], Freq[1][5], Freq[1][6], Freq[1][7], Freq[1][8], Freq[1][9], Freq[1][10], Freq[1][11], Freq[2][0], Freq[2][1], Freq[2][2], Freq[2][3], Freq[2][4], Freq[2][5], Freq[2][6], Freq[2][7], Freq[2][8], Freq[2][9], Freq[2][10], Freq[2][11], Freq[3][0], Freq[3][1], Freq[3][2], Freq[3][3], Freq[3][4], Freq[3][5], Freq[3][6], Freq[3][7], Freq[3][8], Freq[3][9], Freq[3][10], Freq[3][11], Freq[4][0], Freq[4][1], Freq[4][2], Freq[4][3], Freq[4][4], Freq[4][5], Freq[4][6], Freq[4][7], Freq[4][8], Freq[4][9], Freq[4][10], Freq[4][11], Freq[5][0], Freq[5][1], Freq[5][2], Freq[5][3], Freq[5][4], Freq[5][5], Freq[5][6], Freq[5][7], Freq[5][8], Freq[5][9], Freq[5][10], Freq[5][11], Freq[6][0], Freq[6][1], Freq[6][2], Freq[6][3], Freq[6][4], Freq[6][5], Freq[6][6], Freq[6][7], Freq[6][8], Freq[6][9], Freq[6][10], Freq[6][11], Freq[7][0], Freq[7][1], Freq[7][2], Freq[7][3], Freq[7][4], Freq[7][5], Freq[7][6], Freq[7][7], Freq[7][8], Freq[7][9], Freq[7][10], Freq[7][11], Freq[8][0], Freq[8][1], Freq[8][2], Freq[8][3], Freq[8][4], Freq[8][5], Freq[8][6], Freq[8][7], Freq[8][8], Freq[8][9], Freq[8][10], Freq[8][11], Freq[9][0], Freq[9][1], Freq[9][2], Freq[9][3], Freq[9][4], Freq[9][5], Freq[9][6], Freq[9][7], Freq[9][8], Freq[9][9], Freq[9][10], Freq[9][11], R-Idx[0], R-OP[0][s], R-Busy[0], R-Busy Duration[0][s], Max Calls[0], Searching[0], Recovery[0], New SyncSource[0], Wide Band[0], Narrow Band[0], R-Idx[1], R-OP[1][s], R-Busy[1], R-Busy Duration[1][s], Max Calls[1], Searching[1], Recovery[1], New SyncSource[1], Wide Band[1], Narrow Band[1], R-Idx[2], R-OP[2][s], R-Busy[2], R-Busy Duration[2][s], Max Calls[2], Searching[2], Recovery[2], New SyncSource[2], Wide Band[2], Narrow Band[2]</Header_Line_Idx>
|
||||
<Value_Line_0>'00','6CAB05F635DE',3386184,5238180,942,1,8,78353,0,4,0,0,255,0,0,892,0,0,0,35,0,0,285,94,7662,1,1,0,24,0,0,90,109,74,117,75,107,69,112,98,108,80,112,99,142,107,130,90,116,100,119,125,129,107,143,104,136,118,130,106,106,95,141,103,159,97,147,102,139,123,146,123,140,95,166,125,139,127,154,116,164,136,162,113,127,105,164,128,155,128,151,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0</Value_Line_0>
|
||||
<Value_Line_1>'00','6CAB05F63642',3386126,5238247,160,0,0,12875,0,2,0,0,1,1,8,146,0,0,0,2,0,0,30,10,167,2,2,2,25,0,0,60,118,72,105,52,118,48,144,57,127,63,122,109,126,88,129,134,129,140,109,127,128,128,142,132,134,85,133,114,117,106,126,97,117,131,133,142,134,82,141,129,137,144,141,124,141,117,122,98,145,78,141,120,146,98,114,90,134,88,123,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0</Value_Line_1>
|
||||
</Statistics>
|
||||
</Status>
|
||||
386
tests/fixtures/dect/status-933-f63642.inventory.json
vendored
Normal file
386
tests/fixtures/dect/status-933-f63642.inventory.json
vendored
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
[
|
||||
{
|
||||
"path": "Status.System_Information.Released_Build",
|
||||
"isEmpty": false,
|
||||
"preview": "Yes",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Multi_Cell",
|
||||
"isEmpty": false,
|
||||
"preview": "Ready(TXT_STATE_KEEP_ALIVE) Secondary",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Phone_Type",
|
||||
"isEmpty": false,
|
||||
"preview": "IPDECT-V2 (DBS-210-3PC)",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.System_Type",
|
||||
"isEmpty": false,
|
||||
"preview": "Generic SIP (RFC 3261)",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Unit_Name",
|
||||
"isEmpty": false,
|
||||
"preview": "SME VoIP",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Unit_Index",
|
||||
"isEmpty": false,
|
||||
"preview": "Base Idx:1",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RF_Band",
|
||||
"isEmpty": false,
|
||||
"preview": "US",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Conflict_Info",
|
||||
"isEmpty": false,
|
||||
"preview": "No Conflict",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Current_Local_Time",
|
||||
"isEmpty": false,
|
||||
"preview": "27-Jul-2026 15:51:57",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Operating_Time",
|
||||
"isEmpty": false,
|
||||
"preview": "39 Days 04:36:13 (H:M:S)",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RFPI_Address",
|
||||
"isEmpty": false,
|
||||
"preview": "135098C1; RPN:04",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.MAC_Address",
|
||||
"isEmpty": false,
|
||||
"preview": "6cab05f63642",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.IP_Address",
|
||||
"isEmpty": false,
|
||||
"preview": "10.52.38.152",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Product_Configuration",
|
||||
"isEmpty": false,
|
||||
"preview": "0000",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Firmware_Version",
|
||||
"isEmpty": false,
|
||||
"preview": "IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Firmware_URL.Update_Server_Address",
|
||||
"isEmpty": false,
|
||||
"preview": "https://cisco.sipflash.com",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Firmware_URL.Path",
|
||||
"isEmpty": false,
|
||||
"preview": "dms/dbS210",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-06-18 11:09:04 (135) Power Loss (80) Firmware Version 05-01-03-0101-09",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_2",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-05-31 06:44:34 (134) Power Loss (80) Firmware Version 05-01-03-0101-09",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_3",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-05-28 00:47:55 (133) Forced Reboot (81) Firmware Version 05-01-02-0101-05",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_4",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-05-10 07:19:17 (132) Unexpected Reboot (43) Firmware Version 05-01-02-0101-05",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_5",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-05-10 07:11:23 (131) Power Loss (80) Firmware Version 05-01-02-0101-05",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Reboot_Log.Reboot_Line_6",
|
||||
"isEmpty": false,
|
||||
"preview": "2026-04-12 09:21:45 (130) Power Loss (80) Firmware Version 05-01-02-0101-05",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Base_Station_Status",
|
||||
"isEmpty": false,
|
||||
"preview": "Idle",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Custom_CA_Status.Custom_CA_Provisioning_Status",
|
||||
"isEmpty": false,
|
||||
"preview": "N/A",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Custom_CA_Status.Custom_CA_Info",
|
||||
"isEmpty": false,
|
||||
"preview": "Not Installed",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Dot1x_Authentication.Transaction_status",
|
||||
"isEmpty": false,
|
||||
"preview": "Unavailable",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Dot1x_Authentication.Protocol",
|
||||
"isEmpty": false,
|
||||
"preview": "N/A",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RSSI_List.RSSI_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "RSSI for RPN:4 is -49 [dBm]",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Total_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "4",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Max_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "-1",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Time_In_Max_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "49710 days 06:28:15 [H:M:S]",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Current_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Current_Local_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Current_Relay_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Remote_Relay_RTP",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.RTP_Usage.Current_Recording",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_Presence",
|
||||
"isEmpty": true,
|
||||
"preview": null,
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Base_Station",
|
||||
"isEmpty": false,
|
||||
"preview": "Base type:DBS-210-3PC - Required Version:501 Required Branch:309",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Line_0",
|
||||
"isEmpty": false,
|
||||
"preview": "Device type:6825 - Required Version:501 Required Branch:308 Language Pack:6825_default",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "Device type:6825-RGD - Required Version:501 Required Branch:308 Language Pack:6825-RGD_default",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Line_2",
|
||||
"isEmpty": false,
|
||||
"preview": "Device type:6823 - Required Version:501 Required Branch:308 Language Pack:6823_default",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Device_FWU_Info.Device_Line_3",
|
||||
"isEmpty": false,
|
||||
"preview": "Device type:RPT-110-3PC - Required Version:501 Required Branch:303",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Push_To_Talk",
|
||||
"isEmpty": false,
|
||||
"preview": "Off",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_1",
|
||||
"isEmpty": false,
|
||||
"preview": "911",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_2",
|
||||
"isEmpty": false,
|
||||
"preview": "1911",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_3",
|
||||
"isEmpty": false,
|
||||
"preview": "933",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_4",
|
||||
"isEmpty": false,
|
||||
"preview": "No Number set!",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.Emergency_Calls.Emergency_Number_5",
|
||||
"isEmpty": false,
|
||||
"preview": "No Number set!",
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.System_Information.SIP_Identity_Status",
|
||||
"isEmpty": true,
|
||||
"preview": null,
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Device_Information",
|
||||
"isEmpty": true,
|
||||
"preview": null,
|
||||
"handsetRelated": false
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Packets",
|
||||
"isEmpty": false,
|
||||
"preview": "3813285",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Blocked",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Dropped",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Errors",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Tx_Broadcasts",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Packets",
|
||||
"isEmpty": false,
|
||||
"preview": "5485843",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Blocked",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Dropped",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Errors",
|
||||
"isEmpty": false,
|
||||
"preview": "0",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Network_Statistics.Rx_Broadcasts",
|
||||
"isEmpty": false,
|
||||
"preview": "4773",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Header_Line_Idx",
|
||||
"isEmpty": false,
|
||||
"preview": "RPN, MAC-Addr, OP[s], DT[s], Call Cnt, Call Drop, Call Failed, Duration[s], Active Calls, Max Calls, Busy, Busy Duratio…",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Value_Line_0",
|
||||
"isEmpty": false,
|
||||
"preview": "'00','6CAB05F635DE',3386184,5238180,942,1,8,78353,0,4,0,0,255,0,0,892,0,0,0,35,0,0,285,94,7662,1,1,0,24,0,0,90,109,74,1…",
|
||||
"handsetRelated": true
|
||||
},
|
||||
{
|
||||
"path": "Status.Statistics.Value_Line_1",
|
||||
"isEmpty": false,
|
||||
"preview": "'00','6CAB05F63642',3386126,5238247,160,0,0,12875,0,2,0,0,1,1,8,146,0,0,0,2,0,0,30,10,167,2,2,2,25,0,0,60,118,72,105,52…",
|
||||
"handsetRelated": true
|
||||
}
|
||||
]
|
||||
169
tests/fixtures/dect/status-933-f63642.parsed.json
vendored
Normal file
169
tests/fixtures/dect/status-933-f63642.parsed.json
vendored
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
{
|
||||
"parsed": {
|
||||
"device": {
|
||||
"model": "IPDECT-V2 (DBS-210-3PC)",
|
||||
"systemType": "Generic SIP (RFC 3261)",
|
||||
"unitName": "SME VoIP",
|
||||
"unitIndex": "Base Idx:1",
|
||||
"rfBand": "US",
|
||||
"productConfiguration": "0000",
|
||||
"macAddress": "6c:ab:05:f6:36:42",
|
||||
"ipAddress": "10.52.38.152",
|
||||
"rfpiAddress": "135098C1; RPN:04",
|
||||
"releasedBuild": true
|
||||
},
|
||||
"firmware": {
|
||||
"version": "IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29",
|
||||
"updateServer": "https://cisco.sipflash.com",
|
||||
"updatePath": "dms/dbS210",
|
||||
"requiredFor": {
|
||||
"6823": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "308",
|
||||
"languagePack": "6823_default",
|
||||
"_sourceKey": "Device_Line_2"
|
||||
},
|
||||
"6825": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "308",
|
||||
"languagePack": "6825_default",
|
||||
"_sourceKey": "Device_Line_0"
|
||||
},
|
||||
"DBS-210-3PC": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "309",
|
||||
"languagePack": null,
|
||||
"_sourceKey": "Device_Base_Station"
|
||||
},
|
||||
"6825-RGD": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "308",
|
||||
"languagePack": "6825-RGD_default",
|
||||
"_sourceKey": "Device_Line_1"
|
||||
},
|
||||
"RPT-110-3PC": {
|
||||
"requiredVersion": "501",
|
||||
"requiredBranch": "303",
|
||||
"languagePack": null,
|
||||
"_sourceKey": "Device_Line_3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"currentLocalTime": "27-Jul-2026 15:51:57",
|
||||
"operatingTime": "39 Days 04:36:13 (H:M:S)",
|
||||
"operatingTimeSeconds": 16573
|
||||
},
|
||||
"multiCell": {
|
||||
"role": "ready",
|
||||
"raw": "Ready(TXT_STATE_KEEP_ALIVE) Secondary"
|
||||
},
|
||||
"baseStatus": "idle",
|
||||
"conflictInfo": "No Conflict",
|
||||
"security": {
|
||||
"customCa": {
|
||||
"provisioningStatus": "N/A",
|
||||
"info": "Not Installed",
|
||||
"installed": false
|
||||
},
|
||||
"dot1x": {
|
||||
"transactionStatus": "Unavailable",
|
||||
"protocol": "N/A"
|
||||
}
|
||||
},
|
||||
"rebootLog": [
|
||||
{
|
||||
"at": "2026-06-18T11:09:04",
|
||||
"sequence": 135,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-03-0101-09",
|
||||
"raw": "2026-06-18 11:09:04 (135) Power Loss (80) Firmware Version 05-01-03-0101-09"
|
||||
},
|
||||
{
|
||||
"at": "2026-05-31T06:44:34",
|
||||
"sequence": 134,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-03-0101-09",
|
||||
"raw": "2026-05-31 06:44:34 (134) Power Loss (80) Firmware Version 05-01-03-0101-09"
|
||||
},
|
||||
{
|
||||
"at": "2026-05-28T00:47:55",
|
||||
"sequence": 133,
|
||||
"reasonName": "Forced Reboot",
|
||||
"reasonCode": 81,
|
||||
"firmwareAtBoot": "05-01-02-0101-05",
|
||||
"raw": "2026-05-28 00:47:55 (133) Forced Reboot (81) Firmware Version 05-01-02-0101-05"
|
||||
},
|
||||
{
|
||||
"at": "2026-05-10T07:19:17",
|
||||
"sequence": 132,
|
||||
"reasonName": "Unexpected Reboot",
|
||||
"reasonCode": 43,
|
||||
"firmwareAtBoot": "05-01-02-0101-05",
|
||||
"raw": "2026-05-10 07:19:17 (132) Unexpected Reboot (43) Firmware Version 05-01-02-0101-05"
|
||||
},
|
||||
{
|
||||
"at": "2026-05-10T07:11:23",
|
||||
"sequence": 131,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-02-0101-05",
|
||||
"raw": "2026-05-10 07:11:23 (131) Power Loss (80) Firmware Version 05-01-02-0101-05"
|
||||
},
|
||||
{
|
||||
"at": "2026-04-12T09:21:45",
|
||||
"sequence": 130,
|
||||
"reasonName": "Power Loss",
|
||||
"reasonCode": 80,
|
||||
"firmwareAtBoot": "05-01-02-0101-05",
|
||||
"raw": "2026-04-12 09:21:45 (130) Power Loss (80) Firmware Version 05-01-02-0101-05"
|
||||
}
|
||||
],
|
||||
"rtp": {
|
||||
"total": 4,
|
||||
"max": -1,
|
||||
"current": 0,
|
||||
"currentLocal": 0,
|
||||
"currentRelay": 0,
|
||||
"remoteRelay": 0,
|
||||
"currentRecording": 0,
|
||||
"timeInMaxRtp": "49710 days 06:28:15 [H:M:S]"
|
||||
},
|
||||
"network": {
|
||||
"txPackets": 3813285,
|
||||
"txBlocked": 0,
|
||||
"txDropped": 0,
|
||||
"txErrors": 0,
|
||||
"txBroadcasts": 0,
|
||||
"rxPackets": 5485843,
|
||||
"rxBlocked": 0,
|
||||
"rxDropped": 0,
|
||||
"rxErrors": 0,
|
||||
"rxBroadcasts": 4773
|
||||
},
|
||||
"emergencyNumbers": [
|
||||
"911",
|
||||
"1911",
|
||||
"933"
|
||||
],
|
||||
"rssi": [],
|
||||
"devicePresence": [],
|
||||
"sipIdentityStatus": [],
|
||||
"perRpnStats": [],
|
||||
"features": {
|
||||
"pushToTalk": false
|
||||
},
|
||||
"statisticsHeader": "RPN, MAC-Addr, OP[s], DT[s], Call Cnt, Call Drop, Call Failed, Duration[s], Active Calls, Max Calls, Busy, Busy Duration[s], Min Latency[ms], Avg Latency[ms], Max Latency[ms], G711U, G711A, G729, G722, G726, OPUS, BV32, Handovers, Failed Handovers, SIP reg failed, Handset Removed, Searching, RcHeapFree Running, New SyncSource, LAN Sync Lost, LAN Primary Lost, Freq[0][0], Freq[0][1], Freq[0][2], Freq[0][3], Freq[0][4], Freq[0][5], Freq[0][6], Freq[0][7], Freq[0][8], Freq[0][9], Freq[0][10], Freq[0][11], Freq[1][0], Freq[1][1], Freq[1][2], Freq[1][3], Freq[1][4], Freq[1][5], Freq[1][6], Freq[1][7], Freq[1][8], Freq[1][9], Freq[1][10], Freq[1][11], Freq[2][0], Freq[2][1], Freq[2][2], Freq[2][3], Freq[2][4], Freq[2][5], Freq[2][6], Freq[2][7], Freq[2][8], Freq[2][9], Freq[2][10], Freq[2][11], Freq[3][0], Freq[3][1], Freq[3][2], Freq[3][3], Freq[3][4], Freq[3][5], Freq[3][6], Freq[3][7], Freq[3][8], Freq[3][9], Freq[3][10], Freq[3][11], Freq[4][0], Freq[4][1], Freq[4][2], Freq[4][3], Freq[4][4], Freq[4][5], Freq[4][6], Freq[4][7], Freq[4][8], Freq[4][9], Freq[4][10], Freq[4][11], Freq[5][0], Freq[5][1], Freq[5][2], Freq[5][3], Freq[5][4], Freq[5][5], Freq[5][6], Freq[5][7], Freq[5][8], Freq[5][9], Freq[5][10], Freq[5][11], Freq[6][0], Freq[6][1], Freq[6][2], Freq[6][3], Freq[6][4], Freq[6][5], Freq[6][6], Freq[6][7], Freq[6][8], Freq[6][9], Freq[6][10], Freq[6][11], Freq[7][0], Freq[7][1], Freq[7][2], Freq[7][3], Freq[7][4], Freq[7][5], Freq[7][6], Freq[7][7], Freq[7][8], Freq[7][9], Freq[7][10], Freq[7][11], Freq[8][0], Freq[8][1], Freq[8][2], Freq[8][3], Freq[8][4], Freq[8][5], Freq[8][6], Freq[8][7], Freq[8][8], Freq[8][9], Freq[8][10], Freq[8][11], Freq[9][0], Freq[9][1], Freq[9][2], Freq[9][3], Freq[9][4], Freq[9][5], Freq[9][6], Freq[9][7], Freq[9][8], Freq[9][9], Freq[9][10], Freq[9][11], R-Idx[0], R-OP[0][s], R-Busy[0], R-Busy Duration[0][s], Max Calls[0], Searching[0], Recovery[0], New SyncSource[0], Wide Band[0], Narrow Band[0], R-Idx[1], R-OP[1][s], R-Busy[1], R-Busy Duration[1][s], Max Calls[1], Searching[1], Recovery[1], New SyncSource[1], Wide Band[1], Narrow Band[1], R-Idx[2], R-OP[2][s], R-Busy[2], R-Busy Duration[2][s], Max Calls[2], Searching[2], Recovery[2], New SyncSource[2], Wide Band[2], Narrow Band[2]",
|
||||
"_rawStatisticsHeader": "RPN, MAC-Addr, OP[s], DT[s], Call Cnt, Call Drop, Call Failed, Duration[s], Active Calls, Max Calls, Busy, Busy Duration[s], Min Latency[ms], Avg Latency[ms], Max Latency[ms], G711U, G711A, G729, G722, G726, OPUS, BV32, Handovers, Failed Handovers, SIP reg failed, Handset Removed, Searching, RcHeapFree Running, New SyncSource, LAN Sync Lost, LAN Primary Lost, Freq[0][0], Freq[0][1], Freq[0][2], Freq[0][3], Freq[0][4], Freq[0][5], Freq[0][6], Freq[0][7], Freq[0][8], Freq[0][9], Freq[0][10], Freq[0][11], Freq[1][0], Freq[1][1], Freq[1][2], Freq[1][3], Freq[1][4], Freq[1][5], Freq[1][6], Freq[1][7], Freq[1][8], Freq[1][9], Freq[1][10], Freq[1][11], Freq[2][0], Freq[2][1], Freq[2][2], Freq[2][3], Freq[2][4], Freq[2][5], Freq[2][6], Freq[2][7], Freq[2][8], Freq[2][9], Freq[2][10], Freq[2][11], Freq[3][0], Freq[3][1], Freq[3][2], Freq[3][3], Freq[3][4], Freq[3][5], Freq[3][6], Freq[3][7], Freq[3][8], Freq[3][9], Freq[3][10], Freq[3][11], Freq[4][0], Freq[4][1], Freq[4][2], Freq[4][3], Freq[4][4], Freq[4][5], Freq[4][6], Freq[4][7], Freq[4][8], Freq[4][9], Freq[4][10], Freq[4][11], Freq[5][0], Freq[5][1], Freq[5][2], Freq[5][3], Freq[5][4], Freq[5][5], Freq[5][6], Freq[5][7], Freq[5][8], Freq[5][9], Freq[5][10], Freq[5][11], Freq[6][0], Freq[6][1], Freq[6][2], Freq[6][3], Freq[6][4], Freq[6][5], Freq[6][6], Freq[6][7], Freq[6][8], Freq[6][9], Freq[6][10], Freq[6][11], Freq[7][0], Freq[7][1], Freq[7][2], Freq[7][3], Freq[7][4], Freq[7][5], Freq[7][6], Freq[7][7], Freq[7][8], Freq[7][9], Freq[7][10], Freq[7][11], Freq[8][0], Freq[8][1], Freq[8][2], Freq[8][3], Freq[8][4], Freq[8][5], Freq[8][6], Freq[8][7], Freq[8][8], Freq[8][9], Freq[8][10], Freq[8][11], Freq[9][0], Freq[9][1], Freq[9][2], Freq[9][3], Freq[9][4], Freq[9][5], Freq[9][6], Freq[9][7], Freq[9][8], Freq[9][9], Freq[9][10], Freq[9][11], R-Idx[0], R-OP[0][s], R-Busy[0], R-Busy Duration[0][s], Max Calls[0], Searching[0], Recovery[0], New SyncSource[0], Wide Band[0], Narrow Band[0], R-Idx[1], R-OP[1][s], R-Busy[1], R-Busy Duration[1][s], Max Calls[1], Searching[1], Recovery[1], New SyncSource[1], Wide Band[1], Narrow Band[1], R-Idx[2], R-OP[2][s], R-Busy[2], R-Busy Duration[2][s], Max Calls[2], Searching[2], Recovery[2], New SyncSource[2], Wide Band[2], Narrow Band[2]"
|
||||
},
|
||||
"verdict": {
|
||||
"healthy": false,
|
||||
"warnings": [
|
||||
"4 recent power-loss reboot(s); most recent at 2026-06-18T11:09:04"
|
||||
],
|
||||
"info": []
|
||||
},
|
||||
"fetchedAt": "2026-07-27T19:51:57.070Z"
|
||||
}
|
||||
92
tests/fixtures/dect/status-933-f63642.xml
vendored
Normal file
92
tests/fixtures/dect/status-933-f63642.xml
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Status>
|
||||
<System_Information>
|
||||
<Released_Build>Yes</Released_Build>
|
||||
<Multi_Cell>Ready(TXT_STATE_KEEP_ALIVE) Secondary</Multi_Cell>
|
||||
<Phone_Type>IPDECT-V2 (DBS-210-3PC)</Phone_Type>
|
||||
<System_Type>Generic SIP (RFC 3261)</System_Type>
|
||||
<Unit_Name>SME VoIP</Unit_Name>
|
||||
<Unit_Index>Base Idx:1</Unit_Index>
|
||||
<RF_Band>US</RF_Band>
|
||||
<Conflict_Info>No Conflict</Conflict_Info>
|
||||
<Current_Local_Time>27-Jul-2026 15:51:57</Current_Local_Time>
|
||||
<Operating_Time>39 Days 04:36:13 (H:M:S)</Operating_Time>
|
||||
<RFPI_Address>135098C1; RPN:04</RFPI_Address>
|
||||
<MAC_Address>6cab05f63642</MAC_Address>
|
||||
<IP_Address>10.52.38.152</IP_Address>
|
||||
<Product_Configuration>0000</Product_Configuration>
|
||||
<Firmware_Version>IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29</Firmware_Version>
|
||||
<Firmware_URL>
|
||||
<Update_Server_Address>https://cisco.sipflash.com</Update_Server_Address>
|
||||
<Path>dms/dbS210</Path>
|
||||
</Firmware_URL>
|
||||
<Reboot_Log>
|
||||
<Reboot_Line_1>2026-06-18 11:09:04 (135) Power Loss (80) Firmware Version 05-01-03-0101-09</Reboot_Line_1>
|
||||
<Reboot_Line_2>2026-05-31 06:44:34 (134) Power Loss (80) Firmware Version 05-01-03-0101-09</Reboot_Line_2>
|
||||
<Reboot_Line_3>2026-05-28 00:47:55 (133) Forced Reboot (81) Firmware Version 05-01-02-0101-05</Reboot_Line_3>
|
||||
<Reboot_Line_4>2026-05-10 07:19:17 (132) Unexpected Reboot (43) Firmware Version 05-01-02-0101-05</Reboot_Line_4>
|
||||
<Reboot_Line_5>2026-05-10 07:11:23 (131) Power Loss (80) Firmware Version 05-01-02-0101-05</Reboot_Line_5>
|
||||
<Reboot_Line_6>2026-04-12 09:21:45 (130) Power Loss (80) Firmware Version 05-01-02-0101-05</Reboot_Line_6>
|
||||
</Reboot_Log>
|
||||
<Base_Station_Status>Idle</Base_Station_Status>
|
||||
<Custom_CA_Status>
|
||||
<Custom_CA_Provisioning_Status>N/A</Custom_CA_Provisioning_Status>
|
||||
<Custom_CA_Info>Not Installed</Custom_CA_Info>
|
||||
</Custom_CA_Status>
|
||||
<Dot1x_Authentication>
|
||||
<Transaction_status>Unavailable</Transaction_status>
|
||||
<Protocol>N/A</Protocol>
|
||||
</Dot1x_Authentication>
|
||||
<RSSI_List>
|
||||
<RSSI_Line_1>RSSI for RPN:4 is -49 [dBm]</RSSI_Line_1>
|
||||
</RSSI_List>
|
||||
<RTP_Usage>
|
||||
<Total_RTP>4</Total_RTP>
|
||||
<Max_RTP>-1</Max_RTP>
|
||||
<Time_In_Max_RTP>49710 days 06:28:15 [H:M:S]</Time_In_Max_RTP>
|
||||
<Current_RTP>0</Current_RTP>
|
||||
<Current_Local_RTP>0</Current_Local_RTP>
|
||||
<Current_Relay_RTP>0</Current_Relay_RTP>
|
||||
<Remote_Relay_RTP>0</Remote_Relay_RTP>
|
||||
<Current_Recording>0</Current_Recording>
|
||||
</RTP_Usage>
|
||||
<Device_Presence>
|
||||
</Device_Presence>
|
||||
<Device_FWU_Info>
|
||||
<Device_Base_Station>Base type:DBS-210-3PC - Required Version:501 Required Branch:309</Device_Base_Station>
|
||||
<Device_Line_0>Device type:6825 - Required Version:501 Required Branch:308 Language Pack:6825_default</Device_Line_0>
|
||||
<Device_Line_1>Device type:6825-RGD - Required Version:501 Required Branch:308 Language Pack:6825-RGD_default</Device_Line_1>
|
||||
<Device_Line_2>Device type:6823 - Required Version:501 Required Branch:308 Language Pack:6823_default</Device_Line_2>
|
||||
<Device_Line_3>Device type:RPT-110-3PC - Required Version:501 Required Branch:303</Device_Line_3>
|
||||
</Device_FWU_Info>
|
||||
<Push_To_Talk>Off</Push_To_Talk>
|
||||
<Emergency_Calls>
|
||||
<Emergency_Number_1>911</Emergency_Number_1>
|
||||
<Emergency_Number_2>1911</Emergency_Number_2>
|
||||
<Emergency_Number_3>933</Emergency_Number_3>
|
||||
<Emergency_Number_4>No Number set!</Emergency_Number_4>
|
||||
<Emergency_Number_5>No Number set!</Emergency_Number_5>
|
||||
</Emergency_Calls>
|
||||
<SIP_Identity_Status>
|
||||
</SIP_Identity_Status>
|
||||
</System_Information>
|
||||
<Device_Information>
|
||||
</Device_Information>
|
||||
<Statistics>
|
||||
<Network_Statistics>
|
||||
<Tx_Packets>3813285</Tx_Packets>
|
||||
<Tx_Blocked>0</Tx_Blocked>
|
||||
<Tx_Dropped>0</Tx_Dropped>
|
||||
<Tx_Errors>0</Tx_Errors>
|
||||
<Tx_Broadcasts>0</Tx_Broadcasts>
|
||||
<Rx_Packets>5485843</Rx_Packets>
|
||||
<Rx_Blocked>0</Rx_Blocked>
|
||||
<Rx_Dropped>0</Rx_Dropped>
|
||||
<Rx_Errors>0</Rx_Errors>
|
||||
<Rx_Broadcasts>4773</Rx_Broadcasts>
|
||||
</Network_Statistics>
|
||||
<Header_Line_Idx>RPN, MAC-Addr, OP[s], DT[s], Call Cnt, Call Drop, Call Failed, Duration[s], Active Calls, Max Calls, Busy, Busy Duration[s], Min Latency[ms], Avg Latency[ms], Max Latency[ms], G711U, G711A, G729, G722, G726, OPUS, BV32, Handovers, Failed Handovers, SIP reg failed, Handset Removed, Searching, RcHeapFree Running, New SyncSource, LAN Sync Lost, LAN Primary Lost, Freq[0][0], Freq[0][1], Freq[0][2], Freq[0][3], Freq[0][4], Freq[0][5], Freq[0][6], Freq[0][7], Freq[0][8], Freq[0][9], Freq[0][10], Freq[0][11], Freq[1][0], Freq[1][1], Freq[1][2], Freq[1][3], Freq[1][4], Freq[1][5], Freq[1][6], Freq[1][7], Freq[1][8], Freq[1][9], Freq[1][10], Freq[1][11], Freq[2][0], Freq[2][1], Freq[2][2], Freq[2][3], Freq[2][4], Freq[2][5], Freq[2][6], Freq[2][7], Freq[2][8], Freq[2][9], Freq[2][10], Freq[2][11], Freq[3][0], Freq[3][1], Freq[3][2], Freq[3][3], Freq[3][4], Freq[3][5], Freq[3][6], Freq[3][7], Freq[3][8], Freq[3][9], Freq[3][10], Freq[3][11], Freq[4][0], Freq[4][1], Freq[4][2], Freq[4][3], Freq[4][4], Freq[4][5], Freq[4][6], Freq[4][7], Freq[4][8], Freq[4][9], Freq[4][10], Freq[4][11], Freq[5][0], Freq[5][1], Freq[5][2], Freq[5][3], Freq[5][4], Freq[5][5], Freq[5][6], Freq[5][7], Freq[5][8], Freq[5][9], Freq[5][10], Freq[5][11], Freq[6][0], Freq[6][1], Freq[6][2], Freq[6][3], Freq[6][4], Freq[6][5], Freq[6][6], Freq[6][7], Freq[6][8], Freq[6][9], Freq[6][10], Freq[6][11], Freq[7][0], Freq[7][1], Freq[7][2], Freq[7][3], Freq[7][4], Freq[7][5], Freq[7][6], Freq[7][7], Freq[7][8], Freq[7][9], Freq[7][10], Freq[7][11], Freq[8][0], Freq[8][1], Freq[8][2], Freq[8][3], Freq[8][4], Freq[8][5], Freq[8][6], Freq[8][7], Freq[8][8], Freq[8][9], Freq[8][10], Freq[8][11], Freq[9][0], Freq[9][1], Freq[9][2], Freq[9][3], Freq[9][4], Freq[9][5], Freq[9][6], Freq[9][7], Freq[9][8], Freq[9][9], Freq[9][10], Freq[9][11], R-Idx[0], R-OP[0][s], R-Busy[0], R-Busy Duration[0][s], Max Calls[0], Searching[0], Recovery[0], New SyncSource[0], Wide Band[0], Narrow Band[0], R-Idx[1], R-OP[1][s], R-Busy[1], R-Busy Duration[1][s], Max Calls[1], Searching[1], Recovery[1], New SyncSource[1], Wide Band[1], Narrow Band[1], R-Idx[2], R-OP[2][s], R-Busy[2], R-Busy Duration[2][s], Max Calls[2], Searching[2], Recovery[2], New SyncSource[2], Wide Band[2], Narrow Band[2]</Header_Line_Idx>
|
||||
<Value_Line_0>'00','6CAB05F635DE',3386184,5238180,942,1,8,78353,0,4,0,0,255,0,0,892,0,0,0,35,0,0,285,94,7662,1,1,0,24,0,0,90,109,74,117,75,107,69,112,98,108,80,112,99,142,107,130,90,116,100,119,125,129,107,143,104,136,118,130,106,106,95,141,103,159,97,147,102,139,123,146,123,140,95,166,125,139,127,154,116,164,136,162,113,127,105,164,128,155,128,151,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0</Value_Line_0>
|
||||
<Value_Line_1>'00','6CAB05F63642',3386126,5238247,160,0,0,12875,0,2,0,0,1,1,8,146,0,0,0,2,0,0,30,10,167,2,2,2,25,0,0,60,118,72,105,52,118,48,144,57,127,63,122,109,126,88,129,134,129,140,109,127,128,128,142,132,134,85,133,114,117,106,126,97,117,131,133,142,134,82,141,129,137,144,141,124,141,117,122,98,145,78,141,120,146,98,114,90,134,88,123,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0,255,0,0,0,0,0,0,0,0,0</Value_Line_1>
|
||||
</Statistics>
|
||||
</Status>
|
||||
86
tests/fixtures/dect/status-with-handsets.xml
vendored
Normal file
86
tests/fixtures/dect/status-with-handsets.xml
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Status>
|
||||
<System_Information>
|
||||
<Released_Build>Yes</Released_Build>
|
||||
<Multi_Cell>Primary(TXT_STATE_PRIMARY)</Multi_Cell>
|
||||
<Phone_Type>IPDECT-V2 (DBS-210-3PC)</Phone_Type>
|
||||
<System_Type>Generic SIP (RFC 3261)</System_Type>
|
||||
<Unit_Name>SME VoIP</Unit_Name>
|
||||
<Unit_Index>Base Idx:0</Unit_Index>
|
||||
<RF_Band>US</RF_Band>
|
||||
<Conflict_Info>No Conflict</Conflict_Info>
|
||||
<Current_Local_Time>21-Jul-2026 14:30:00</Current_Local_Time>
|
||||
<Operating_Time>48:15:30 (H:M:S)</Operating_Time>
|
||||
<RFPI_Address>13508C9C; RPN:00</RFPI_Address>
|
||||
<MAC_Address>6CAB05F62819</MAC_Address>
|
||||
<IP_Address>10.4.11.87</IP_Address>
|
||||
<Product_Configuration>0000</Product_Configuration>
|
||||
<Firmware_Version>IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29</Firmware_Version>
|
||||
<Firmware_URL>
|
||||
<Update_Server_Address>https://example.invalid</Update_Server_Address>
|
||||
<Path>dms/dbS210</Path>
|
||||
</Firmware_URL>
|
||||
<Reboot_Log>
|
||||
<Reboot_Line_1>2026-07-15 08:00:00 (200) Normal Reboot (21) Firmware Version 05-01-03-0101-09</Reboot_Line_1>
|
||||
</Reboot_Log>
|
||||
<Base_Station_Status>Idle</Base_Station_Status>
|
||||
<Custom_CA_Status>
|
||||
<Custom_CA_Provisioning_Status>N/A</Custom_CA_Provisioning_Status>
|
||||
<Custom_CA_Info>Not Installed</Custom_CA_Info>
|
||||
</Custom_CA_Status>
|
||||
<Dot1x_Authentication>
|
||||
<Transaction_status>Unavailable</Transaction_status>
|
||||
<Protocol>N/A</Protocol>
|
||||
</Dot1x_Authentication>
|
||||
<RSSI_List>
|
||||
<RPN_00>MAC:6CAB05A1B2C3; RSSI:-58 dBm</RPN_00>
|
||||
<RPN_01>MAC:6CAB05D4E5F6; RSSI:-71 dBm</RPN_01>
|
||||
</RSSI_List>
|
||||
<RTP_Usage>
|
||||
<Total_RTP>42</Total_RTP>
|
||||
<Max_RTP>4</Max_RTP>
|
||||
<Current_RTP>0</Current_RTP>
|
||||
<Current_Local_RTP>0</Current_Local_RTP>
|
||||
<Current_Relay_RTP>0</Current_Relay_RTP>
|
||||
<Remote_Relay_RTP>0</Remote_Relay_RTP>
|
||||
<Current_Recording>0</Current_Recording>
|
||||
</RTP_Usage>
|
||||
<Device_Presence>
|
||||
<RPN_00>Present</RPN_00>
|
||||
<RPN_01>Present</RPN_01>
|
||||
</Device_Presence>
|
||||
<Device_FWU_Info>
|
||||
<Device_Base_Station>Base type:DBS-210-3PC - Required Version:501 Required Branch:309</Device_Base_Station>
|
||||
<Device_Line_0>Device type:6825 - Required Version:501 Required Branch:308 Language Pack:6825_default</Device_Line_0>
|
||||
</Device_FWU_Info>
|
||||
<Push_To_Talk>Off</Push_To_Talk>
|
||||
<Emergency_Calls>
|
||||
<Emergency_Number_1>911</Emergency_Number_1>
|
||||
</Emergency_Calls>
|
||||
<SIP_Identity_Status>
|
||||
<Line_0>Registered</Line_0>
|
||||
<Line_1>Registered</Line_1>
|
||||
<Line_2>Not Registered</Line_2>
|
||||
<Line_3>Not Registered</Line_3>
|
||||
</SIP_Identity_Status>
|
||||
</System_Information>
|
||||
<Device_Information>
|
||||
</Device_Information>
|
||||
<Statistics>
|
||||
<Network_Statistics>
|
||||
<Tx_Packets>5000</Tx_Packets>
|
||||
<Tx_Blocked>0</Tx_Blocked>
|
||||
<Tx_Dropped>0</Tx_Dropped>
|
||||
<Tx_Errors>0</Tx_Errors>
|
||||
<Tx_Broadcasts>0</Tx_Broadcasts>
|
||||
<Rx_Packets>12000</Rx_Packets>
|
||||
<Rx_Blocked>0</Rx_Blocked>
|
||||
<Rx_Dropped>0</Rx_Dropped>
|
||||
<Rx_Errors>0</Rx_Errors>
|
||||
<Rx_Broadcasts>100</Rx_Broadcasts>
|
||||
</Network_Statistics>
|
||||
<Header_Line_Idx>RPN, MAC-Addr, OP[s], DT[s]</Header_Line_Idx>
|
||||
<Statistics_Line_1>00, 6CAB05A1B2C3, 1200, 45</Statistics_Line_1>
|
||||
<Statistics_Line_2>01, 6CAB05D4E5F6, 980, 62</Statistics_Line_2>
|
||||
</Statistics>
|
||||
</Status>
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '../services/renderers/phoneStatusRenderer.js';
|
||||
import { renderAvStatusMarkdown } from '../services/renderers/avStatusRenderer.js';
|
||||
import { renderDectStatusMarkdown } from '../services/renderers/dectStatusRenderer.js';
|
||||
import { buildHandsetContext } from '../services/dectStatus/buildHandsetContext.js';
|
||||
|
||||
// Timestamp exactly 3 hours in the past — makes `simpleTimeAgo`
|
||||
// deterministic to "3 hours ago" for the duration of this test run.
|
||||
|
|
@ -430,7 +431,18 @@ const fullOkResult = () => ({
|
|||
{ sequence: 164, at: '2026-07-02T12:54:12', reasonName: 'Power Loss', reasonCode: 80, firmwareAtBoot: '05-01-03-0101-09' },
|
||||
{ sequence: 163, at: '2026-07-02T12:49:50', reasonName: 'Normal Reboot', reasonCode: 21, firmwareAtBoot: '05-01-03-0101-09' },
|
||||
],
|
||||
network: { txPackets: 100, rxPackets: 200, rxDropped: 18, rxErrors: 0, txErrors: 0 },
|
||||
network: {
|
||||
txPackets: 100,
|
||||
txBlocked: 0,
|
||||
txDropped: 0,
|
||||
txErrors: 0,
|
||||
txBroadcasts: 0,
|
||||
rxPackets: 200,
|
||||
rxBlocked: 0,
|
||||
rxDropped: 18,
|
||||
rxErrors: 0,
|
||||
rxBroadcasts: 0,
|
||||
},
|
||||
rtp: { total: 2, current: 0, currentLocal: 0, currentRelay: 0 },
|
||||
security: {
|
||||
customCa: { installed: false },
|
||||
|
|
@ -440,13 +452,13 @@ const fullOkResult = () => ({
|
|||
},
|
||||
verdict: {
|
||||
healthy: false,
|
||||
warnings: ['1 recent power-loss reboot(s); most recent at 2026-07-02T12:54:12'],
|
||||
warnings: ['1 concerning reboot(s) in the last 7 days (latest: Power Loss at 2026-07-02T12:54:12)'],
|
||||
info: ['Rx dropped packets: 18 since last boot'],
|
||||
},
|
||||
elapsedMs: 812,
|
||||
});
|
||||
|
||||
test('dect status renderer: full dump includes reboot log, emergency numbers, and verdict', () => {
|
||||
test('dect status renderer: full dump includes reboot log, network stats, and verdict', () => {
|
||||
const md = renderDectStatusMarkdown([fullOkResult()], {
|
||||
storeNum: '782',
|
||||
footer: false,
|
||||
|
|
@ -458,9 +470,83 @@ test('dect status renderer: full dump includes reboot log, emergency numbers, an
|
|||
assert.match(md, /\*\*Reboot log\*\*/);
|
||||
assert.match(md, /⚡ #164/);
|
||||
assert.match(md, /Power Loss/);
|
||||
assert.match(md, /911, 1911/);
|
||||
assert.match(md, /healthy: \*\*NO\*\*/);
|
||||
assert.match(md, /\*\*TX:\*\* 100 pkts · 0 blocked · 0 dropped · 0 errors · 0 bcast/);
|
||||
assert.match(md, /\*\*RX:\*\* 200 pkts · 0 blocked · 18 dropped · 0 errors · 0 bcast/);
|
||||
assert.match(md, /\*\*RTP:\*\* 2 total/);
|
||||
assert.match(md, /multi-cell Primary/);
|
||||
assert.match(md, /Rx dropped packets: 18/);
|
||||
assert.doesNotMatch(md, /\*\*Security\*\*/);
|
||||
assert.doesNotMatch(md, /\*\*Emergency numbers\*\*/);
|
||||
assert.doesNotMatch(md, /Update server/);
|
||||
assert.doesNotMatch(md, /\*\*Health verdict\*\*/);
|
||||
});
|
||||
|
||||
test('dect status renderer: shows registered handsets and RSSI when context provided', () => {
|
||||
const webexId = 'base-1';
|
||||
const handsetCtx = buildHandsetContext({
|
||||
dectNetwork: { name: 'Store DECT', handsetsCount: 2 },
|
||||
dectBasestations: [{ id: webexId, mac: '6c:ab:05:f6:28:19', linesRegistered: 2 }],
|
||||
dectHandsets: [
|
||||
{
|
||||
index: 1,
|
||||
name: 'Handset A',
|
||||
extension: '50482',
|
||||
baseStationId: webexId,
|
||||
mac: '6c:ab:05:a1:b2:c3',
|
||||
lastRegistrationTime: new Date(Date.now() - 3 * 3600 * 1000).toISOString(),
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
name: 'Handset B',
|
||||
extension: '50483',
|
||||
baseStationId: webexId,
|
||||
mac: '6c:ab:05:d4:e5:f6',
|
||||
lastRegistrationTime: new Date(Date.now() - 3 * 3600 * 1000).toISOString(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = fullOkResult();
|
||||
result.base.webexId = webexId;
|
||||
result.data.rssi = [
|
||||
{ rpn: '00', mac: '6c:ab:05:a1:b2:c3', rssiDbm: -58 },
|
||||
{ rpn: '01', mac: '6c:ab:05:d4:e5:f6', rssiDbm: -71 },
|
||||
];
|
||||
result.data.devicePresence = [{ key: 'RPN_00', present: true, raw: 'Present' }];
|
||||
result.data.sipIdentityStatus = [
|
||||
{ key: 'Line_0', status: 'Registered' },
|
||||
{ key: 'Line_1', status: 'Registered' },
|
||||
];
|
||||
|
||||
const md = renderDectStatusMarkdown([result], {
|
||||
storeNum: '782',
|
||||
footer: false,
|
||||
handsetCtx,
|
||||
});
|
||||
|
||||
assert.match(md, /\*\*Handsets & RF\*\*/);
|
||||
assert.match(md, /Handset A.*ext 50482/);
|
||||
assert.match(md, /Webex lines registered on base: \*\*2\*\*/);
|
||||
assert.match(md, /-58 dBm/);
|
||||
assert.match(md, /\*\*SIP\*\* line 0: \*\*Registered\*\*/);
|
||||
assert.match(md, /\*\*Presence\*\* RPN_00: present/);
|
||||
});
|
||||
|
||||
test('dect status renderer: unassigned handsets at store level', () => {
|
||||
const handsetCtx = buildHandsetContext({
|
||||
dectBasestations: [{ id: 'base-1', mac: '6c:ab:05:f6:28:19', linesRegistered: 0 }],
|
||||
dectHandsets: [
|
||||
{ name: 'Floater', extension: '9999', baseStationId: null, lastRegistrationTime: null },
|
||||
],
|
||||
});
|
||||
|
||||
const md = renderDectStatusMarkdown([fullOkResult()], {
|
||||
storeNum: '782',
|
||||
footer: false,
|
||||
handsetCtx,
|
||||
});
|
||||
assert.match(md, /\*\*Unassigned handsets\*\*/);
|
||||
assert.match(md, /Floater/);
|
||||
});
|
||||
|
||||
test('dect status renderer: empty discovery + offline relay still produces a usable message', () => {
|
||||
|
|
|
|||
81
tests/statusXml.handsets.test.js
Normal file
81
tests/statusXml.handsets.test.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
import { parseStatusXml } from '../integrations/cisco-dect/statusXml.js';
|
||||
import { inventoryStatusXml, summarizeInventory } from '../integrations/cisco-dect/statusXmlInventory.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const FIXTURE_DIR = path.join(__dirname, 'fixtures/dect');
|
||||
|
||||
const SYNTHETIC = readFileSync(path.join(FIXTURE_DIR, 'status-with-handsets.xml'), 'utf8');
|
||||
const STORE933_PRIMARY = readFileSync(path.join(FIXTURE_DIR, 'status-933-f635de.xml'), 'utf8');
|
||||
const STORE933_SECONDARY = readFileSync(path.join(FIXTURE_DIR, 'status-933-f63642.xml'), 'utf8');
|
||||
|
||||
test('parseStatusXml: synthetic fixture parses RSSI rows', () => {
|
||||
const s = parseStatusXml(SYNTHETIC);
|
||||
assert.equal(s.rssi.length, 2);
|
||||
assert.equal(s.rssi[0].mac, '6c:ab:05:a1:b2:c3');
|
||||
assert.equal(s.rssi[0].rssiDbm, -58);
|
||||
});
|
||||
|
||||
test('parseStatusXml: store 933 primary parses RSSI_Line format', () => {
|
||||
const s = parseStatusXml(STORE933_PRIMARY);
|
||||
assert.equal(s.rssi.length, 1);
|
||||
assert.equal(s.rssi[0].rpn, '4');
|
||||
assert.equal(s.rssi[0].rssiDbm, -47);
|
||||
});
|
||||
|
||||
test('parseStatusXml: store 933 primary parses device presence and handsets', () => {
|
||||
const s = parseStatusXml(STORE933_PRIMARY);
|
||||
assert.equal(s.devicePresence.length, 2);
|
||||
assert.equal(s.devicePresence[0].extension, '1');
|
||||
assert.equal(s.devicePresence[0].deviceType, '6825');
|
||||
assert.equal(s.devices.length, 2);
|
||||
assert.equal(s.devices[0].displayName, '1-50933');
|
||||
assert.equal(s.devices[0].rssiDbm, -76);
|
||||
assert.equal(s.devices[0].batteryPercent, 81);
|
||||
assert.match(s.devices[1].lockedRpn || '', /RPN:04/);
|
||||
});
|
||||
|
||||
test('parseStatusXml: store 933 primary parses SIP identity blocks', () => {
|
||||
const s = parseStatusXml(STORE933_PRIMARY);
|
||||
assert.equal(s.sipIdentityStatus.length, 2);
|
||||
assert.equal(s.sipIdentityStatus[0].status, 'OK');
|
||||
assert.match(s.sipIdentityStatus[1].status || '', /Registering/i);
|
||||
});
|
||||
|
||||
test('parseStatusXml: store 933 parses Value_Line statistics with MAC', () => {
|
||||
const s = parseStatusXml(STORE933_PRIMARY);
|
||||
assert.ok(s.perRpnStats.length >= 2);
|
||||
assert.equal(s.perRpnStats[0].mac, '6c:ab:05:f6:35:de');
|
||||
assert.ok(s.perRpnStats[0].opSeconds > 0);
|
||||
});
|
||||
|
||||
test('parseStatusXml: store 933 uptime includes days component', () => {
|
||||
const s = parseStatusXml(STORE933_PRIMARY);
|
||||
assert.ok(s.time.operatingTimeSeconds > 86400);
|
||||
});
|
||||
|
||||
test('parseStatusXml: store 933 primary parses multi-cell role', () => {
|
||||
const s = parseStatusXml(STORE933_PRIMARY);
|
||||
assert.equal(s.multiCell.role, 'primary');
|
||||
assert.equal(s.multiCell.state, 'ready');
|
||||
});
|
||||
|
||||
test('parseStatusXml: store 933 secondary has RSSI but no registered handsets', () => {
|
||||
const s = parseStatusXml(STORE933_SECONDARY);
|
||||
assert.equal(s.rssi[0].rssiDbm, -49);
|
||||
assert.equal(s.devices.length, 0);
|
||||
assert.equal(s.multiCell.role, 'secondary');
|
||||
assert.equal(s.multiCell.state, 'ready');
|
||||
});
|
||||
|
||||
test('inventoryStatusXml: store 933 finds handset-related paths', () => {
|
||||
const inv = inventoryStatusXml(STORE933_PRIMARY);
|
||||
const summary = summarizeInventory(inv);
|
||||
assert.ok(summary.handsetRelated > 0);
|
||||
assert.ok(summary.paths.some((p) => /Device_Information/i.test(p)));
|
||||
});
|
||||
|
|
@ -213,6 +213,16 @@ test('parseStatusXml: multi-cell role parsed to a normalized token', () => {
|
|||
assert.match(s.multiCell.raw, /TXT_STATE_UNCHAINED/);
|
||||
});
|
||||
|
||||
test('parseStatusXml: ready+primary multi-cell keeps primary as role', () => {
|
||||
const xml = FIXTURE.replace(
|
||||
/<Multi_Cell>[^<]+<\/Multi_Cell>/,
|
||||
'<Multi_Cell>Ready(TXT_STATE_KEEP_ALIVE) Primary</Multi_Cell>',
|
||||
);
|
||||
const s = parseStatusXml(xml);
|
||||
assert.equal(s.multiCell.role, 'primary');
|
||||
assert.equal(s.multiCell.state, 'ready');
|
||||
});
|
||||
|
||||
test('parseStatusXml: reboot log is 6 sorted entries with structured fields', () => {
|
||||
const s = parseStatusXml(FIXTURE);
|
||||
assert.equal(s.rebootLog.length, 6);
|
||||
|
|
@ -260,23 +270,48 @@ test('parseStatusXml: pushToTalk feature flag', () => {
|
|||
|
||||
// ─── Health verdict ─────────────────────────────────────────────────
|
||||
|
||||
test('summarizeBaseHealth: fixture flags power-loss reboot and short uptime', () => {
|
||||
test('summarizeBaseHealth: fixture rx-dropped surfaces as info (old reboots ignored)', () => {
|
||||
const s = parseStatusXml(FIXTURE);
|
||||
const verdict = summarizeBaseHealth(s);
|
||||
assert.equal(verdict.healthy, false);
|
||||
// Uptime 620s (~10 min) is < 600 → boundary, so no uptime warning.
|
||||
// But we DO have a power loss in the log, so healthy=false via that.
|
||||
// Fixture power-loss reboot is from 2026-07-02 — outside the 7-day window.
|
||||
assert.ok(
|
||||
verdict.warnings.some((w) => /power-loss/i.test(w)),
|
||||
`expected power-loss warning; got: ${JSON.stringify(verdict.warnings)}`,
|
||||
!verdict.warnings.some((w) => /concerning reboot/i.test(w)),
|
||||
`old reboot should not warn; got: ${JSON.stringify(verdict.warnings)}`,
|
||||
);
|
||||
// rxDropped=9 → info, not warning
|
||||
assert.ok(
|
||||
verdict.info.some((i) => /rx dropped/i.test(i)),
|
||||
`expected rx-dropped info; got: ${JSON.stringify(verdict.info)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('summarizeBaseHealth: power-loss within 7 days is flagged', () => {
|
||||
const recent = new Date(Date.now() - 2 * 86400000).toISOString().slice(0, 19);
|
||||
const verdict = summarizeBaseHealth({
|
||||
conflictInfo: 'No Conflict',
|
||||
network: { rxDropped: 0 },
|
||||
rebootLog: [
|
||||
{ at: recent, sequence: 1, reasonName: 'Power Loss', reasonCode: 80 },
|
||||
],
|
||||
});
|
||||
assert.equal(verdict.healthy, false);
|
||||
assert.ok(
|
||||
verdict.warnings.some((w) => /concerning reboot/i.test(w)),
|
||||
`expected concerning reboot warning; got: ${JSON.stringify(verdict.warnings)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('summarizeBaseHealth: unexpected reboot within 7 days is flagged', () => {
|
||||
const recent = new Date(Date.now() - 1 * 86400000).toISOString().slice(0, 19);
|
||||
const verdict = summarizeBaseHealth({
|
||||
conflictInfo: 'No Conflict',
|
||||
network: { rxDropped: 0 },
|
||||
rebootLog: [
|
||||
{ at: recent, sequence: 1, reasonName: 'Unexpected Reboot', reasonCode: 43 },
|
||||
],
|
||||
});
|
||||
assert.ok(verdict.warnings.some((w) => /Unexpected reboot/i.test(w)));
|
||||
});
|
||||
|
||||
test('summarizeBaseHealth: RF conflict is flagged as a warning', () => {
|
||||
const s = parseStatusXml(FIXTURE.replace(
|
||||
'<Conflict_Info>No Conflict</Conflict_Info>',
|
||||
|
|
|
|||
Loading…
Reference in a new issue