// services/enrichment/optisignsMatcher.js // // Shared OptiSigns device matching and enrichment helpers for AV. // This centralizes the logic previously duplicated in deviceService, avDeviceBuilder, and avEnricher. // // - findBestOptiSignsMatch: unified finder (prefers normalized exact match for consistency, // with loose contains fallback for compatibility). // - createOptiSignsDisplay: produces the slim {content, lastHeartBeat, isOld} shape used by chat (/avstatus) and HTML. // - createRichOptiSigns: produces the rich shape used by /build and modals (spreads raw + resolved names). // // Uses normalizePlayerName from normalizers and the resolution helpers from the OptiSigns client. // Extracted as part of Option A incremental consolidation (after RED). import { normalizePlayerName } from './normalizers.js'; import { getPlaylistName, getAssetName } from '../../integrations/optisigns/client.js'; /** * Find the best matching OptiSigns device for a given AV/MDM identifier (e.g. friendlyName or device.identifier * containing VW/LED). * * @param {string} identifier * @param {Array} optiList - list of raw devices from getOptiSignStatus().devices * @returns {Object|null} the matching raw OptiSigns device or null */ export function findBestOptiSignsMatch(identifier, optiList) { if (!identifier || !Array.isArray(optiList) || optiList.length === 0) { return null; } const normId = normalizePlayerName(identifier).toLowerCase().trim(); if (!normId) return null; // Strategy 1: Exact normalized match on deviceName (preferred; matches deviceService and avEnricher logic) let match = optiList.find(o => normalizePlayerName(o.deviceName || '').toLowerCase().trim() === normId ); if (match) { return match; } // Strategy 2: Loose contains fallback using original identifier (preserves builder's previous heuristic // for cases with extra text in names; uses upper for broad match) const idUpper = identifier.toUpperCase().trim(); if (idUpper) { match = optiList.find(d => (d.deviceName || '').toUpperCase().includes(idUpper) ); if (match) { return match; } } return null; } /** * Build the slim OptiSigns display object used by chat commands (/avstatus) and HTML views. * (Logic moved from deviceService for sharing.) */ export function createOptiSignsDisplay(rawOpti) { if (!rawOpti) return null; let content = 'Idle'; if (rawOpti.currentType === 'playlist' && rawOpti.currentPlaylistId) { content = `Playing "${getPlaylistName(rawOpti.currentPlaylistId)}"`; } else if (rawOpti.currentType === 'asset' && rawOpti.currentAssetId) { content = `Showing "${getAssetName(rawOpti.currentAssetId)}"`; } else if (rawOpti.currentType) { content = rawOpti.currentType; } const hoursSinceHeartbeat = rawOpti.lastHeartBeat ? (Date.now() - new Date(rawOpti.lastHeartBeat).getTime()) / (1000 * 60 * 60) : 999; return { content, lastHeartBeat: rawOpti.lastHeartBeat, isOld: hoursSinceHeartbeat > 24 }; } /** * Build the rich OptiSigns object used by the build endpoint and modals. * Spreads the raw + resolves current names/ids for display (logic moved from avDeviceBuilder). */ export function createRichOptiSigns(rawOpti) { if (!rawOpti) return null; return { ...rawOpti, currentPlaylistName: getPlaylistName(rawOpti.currentPlaylistId), currentAssetName: rawOpti.currentAssetId ? getAssetName(rawOpti.currentAssetId) : null, currentPlaylistId: rawOpti.currentPlaylistId, currentAssetId: rawOpti.currentAssetId }; }