// src/integrations/meraki/devices.js import { merakiAxios } from './client.js'; import { logger } from '../../utils/logger.js'; /** * Get simple list of all devices in a Meraki network (switches + APs) */ export async function getAllMerakiDevices(networkId) { if (!networkId) return []; try { const res = await merakiAxios.get(`/networks/${networkId}/devices`); return res.data || []; } catch (err) { logger('meraki:devices', `Failed to fetch device list for network ${networkId}: ${err.message}`, 'warn'); return []; } } /** * Get full details for a single Meraki device (switch or AP) */ export async function getMerakiDeviceDetail(serial) { try { const res = await merakiAxios.get(`/devices/${serial}`); return res.data; } catch (err) { logger('meraki:devices', `Failed to get detail for device ${serial}: ${err.message}`, 'warn'); return { serial, error: err.message }; } } /** * Get wireless status for an AP (Tx power, channels, client counts, etc.) * Returns null gracefully for non-AP devices (switches) */ export async function getMerakiWirelessStatus(serial) { try { const res = await merakiAxios.get(`/devices/${serial}/wireless/status`); return res.data; } catch (err) { // 404 is expected for switches — treat as normal if (err.response?.status === 404) { return null; } logger('meraki:devices', `Wireless status failed for ${serial}: ${err.message}`, 'debug'); return null; } } /** * Enrich a set of referenced Meraki devices * Returns map: serial → full device object + wirelessStatus (for APs) */ export async function enrichMerakiDevices(networkId, referencedSerials) { if (!networkId || !referencedSerials?.size) return {}; const detailsMap = {}; logger('meraki:devices', `Enriching ${referencedSerials.size} Meraki devices with full details + wireless status`); for (const serial of referencedSerials) { try { const [basicRes, wirelessRes] = await Promise.allSettled([ getMerakiDeviceDetail(serial), getMerakiWirelessStatus(serial) ]); const fullDevice = basicRes.status === 'fulfilled' ? basicRes.value : { serial }; if (wirelessRes.status === 'fulfilled' && wirelessRes.value) { fullDevice.wirelessStatus = wirelessRes.value; } detailsMap[serial] = fullDevice; } catch (err) { logger('meraki:devices', `Failed enriching device ${serial}: ${err.message}`, 'warn'); detailsMap[serial] = { serial, error: err.message }; } } return detailsMap; } /** * Get recent signal quality (RSSI + SNR) for a wireless client * Uses the exact endpoint and parameters you provided (1-hour resolution) */ export async function getWirelessClientSignalQuality(networkId, clientId) { if (!networkId || !clientId) return { rssi: null, snr: null }; try { const timespan = 3600; // 1 hour for near-realtime const url = `/networks/${networkId}/wireless/signalQualityHistory?clientId=${clientId}×pan=${timespan}&perPage=1&resolution=3600`; const response = await merakiAxios.get(url); const history = response.data || []; const latest = history.length > 0 ? history[history.length - 1] : null; return { rssi: latest?.rssi ?? null, // e.g. -45 snr: latest?.snr ?? null // e.g. 50 }; } catch (err) { logger('meraki:devices', `Signal quality failed for client ${clientId}: ${err.message}`, 'debug'); return { rssi: null, snr: null }; } } /** * Get recent average latency for a wireless client */ export async function getWirelessClientLatency(networkId, clientId) { if (!networkId || !clientId) return { avgLatencyMs: null }; try { const timespan = 3600; // 1 hour const url = `/networks/${networkId}/wireless/latencyHistory?clientId=${clientId}×pan=${timespan}&perPage=1&resolution=3600`; const response = await merakiAxios.get(url); const history = response.data || []; const latest = history.length > 0 ? history[history.length - 1] : null; return { avgLatencyMs: latest?.avgLatencyMs ?? null }; } catch (err) { logger('meraki:devices', `Latency history failed for client ${clientId}: ${err.message}`, 'debug'); return { avgLatencyMs: null }; } } /** * Get failed connection attempts for a client (last 7 days) * Returns empty array if none */ export async function getWirelessClientFailedConnections(networkId, clientId) { if (!networkId || !clientId) return []; try { const timespan = 604800; // 7 days const url = `/networks/${networkId}/wireless/failedConnections?clientId=${clientId}×pan=${timespan}`; const response = await merakiAxios.get(url); return response.data || []; } catch (err) { logger('meraki:devices', `Failed connections query failed for client ${clientId}: ${err.message}`, 'debug'); return []; } } /** * Get Link Layer Topology for the network * This is the endpoint that returns nodes + links suitable for Mermaid diagrams */ export async function getMerakiTopology(networkId) { if (!networkId) { return { nodes: [], links: [], errors: ["No networkId provided"] }; } try { logger('meraki:topology', `Fetching linkLayer topology for network ${networkId}`); const response = await merakiAxios.get(`/networks/${networkId}/topology/linkLayer`); const topology = response.data || { nodes: [], links: [], errors: [] }; logger('meraki:topology', `Received ${topology.nodes?.length || 0} nodes and ${topology.links?.length || 0} links`); return topology; } catch (err) { logger('meraki:topology', `Failed to fetch topology for ${networkId}: ${err.message}`, 'warn'); return { nodes: [], links: [], errors: [err.message] }; } } export default { getAllMerakiDevices, getMerakiDeviceDetail, getMerakiWirelessStatus, enrichMerakiDevices, getWirelessClientSignalQuality, getWirelessClientLatency, getWirelessClientFailedConnections, getMerakiTopology };