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:
jmcqueen 2026-07-28 09:02:55 -04:00
parent 90a56c4640
commit 2b8c4e06aa
26 changed files with 3128 additions and 134 deletions

View file

@ -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.');

View file

@ -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.',

View file

@ -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" }

View file

@ -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-

View file

@ -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);

View file

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

View 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)}`;
}

View file

@ -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,
};

View file

@ -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',

View file

@ -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

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

View 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 24 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;
});
}

View 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,
};
});
}

View file

@ -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 {
for (const entry of log) {
if (entry.unrecognized) {
lines.push(`- ??? ${entry.raw || ''}`);
continue;
}
const tag = entry.reasonCode === 80 ? '⚡' : '•';
lines.push(
`- ${tag} #${entry.sequence} ${entry.at} **${entry.reasonName}** (${entry.reasonCode}) fw=${entry.firmwareAtBoot || '?'}`,
);
return lines;
}
for (const entry of log) {
if (entry.unrecognized) {
lines.push(`- ??? ${entry.raw || ''}`);
continue;
}
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')}`);
}
if (hasRtp) {
lines.push(
`- **RTP:** ${netVal(rtp.total)} total · ${netVal(rtp.current)} active · ` +
`${netVal(rtp.currentLocal)} local · ${netVal(rtp.currentRelay)} relay`,
);
}
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;
}
}
// ── 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',
);
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}`);
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;
}
for (const i of verdict.info || []) {
lines.push(`- ${i}`);
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;
}

View file

@ -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: [

View 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);
});

View 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
}
]

View 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"
}

View 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>

View 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
}
]

View 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"
}

View 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>

View 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>

View file

@ -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', () => {

View 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)));
});

View file

@ -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>',