UNPKG

homebridge-eufy-security

Version:
297 lines (256 loc) 10.8 kB
/** * Unsupported Device Detail View — shows device info dump and guided CTA * for requesting support via GitHub. */ // eslint-disable-next-line no-unused-vars const UnsupportedDetailView = { _container: null, /** * @param {HTMLElement} container * @param {string} id - uniqueId / serial number */ async render(container, id) { this._container = container; container.innerHTML = ''; const accessory = this._findAccessory(id); if (!accessory) { container.innerHTML = ` <div class="text-center text-muted py-5"> <p>Device not found.</p> <button class="btn btn-outline-secondary btn-sm" id="btn-back">Back to Dashboard</button> </div>`; container.querySelector('#btn-back').addEventListener('click', () => App.navigate('dashboard')); return; } // Header (no image for unsupported) this._renderHeader(container, accessory); // Main content const content = document.createElement('div'); await this._renderDetail(content, accessory); container.appendChild(content); }, // ===== Header ===== _renderHeader(container, accessory) { const header = document.createElement('div'); header.className = 'detail-header'; const backBtn = document.createElement('button'); backBtn.className = 'btn btn-link p-0'; backBtn.innerHTML = '← Back'; backBtn.style.textDecoration = 'none'; backBtn.addEventListener('click', () => App.navigate('dashboard')); const info = document.createElement('div'); info.className = 'detail-header__info'; info.innerHTML = ` <h5>${Helpers.escHtml(accessory.displayName)}</h5> <small>${accessory.typename || ('Type ' + accessory.type)} · ${accessory.uniqueId}</small> `; header.appendChild(backBtn); header.appendChild(info); container.appendChild(header); }, // ===== Detail Content ===== async _renderDetail(content, accessory) { const REPO = 'homebridge-plugins/homebridge-eufy-security'; const LABEL = 'device-support'; const COMPAT_URL = 'https://bropat.github.io/eufy-security-client/#/supported_devices'; const section = document.createElement('div'); section.className = 'detail-section unsupported-detail'; // Description const desc = document.createElement('p'); desc.className = 'text-muted'; desc.innerHTML = '<strong>' + Helpers.iconHtml('info.svg') + ' This device was detected but is not yet supported.</strong><br />' + 'New device support must first be added to the eufy-security-client library. ' + 'If your device is not on the compatibility list, please <strong>search for existing issues first</strong> before opening a new one.'; section.appendChild(desc); // Device info dump — all available data in one box const props = accessory.properties || {}; // Load raw intel from unsupported.json (served via /unsupportedDevices endpoint) let unsupportedIntel = null; try { const unsupportedData = await Api.loadUnsupportedDevices(); const entries = unsupportedData.devices || []; unsupportedIntel = entries.find(e => e.uniqueId === accessory.uniqueId) || null; } catch (e) { // ignore — will fall back to basic properties } const deviceInfo = { uniqueId: accessory.uniqueId, displayName: accessory.displayName, type: accessory.type, typename: accessory.typename || undefined, ...props, ...(unsupportedIntel ? { rawDevice: unsupportedIntel.rawDevice, rawProperties: unsupportedIntel.rawProperties, } : {}), }; // Remove potentially large/sensitive fields delete deviceInfo.picture; // CTA stepper + buttons const stepsWrap = document.createElement('div'); stepsWrap.className = 'unsupported-detail__steps'; // Stepper frieze const stepper = document.createElement('div'); stepper.className = 'unsupported-stepper'; const steps = [ { num: '1', label: 'Check', color: 'success' }, { num: '2', label: 'Search', color: 'primary' }, { num: '3', label: 'Copy', color: 'warning' }, { num: '4', label: 'Create', color: 'danger' }, ]; steps.forEach((step, i) => { const stepEl = document.createElement('div'); stepEl.className = 'unsupported-stepper__step'; stepEl.innerHTML = `<span class="unsupported-stepper__circle unsupported-stepper__circle--${step.color}">${step.num}</span><span class="unsupported-stepper__label">${step.label}</span>`; stepper.appendChild(stepEl); if (i < steps.length - 1) { const line = document.createElement('div'); line.className = 'unsupported-stepper__line'; stepper.appendChild(line); } }); stepsWrap.appendChild(stepper); // Buttons row const btnGroup = document.createElement('div'); btnGroup.className = 'unsupported-detail__actions'; // 1) Check compatibility list const compatBtn = document.createElement('a'); compatBtn.href = COMPAT_URL; compatBtn.target = '_blank'; compatBtn.rel = 'noopener noreferrer'; compatBtn.className = 'btn btn-success'; compatBtn.textContent = 'Check Compatibility ↗'; btnGroup.appendChild(compatBtn); // 2) Search existing issues with label const searchQuery = encodeURIComponent(`is:issue label:${LABEL} ${accessory.type}`); const searchBtn = document.createElement('a'); searchBtn.href = `https://github.com/${REPO}/issues?q=${searchQuery}`; searchBtn.target = '_blank'; searchBtn.rel = 'noopener noreferrer'; searchBtn.className = 'btn btn-outline-primary'; searchBtn.textContent = 'Search Existing Issues ↗'; btnGroup.appendChild(searchBtn); // 3) Copy device JSON to clipboard const deviceDump = JSON.stringify(deviceInfo, null, 2); const copyJsonBtn = document.createElement('button'); copyJsonBtn.className = 'btn btn-outline-warning'; copyJsonBtn.textContent = 'Copy Device Info'; copyJsonBtn.addEventListener('click', () => { this._copyToClipboard(deviceDump).then(() => { copyJsonBtn.textContent = '✓ Copied!'; setTimeout(() => { copyJsonBtn.textContent = 'Copy Device Info'; }, 2000); }); }); btnGroup.appendChild(copyJsonBtn); // 4) Create new issue (without embedding JSON — user pastes it) const model = props.model || accessory.type; const issueTitle = encodeURIComponent(`[Device Support] ${model} (Type ${accessory.type})`); const templateParams = [ `template=device_support.yml`, `title=${issueTitle}`, `labels=${LABEL}`, ].join('&'); const createBtn = document.createElement('a'); createBtn.href = `https://github.com/${REPO}/issues/new?${templateParams}`; createBtn.target = '_blank'; createBtn.rel = 'noopener noreferrer'; createBtn.className = 'btn btn-outline-danger'; createBtn.textContent = 'Create an Issue ↗'; btnGroup.appendChild(createBtn); stepsWrap.appendChild(btnGroup); // Paste reminder note const pasteNote = document.createElement('p'); pasteNote.className = 'unsupported-detail__paste-note'; pasteNote.innerHTML = Helpers.iconHtml('info.svg', 14) + ' Copy the <strong>Device Information</strong> below first, then paste it into the issue form on GitHub.'; stepsWrap.appendChild(pasteNote); section.appendChild(stepsWrap); // External links note const extNote = document.createElement('p'); extNote.className = 'text-muted'; extNote.style.cssText = 'font-size: 0.75rem; text-align: right;'; extNote.textContent = '↗ These links open in a new browser tab on GitHub.'; section.appendChild(extNote); const infoTitle = document.createElement('div'); infoTitle.className = 'detail-section__title'; infoTitle.textContent = 'Device Information'; section.appendChild(infoTitle); const infoNote = document.createElement('p'); infoNote.className = 'text-muted'; infoNote.style.cssText = 'font-size: 0.75rem;'; infoNote.textContent = 'Diagnostic information below includes raw device properties from the client library.'; section.appendChild(infoNote); // JSON dump with copy button overlay const dumpWrap = document.createElement('div'); dumpWrap.className = 'unsupported-detail__dump-wrap'; const copyBtn = document.createElement('button'); copyBtn.className = 'unsupported-detail__copy-btn'; const _setCopyLabel = (text) => { copyBtn.innerHTML = ''; copyBtn.appendChild(Helpers.icon('copy.svg')); copyBtn.append(' ' + text); }; _setCopyLabel('Copy'); copyBtn.addEventListener('click', () => { this._copyToClipboard(pre.textContent).then(() => { copyBtn.textContent = '✓ Copied!'; setTimeout(() => { _setCopyLabel('Copy'); }, 2000); }); }); const pre = document.createElement('pre'); pre.className = 'unsupported-detail__dump'; pre.textContent = JSON.stringify(deviceInfo, null, 2); dumpWrap.appendChild(copyBtn); dumpWrap.appendChild(pre); section.appendChild(dumpWrap); content.appendChild(section); }, // ===== Helpers ===== /** * Copy text to clipboard with fallback for non-secure contexts (e.g. Homebridge iframe). * @param {string} text * @returns {Promise<void>} */ _copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text).catch(() => this._copyFallback(text)); } return this._copyFallback(text); }, _copyFallback(text) { return new Promise((resolve, reject) => { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed')); } catch (err) { reject(err); } finally { document.body.removeChild(textarea); } }); }, /** * Find an accessory (device or station) by uniqueId across all stations. * For standalone devices the station and device share the same uniqueId — * prefer the device because it carries richer data. * @param {string} id * @returns {object|null} */ _findAccessory(id) { const stations = App.state.stations || []; for (const s of stations) { // Check devices first — standalone devices have the same uniqueId as their station // but carry richer data for (const d of s.devices || []) { if (d.uniqueId === id) return d; } if (s.uniqueId === id) return s; } return null; }, };