// integrations/atlas/azmWs.js — Atmosphere AZM JSON-RPC over WebSocket (/ws) through a Xyte tunnel import crypto from 'crypto'; import WebSocket from 'ws'; const AUTH_KEY = Buffer.from('M1T3kU5@W31c0m35Ev3ry0n3', 'utf8'); const AUTH_IV = Buffer.from('Ev3ry0n3', 'utf8'); const METER_PARAMS = new Set(['Source Meter', 'Out Group Meter']); export function computeAuthReply(challenge) { const cipher = crypto.createCipheriv('des-ede3-cbc', AUTH_KEY, AUTH_IV); return cipher.update(String(challenge), 'utf8', 'base64') + cipher.final('base64'); } export function encryptUserCredentials(userName, password) { const payload = JSON.stringify({ userName, password }); const cipher = crypto.createCipheriv('des-ede3-cbc', AUTH_KEY, AUTH_IV); return cipher.update(payload, 'utf8', 'base64') + cipher.final('base64'); } export function decryptUserBlob(encrypted) { if (!encrypted || encrypted === 'Not Found') return null; try { const decipher = crypto.createDecipheriv('des-ede3-cbc', AUTH_KEY, AUTH_IV); const raw = decipher.update(String(encrypted), 'base64', 'utf8') + decipher.final('utf8'); return JSON.parse(raw); } catch { return null; } } export function getDeviceCredentials() { const userName = process.env.ATLAS_DEVICE_USERNAME; const password = process.env.ATLAS_DEVICE_PASSWORD; if (!userName || !password) return null; return { userName, password }; } function paramKey(p) { const parts = [p.obj, p.param]; if (p.x !== undefined && p.x !== '*') parts.push(`x=${p.x}`); if (p.channel !== undefined && p.channel !== '*') parts.push(`ch=${p.channel}`); if (p.device !== undefined && p.device !== '*') parts.push(`dev=${p.device}`); return parts.join('|'); } function upsertMetric(byKey, p) { if (!p?.obj || !p?.param) return; byKey.set(paramKey(p), p); } function appendMeterSample(meterSamples, p) { if (!p?.obj || !METER_PARAMS.has(p.param)) return; const key = paramKey(p); const n = p.val != null ? Number(p.val) : Number(p.str); if (!Number.isFinite(n)) return; if (!meterSamples.has(key)) meterSamples.set(key, []); meterSamples.get(key).push(n); } function summarizeSamples(samples) { if (!samples?.length) return null; const low = Math.min(...samples); const high = Math.max(...samples); const avg = samples.reduce((a, b) => a + b, 0) / samples.length; return { low, high, avg, samples: samples.length }; } export function fetchAzmMetrics(wsUrl, { cookie, origin, sessionName = 'test-ops', gets = DEFAULT_GETS, followUpGets = null, followUpCollectMs = 1500, meterSubscribes = null, meterSampleMs = 3000, collectMs = 0, timeoutMs = 30000, username, password, login = true, } = {}) { return new Promise((resolve, reject) => { if (!cookie) { reject(new Error('cookie (xyte_auth) is required')); return; } const envCreds = getDeviceCredentials(); const userName = username || envCreds?.userName; const pass = password || envCreds?.password; const doLogin = login && userName && pass; const headers = { Cookie: cookie }; if (origin) headers.Origin = origin; const ws = new WebSocket(wsUrl, { headers }); const byKey = new Map(); const meterSamples = new Map(); let phase = 'challenge'; let settled = false; let deviceUser = null; let collectTimer = null; const resultPayload = () => ({ metrics: [...byKey.values()], meterSamples: Object.fromEntries( [...meterSamples.entries()].map(([k, samples]) => [k, summarizeSamples(samples)]), ), sessionName, deviceLogin: doLogin ? { ok: true, userName: deviceUser?.userName || userName, role: deviceUser?.role } : null, }); const finish = (err, result) => { if (settled) return; settled = true; clearTimeout(timer); if (collectTimer) clearTimeout(collectTimer); try { ws.close(); } catch { /* ignore */ } if (err) reject(err); else resolve(result); }; const timer = setTimeout(() => { if (phase === 'device-login') { finish(new Error( `Device login timed out for user "${userName}" (no successful User Accounts/Validate response)`, )); } else if (phase === 'metrics' || phase === 'meters' || phase === 'follow-up') { if (byKey.size || meterSamples.size) finish(null, resultPayload()); else finish(new Error(`AZM WS timeout in phase=${phase}`)); } else { finish(new Error(`AZM WS timeout in phase=${phase}`)); } }, timeoutMs); const send = (obj) => ws.send(JSON.stringify(obj)); const completeMetrics = () => finish(null, resultPayload()); const startFollowUp = () => { if (!followUpGets?.length) { completeMetrics(); return; } phase = 'follow-up'; try { const params = followUpGets.map(p => ({ ...p, sub: p.sub ?? 1 })); send({ jsonrpc: '2.0', method: 'subscribe', params }); } catch { completeMetrics(); return; } collectTimer = setTimeout(completeMetrics, followUpCollectMs); }; const startMeterSampling = () => { if (!meterSubscribes?.length) { startFollowUp(); return; } phase = 'meters'; const params = meterSubscribes.map(p => ({ ...p, sub: p.sub ?? 0 })); send({ jsonrpc: '2.0', method: 'subscribe', params }); collectTimer = setTimeout(() => { collectTimer = null; startFollowUp(); }, meterSampleMs); }; const requestMetrics = () => { phase = 'metrics'; if (gets?.length) { send({ jsonrpc: '2.0', method: 'get', params: gets }); if (collectMs > 0) { collectTimer = setTimeout(() => { collectTimer = null; startMeterSampling(); }, collectMs); } } else if (meterSubscribes?.length) { startMeterSampling(); } else if (followUpGets?.length) { startFollowUp(); } else { finish(null, { metrics: [], meterSamples: {}, sessionName, deviceLogin: doLogin ? { ok: !!deviceUser, userName: deviceUser?.userName || userName } : null, }); } }; const startDeviceLogin = () => { phase = 'device-login'; const encrypted = encryptUserCredentials(userName, pass); send({ jsonrpc: '2.0', method: 'set', params: [{ obj: 'User Accounts', param: 'Validate', str: encrypted }], }); setTimeout(() => { if (phase === 'device-login') { send({ jsonrpc: '2.0', method: 'get', params: [{ obj: 'User Accounts', param: 'Validate' }], }); } }, 400); }; ws.on('open', () => { send({ jsonrpc: '2.0', method: 'auth', params: {} }); }); ws.on('message', (buf) => { let msg; try { msg = JSON.parse(buf.toString()); } catch (err) { finish(err); return; } if (phase === 'challenge' && msg.method === 'authResp') { const challenge = msg.params?.resp; if (!challenge || challenge === 'Welcome!') { finish(new Error(`Unexpected auth challenge: ${JSON.stringify(msg.params)}`)); return; } phase = 'welcome'; send({ jsonrpc: '2.0', method: 'auth', params: { reply: computeAuthReply(challenge) }, }); return; } if (phase === 'welcome' && msg.method === 'authResp') { if (msg.params?.resp !== 'Welcome!') { finish(new Error(`Auth rejected: ${JSON.stringify(msg.params)}`)); return; } send({ jsonrpc: '2.0', method: 'setSessionName', params: { name: sessionName } }); if (doLogin) startDeviceLogin(); else requestMetrics(); return; } if (phase === 'device-login' && msg.method === 'paramUpdate') { const params = Array.isArray(msg.params) ? msg.params : [msg.params]; const validate = params.find(p => p?.obj === 'User Accounts' && p?.param === 'Validate'); if (!validate) return; if (!validate.str || validate.str === 'Not Found') { return; } deviceUser = decryptUserBlob(validate.str); if (!deviceUser?.userName) { deviceUser = { userName, raw: true }; } requestMetrics(); return; } if ((phase === 'metrics' || phase === 'meters' || phase === 'follow-up') && msg.method === 'paramUpdate') { const params = Array.isArray(msg.params) ? msg.params : [msg.params]; const useful = params.filter(p => p && !(p.obj === 'User Accounts' && p.param === 'Validate')); for (const p of useful) { if (phase === 'meters' && METER_PARAMS.has(p.param)) { appendMeterSample(meterSamples, p); upsertMetric(byKey, p); } else { upsertMetric(byKey, p); if (METER_PARAMS.has(p.param)) appendMeterSample(meterSamples, p); } } if ( phase === 'metrics' && collectMs <= 0 && !followUpGets?.length && !meterSubscribes?.length && useful.length ) { completeMetrics(); } } }); ws.on('error', (err) => finish(err)); ws.on('close', (code, reason) => { if (!settled && (phase === 'metrics' || phase === 'meters' || phase === 'follow-up') && (byKey.size || meterSamples.size)) { completeMetrics(); return; } if (!settled && phase !== 'metrics' && phase !== 'meters' && phase !== 'follow-up') { finish(new Error(`WS closed early code=${code} reason=${reason?.toString?.() || ''} phase=${phase}`)); } }); }); } export const DEFAULT_GETS = [ { obj: 'System Info', param: 'Model' }, { obj: 'System Info', param: 'Uptime' }, { obj: 'System Info', param: 'Firmware Version' }, { obj: 'System Info', param: 'Main Cpu Smoothed' }, { obj: 'Project Info', param: 'AZM Name' }, { obj: 'Project Info', param: 'Project Name' }, { obj: 'Network Settings', param: 'Wired IP Address' }, { obj: 'Error Log', param: 'All Live Errors' }, { obj: 'User Accounts', param: 'Validate' }, ]; export const MONITOR_SPEAKER_GETS = [ { obj: 'MonitorRouter', param: 'Input Channel' }, { obj: 'MonitorRouter', param: 'Output Gain' }, ]; /** Atmosphere UI: Off = last enum () at pct 100 on MonitorRouter/Input Channel. */ export const MONITOR_OFF_SET = { obj: 'MonitorRouter', param: 'Input Channel', pct: 100 }; export const METER_SUBSCRIBES = [ { obj: 'Link Manager', param: 'Source Meter', x: '*', sub: 1 }, { obj: 'Link Manager', param: 'Out Group Meter', x: '*', sub: 1 }, ]; export const CHECK_GETS = [ { obj: 'Project Info', param: 'Project Name' }, { obj: 'Project Info', param: 'AZM Name' }, { obj: 'System Info', param: 'Uptime' }, { obj: 'System Info', param: 'Model' }, { obj: 'System Info', param: 'Firmware Version' }, { obj: 'Link Manager', param: 'Source Defined', x: '*' }, { obj: 'Link Manager', param: 'Source Name', x: '*' }, { obj: 'Link Manager', param: 'Source DSP Chans', x: '*' }, { obj: 'Link Manager', param: 'Out Group Defined', x: '*' }, { obj: 'Link Manager', param: 'Out Group Name', x: '*' }, { obj: 'Link Manager', param: 'Out Group Source', x: '*' }, { obj: 'Link Manager', param: 'Out Group Mute', x: '*' }, { obj: 'Link Manager', param: 'Out Group Master Gain', x: '*' }, { obj: 'InGain', param: 'Mute', channel: '*' }, { obj: 'InGain', param: 'Gain', channel: '*' }, { obj: 'Control Wallplate', param: 'Name', device: '*' }, { obj: 'Control Wallplate', param: 'Online', device: '*' }, { obj: 'Control Wallplate', param: 'Type', device: '*' }, ]; function strVal(p) { if (p == null) return null; if (p.str !== undefined && p.str !== null && p.str !== '') return String(p.str); if (p.val !== undefined && p.val !== null) return String(p.val); return null; } function isYes(v) { const s = String(v || '').trim().toLowerCase(); return s === 'yes' || s === 'on' || s === 'true' || s === '1' || s === 'enabled'; } function isMuted(v) { const s = String(v || '').trim().toLowerCase(); if (s === 'yes' || s === 'on' || s === 'true' || s === '1' || s === 'muted') return true; if (s === 'no' || s === 'off' || s === 'false' || s === '0' || s === 'unmuted') return false; return null; } function findParam(metrics, obj, param, indexKey, indexVal) { return metrics.find(p => { if (p.obj !== obj || p.param !== param) return false; if (indexKey == null) return true; return String(p[indexKey]) === String(indexVal); }) || null; } function indexSet(metrics, obj, param, indexKey) { const set = new Set(); for (const p of metrics) { if (p.obj === obj && p.param === param && p[indexKey] !== undefined && p[indexKey] !== '*') { set.add(String(p[indexKey])); } } return [...set].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)); } export function resolveMonitorSpeaker(metrics) { const input = (metrics || []).find( p => p?.obj === 'MonitorRouter' && p?.param === 'Input Channel', ); const gain = (metrics || []).find( p => p?.obj === 'MonitorRouter' && p?.param === 'Output Gain', ); if (!input) { return { status: 'Not present' }; } const raw = strVal(input); if (raw == null || raw === '' || raw === 'Not Found') { return { status: 'Not present' }; } const gainStr = strVal(gain); const isOff = (input.pct != null && Number(input.pct) >= 99.5); if (isOff) { return { status: 'Off', gain: gainStr, inputChannel: raw, detail: `MonitorRouter/Input Channel=${raw} (none)`, }; } return { status: 'On', gain: gainStr, inputChannel: raw, detail: `MonitorRouter/Input Channel=${raw}`, }; } export function normalizeCheckReport(metrics = [], meterSamples = {}) { const projectName = strVal(findParam(metrics, 'Project Info', 'Project Name')) || null; const deviceName = strVal(findParam(metrics, 'Project Info', 'AZM Name')) || null; const uptimeParam = findParam(metrics, 'System Info', 'Uptime'); const uptimeHours = uptimeParam?.val != null ? Number(uptimeParam.val) : null; const model = strVal(findParam(metrics, 'System Info', 'Model')) || null; const firmware = strVal(findParam(metrics, 'System Info', 'Firmware Version')) || null; const meterFor = (obj, param, x) => { const key = paramKey({ obj, param, x }); return meterSamples[key] || null; }; const sourceIndexes = indexSet(metrics, 'Link Manager', 'Source Defined', 'x'); const sources = []; for (const x of sourceIndexes) { const defined = strVal(findParam(metrics, 'Link Manager', 'Source Defined', 'x', x)); if (!isYes(defined)) continue; const name = strVal(findParam(metrics, 'Link Manager', 'Source Name', 'x', x)) || `Source ${x}`; const dspChans = strVal(findParam(metrics, 'Link Manager', 'Source DSP Chans', 'x', x)); const channel = dspChans ? String(dspChans).split(/[,\s]+/).filter(Boolean)[0] : x; const muteRaw = strVal(findParam(metrics, 'InGain', 'Mute', 'channel', channel)); const gainRaw = strVal(findParam(metrics, 'InGain', 'Gain', 'channel', channel)); const volume = meterFor('Link Manager', 'Source Meter', x); sources.push({ index: Number(x) || x, name, channel, muted: isMuted(muteRaw), db: gainRaw, volume, muteRaw, gainRaw, }); } const zoneIndexes = indexSet(metrics, 'Link Manager', 'Out Group Defined', 'x'); const sourceByIndex = new Map(sources.map(s => [String(s.index), s])); const zones = []; for (const x of zoneIndexes) { const defined = strVal(findParam(metrics, 'Link Manager', 'Out Group Defined', 'x', x)); if (!isYes(defined)) continue; const name = strVal(findParam(metrics, 'Link Manager', 'Out Group Name', 'x', x)) || `Zone ${x}`; const sourceIdx = strVal(findParam(metrics, 'Link Manager', 'Out Group Source', 'x', x)); const source = sourceByIndex.get(String(sourceIdx)); const muteRaw = strVal(findParam(metrics, 'Link Manager', 'Out Group Mute', 'x', x)); const gainRaw = strVal(findParam(metrics, 'Link Manager', 'Out Group Master Gain', 'x', x)); const volume = meterFor('Link Manager', 'Out Group Meter', x); zones.push({ index: Number(x) || x, name, sourceIndex: sourceIdx, sourceName: source?.name || (sourceIdx ? `Source ${sourceIdx}` : null), muted: isMuted(muteRaw), db: gainRaw, volume, muteRaw, gainRaw, }); } const deviceIndexes = indexSet(metrics, 'Control Wallplate', 'Name', 'device'); for (const d of indexSet(metrics, 'Control Wallplate', 'Online', 'device')) { if (!deviceIndexes.includes(d)) deviceIndexes.push(d); } deviceIndexes.sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)); const accessories = []; for (const device of deviceIndexes) { const name = strVal(findParam(metrics, 'Control Wallplate', 'Name', 'device', device)); const online = strVal(findParam(metrics, 'Control Wallplate', 'Online', 'device', device)); const type = strVal(findParam(metrics, 'Control Wallplate', 'Type', 'device', device)); if (!name && !online) continue; const typeNorm = String(type || '').trim().toLowerCase(); const nameNorm = String(name || '').trim(); if (typeNorm === 'none' || nameNorm === '0' || nameNorm === '') continue; accessories.push({ index: Number(device) || device, name: nameNorm, online: online || 'Unknown', type: type || null, }); } return { projectName, deviceName, model, firmware, uptimeHours: Number.isFinite(uptimeHours) ? uptimeHours : null, sources, zones, accessories, monitorSpeaker: resolveMonitorSpeaker(metrics), }; } export function formatUptime(hours) { if (hours == null || !Number.isFinite(hours)) return 'Unknown'; if (hours > 24) return `${(hours / 24).toFixed(1)} days`; return `${hours.toFixed(1)} hours`; } function muteIcon(muted) { if (muted === true) return '🔇'; if (muted === false) return '🔊'; return '❔'; } function volumeLabel(volume, fallbackDb) { if (volume && Number.isFinite(volume.avg)) { return `low ${volume.low.toFixed(1)} / avg ${volume.avg.toFixed(1)} / high ${volume.high.toFixed(1)} dB`; } if (fallbackDb == null || fallbackDb === '') return 'vol unknown'; const n = Number(fallbackDb); if (Number.isFinite(n)) return `gain ${n.toFixed(1)} dB`; return `gain ${fallbackDb} dB`; } /** Prefer live meter avg; fall back to InGain gain setpoint. */ export function getSourceLevelDb(source) { if (source?.volume && Number.isFinite(source.volume.avg)) { return source.volume.avg; } const n = Number(source?.db); return Number.isFinite(n) ? n : null; } /** Weak-signal band: between -80 dB and -70 dB inclusive. */ export function isLowSourceLevel(levelDb) { if (levelDb == null || !Number.isFinite(levelDb)) return false; return levelDb >= -80 && levelDb <= -70; } function formatSourceLine(s) { const level = getSourceLevelDb(s); const lowAlert = isLowSourceLevel(level); const vol = volumeLabel(s.volume, s.db); if (lowAlert) { return `• ⚠️ ${muteIcon(s.muted)} ${s.name} — ${vol} (low level: ${level.toFixed(1)} dB)`; } return `• ${muteIcon(s.muted)} ${s.name} — ${vol}`; } export function formatCheckMarkdown(report, { storeNumber, deviceStatus: status, monitorDisable } = {}) { const storeLabel = storeNumber != null ? `Store ${String(storeNumber).replace(/^0+/, '') || storeNumber}` : (report.projectName || 'Store'); const title = report.projectName ? `**${storeLabel}** — ${report.projectName}` : `**${storeLabel}**`; const lines = [ title, `Device: ${report.deviceName || 'Unknown'}` + (report.model ? ` (${report.model})` : '') + ` | Uptime: ${formatUptime(report.uptimeHours)}` + (status ? ` | Hub: ${status}` : ''), '', ]; const lowSourceCount = (report.sources || []).filter(s => isLowSourceLevel(getSourceLevelDb(s))).length; lines.push('**Sources**'); if (lowSourceCount > 0) { lines.push(`⚠️ ${lowSourceCount} source(s) with low input level (-80 to -70 dB)`); } if (!report.sources?.length) { lines.push('• _(none defined)_'); } else { for (const s of report.sources) { lines.push(formatSourceLine(s)); } } lines.push(''); lines.push('**Zones**'); if (!report.zones?.length) { lines.push('• _(none defined)_'); } else { for (const z of report.zones) { const src = z.sourceName || 'unassigned'; lines.push(`• ${muteIcon(z.muted)} ${z.name} — source ${src} — ${volumeLabel(z.volume, z.db)}`); } } lines.push(''); lines.push('**Accessories**'); const mon = report.monitorSpeaker; const monPresent = mon && mon.status !== 'Not present'; if (!report.accessories?.length && !monPresent) { lines.push('• _(none)_'); } else { for (const a of report.accessories || []) { const type = a.type ? ` (${a.type})` : ''; lines.push(`• ${a.name}${type} — ${a.online}`); } if (monPresent) { let monLine = `• Monitor speaker — ${mon.status}`; if (monitorDisable?.attempted) { if (monitorDisable.ok) { const was = monitorDisable.previousChannel || 'On'; monLine = `• Monitor speaker — Off (auto-disabled; was listening to **${was}**)`; } else { if (mon.status === 'On' && mon.gain != null && mon.gain !== '') { const g = Number(mon.gain); monLine += Number.isFinite(g) ? ` (${g.toFixed(1)} dB)` : ` (${mon.gain} dB)`; } monLine += ` ⚠️ auto-disable failed${monitorDisable.error ? `: ${monitorDisable.error}` : ''}`; } } else if (mon.status === 'On' && mon.gain != null && mon.gain !== '') { const g = Number(mon.gain); monLine += Number.isFinite(g) ? ` (${g.toFixed(1)} dB)` : ` (${mon.gain} dB)`; } lines.push(monLine); } } return lines.join('\n'); } /** * Turn off the AZM monitor speaker (MonitorRouter → Input Channel = none). * Opens a short-lived /ws session: auth → device login → set → verify. */ export function disableMonitorSpeaker(wsUrl, { cookie, origin, sessionName = 'monitor-off', timeoutMs = 25000, verifyMs = 1500, username, password, login = true, } = {}) { return new Promise((resolve, reject) => { if (!cookie) { reject(new Error('cookie (xyte_auth) is required')); return; } const envCreds = getDeviceCredentials(); const userName = username || envCreds?.userName; const pass = password || envCreds?.password; const doLogin = login && userName && pass; const headers = { Cookie: cookie }; if (origin) headers.Origin = origin; const ws = new WebSocket(wsUrl, { headers }); const byKey = new Map(); let phase = 'challenge'; let settled = false; let collectTimer = null; const finish = (err, result) => { if (settled) return; settled = true; clearTimeout(timer); if (collectTimer) clearTimeout(collectTimer); try { ws.close(); } catch { /* ignore */ } if (err) reject(err); else resolve(result); }; const completeVerify = () => { const monitorSpeaker = resolveMonitorSpeaker([...byKey.values()]); const ok = monitorSpeaker.status === 'Off'; finish(null, { ok, monitorSpeaker, metrics: [...byKey.values()], error: ok ? null : `Monitor still ${monitorSpeaker.status}`, }); }; const timer = setTimeout(() => { if (phase === 'verify' && byKey.size) { completeVerify(); return; } if (phase === 'device-login') { finish(new Error( `Device login timed out for user "${userName}" (no successful User Accounts/Validate response)`, )); } else { finish(new Error(`AZM WS timeout in phase=${phase}`)); } }, timeoutMs); const send = (obj) => ws.send(JSON.stringify(obj)); const startDisable = () => { phase = 'disable'; send({ jsonrpc: '2.0', method: 'set', params: [MONITOR_OFF_SET] }); phase = 'verify'; setTimeout(() => { send({ jsonrpc: '2.0', method: 'subscribe', params: MONITOR_SPEAKER_GETS.map(p => ({ ...p, sub: 1 })), }); collectTimer = setTimeout(completeVerify, verifyMs); }, 300); }; const startDeviceLogin = () => { phase = 'device-login'; const encrypted = encryptUserCredentials(userName, pass); send({ jsonrpc: '2.0', method: 'set', params: [{ obj: 'User Accounts', param: 'Validate', str: encrypted }], }); setTimeout(() => { if (phase === 'device-login') { send({ jsonrpc: '2.0', method: 'get', params: [{ obj: 'User Accounts', param: 'Validate' }], }); } }, 400); }; ws.on('open', () => { send({ jsonrpc: '2.0', method: 'auth', params: {} }); }); ws.on('message', (buf) => { let msg; try { msg = JSON.parse(buf.toString()); } catch (err) { finish(err); return; } if (phase === 'challenge' && msg.method === 'authResp') { const challenge = msg.params?.resp; if (!challenge || challenge === 'Welcome!') { finish(new Error(`Unexpected auth challenge: ${JSON.stringify(msg.params)}`)); return; } phase = 'welcome'; send({ jsonrpc: '2.0', method: 'auth', params: { reply: computeAuthReply(challenge) }, }); return; } if (phase === 'welcome' && msg.method === 'authResp') { if (msg.params?.resp !== 'Welcome!') { finish(new Error(`Auth rejected: ${JSON.stringify(msg.params)}`)); return; } send({ jsonrpc: '2.0', method: 'setSessionName', params: { name: sessionName } }); if (doLogin) startDeviceLogin(); else startDisable(); return; } if (phase === 'device-login' && msg.method === 'paramUpdate') { const params = Array.isArray(msg.params) ? msg.params : [msg.params]; const validate = params.find(p => p?.obj === 'User Accounts' && p?.param === 'Validate'); if (!validate) return; if (!validate.str || validate.str === 'Not Found') return; startDisable(); return; } if (phase === 'verify' && msg.method === 'paramUpdate') { const params = Array.isArray(msg.params) ? msg.params : [msg.params]; for (const p of params) { if (p?.obj === 'MonitorRouter') upsertMetric(byKey, p); } } }); ws.on('error', (err) => finish(err)); ws.on('close', (code, reason) => { if (!settled && phase === 'verify' && byKey.size) { completeVerify(); return; } if (!settled && phase !== 'verify') { finish(new Error(`WS closed early code=${code} reason=${reason?.toString?.() || ''} phase=${phase}`)); } }); }); } export async function fetchAzmCheckReport(wsUrl, opts = {}) { const result = await fetchAzmMetrics(wsUrl, { ...opts, sessionName: opts.sessionName || 'check', gets: opts.gets || CHECK_GETS, meterSubscribes: opts.meterSubscribes ?? METER_SUBSCRIBES, meterSampleMs: opts.meterSampleMs ?? 3000, followUpGets: opts.followUpGets ?? MONITOR_SPEAKER_GETS, followUpCollectMs: opts.followUpCollectMs ?? 1500, collectMs: opts.collectMs ?? 4000, timeoutMs: opts.timeoutMs ?? 45000, }); const report = normalizeCheckReport(result.metrics, result.meterSamples || {}); let monitorDisable = null; let finalReport = report; if (report.monitorSpeaker?.status === 'On' && opts.autoDisableMonitor !== false) { monitorDisable = { attempted: true, previousChannel: report.monitorSpeaker.inputChannel, previousGain: report.monitorSpeaker.gain, }; try { const off = await disableMonitorSpeaker(wsUrl, opts); monitorDisable.ok = off.ok; monitorDisable.error = off.error; if (off.ok) { finalReport = { ...report, monitorSpeaker: off.monitorSpeaker }; } } catch (err) { monitorDisable.ok = false; monitorDisable.error = err.message || String(err); } } return { ...result, report: finalReport, monitorDisable, markdown: formatCheckMarkdown(finalReport, { storeNumber: opts.storeNumber, deviceStatus: opts.deviceStatus, monitorDisable, }), }; }