From 3bcc427683925b14390814d862ca12f358e1867b Mon Sep 17 00:00:00 2001 From: SRS IT Date: Sat, 28 Mar 2026 20:25:12 -0400 Subject: [PATCH] feat: add Import/Export for DWM rules in Homebridge UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export: - '⬇ Export' button downloads all rules as dwm-rules-YYYY-MM-DD.json - Compatible with desktop app rule format Import: - '⬆ Import' button opens file picker (accepts .json) - Preview panel shows rule names + types before committing - Merge mode: adds rules, skips any whose name already exists - Replace mode: deletes all current rules then imports - Server strips imported IDs/timestamps — fresh ones are assigned - Reports imported/skipped count on completion Co-Authored-By: Claude Sonnet 4.6 --- .../homebridge-ui/public/index.html | 23 +++++ .../homebridge-ui/public/index.js | 91 +++++++++++++++++++ .../homebridge-plugin/homebridge-ui/server.js | 29 ++++++ 3 files changed, 143 insertions(+) diff --git a/packages/homebridge-plugin/homebridge-ui/public/index.html b/packages/homebridge-plugin/homebridge-ui/public/index.html index b49c624..71be028 100644 --- a/packages/homebridge-plugin/homebridge-ui/public/index.html +++ b/packages/homebridge-plugin/homebridge-ui/public/index.html @@ -220,9 +220,32 @@

DWM Automation Rules

+ + +
+ + +
diff --git a/packages/homebridge-plugin/homebridge-ui/public/index.js b/packages/homebridge-plugin/homebridge-ui/public/index.js index 8685a29..9fa8517 100644 --- a/packages/homebridge-plugin/homebridge-ui/public/index.js +++ b/packages/homebridge-plugin/homebridge-ui/public/index.js @@ -274,6 +274,97 @@ function deleteDwmRule(id) { setTimeout(() => row.remove(), 5000); } +// ── Export ──────────────────────────────────────────────────────────────────── + +document.getElementById('btn-export-dwm').addEventListener('click', async () => { + try { + const rules = await homebridge.request('/rules/export'); + if (!rules || !rules.length) { showStatus('dwm-rules-status', 'No rules to export.', 'warn'); return; } + const blob = new Blob([JSON.stringify(rules, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const date = new Date().toISOString().slice(0, 10); + a.href = url; + a.download = `dwm-rules-${date}.json`; + a.click(); + URL.revokeObjectURL(url); + showStatus('dwm-rules-status', `Exported ${rules.length} rule${rules.length !== 1 ? 's' : ''}.`, 'success'); + } catch (e) { + showStatus('dwm-rules-status', 'Export failed: ' + e.message, 'error'); + } +}); + +// ── Import ──────────────────────────────────────────────────────────────────── + +let _importRules = []; // parsed rules waiting for confirmation + +document.getElementById('btn-import-dwm').addEventListener('click', () => { + document.getElementById('dwm-import-file').value = ''; // reset so same file can be re-selected + document.getElementById('dwm-import-file').click(); +}); + +document.getElementById('dwm-import-file').addEventListener('change', (e) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (ev) => { + try { + const parsed = JSON.parse(ev.target.result); + const rules = Array.isArray(parsed) ? parsed : parsed.rules ?? []; + if (!rules.length) throw new Error('No rules found in file'); + + _importRules = rules; + document.getElementById('dwm-import-title').textContent = + `Import ${rules.length} rule${rules.length !== 1 ? 's' : ''} from "${file.name}"`; + + // Build preview list + const listEl = document.getElementById('dwm-import-list'); + listEl.innerHTML = rules.map((r) => + `
` + + `${esc(r.name ?? '(unnamed)')} ` + + `${esc(r.type ?? '')}
` + ).join(''); + + document.getElementById('dwm-import-status').textContent = ''; + document.getElementById('dwm-import-panel').style.display = ''; + document.getElementById('btn-import-confirm').disabled = false; + } catch (err) { + showStatus('dwm-rules-status', 'Import failed: ' + err.message, 'error'); + } + }; + reader.readAsText(file); +}); + +document.getElementById('btn-import-cancel').addEventListener('click', () => { + document.getElementById('dwm-import-panel').style.display = 'none'; + _importRules = []; +}); + +document.getElementById('btn-import-confirm').addEventListener('click', async () => { + if (!_importRules.length) return; + const mode = document.querySelector('input[name="dwm-import-mode"]:checked')?.value ?? 'merge'; + const statusEl = document.getElementById('dwm-import-status'); + const btn = document.getElementById('btn-import-confirm'); + btn.disabled = true; + statusEl.style.color = '#9ca3af'; + statusEl.textContent = 'Importing…'; + try { + const res = await homebridge.request('/rules/import', { rules: _importRules, mode }); + document.getElementById('dwm-import-panel').style.display = 'none'; + _importRules = []; + await loadDwmRules(); + const msg = mode === 'replace' + ? `Replaced all rules — imported ${res.imported}.` + : `Imported ${res.imported} rule${res.imported !== 1 ? 's' : ''}${res.skipped ? `, skipped ${res.skipped} (name already exists)` : ''}.`; + showStatus('dwm-rules-status', msg, 'success'); + } catch (e) { + btn.disabled = false; + statusEl.style.color = '#fca5a5'; + statusEl.textContent = 'Import failed: ' + e.message; + } +}); + // ── Sun-time helpers ───────────────────────────────────────────────────────── function secsToAmPm(secs) { diff --git a/packages/homebridge-plugin/homebridge-ui/server.js b/packages/homebridge-plugin/homebridge-ui/server.js index 21350f2..e2ac639 100644 --- a/packages/homebridge-plugin/homebridge-ui/server.js +++ b/packages/homebridge-plugin/homebridge-ui/server.js @@ -84,6 +84,35 @@ class DibbyWemoUiServer extends HomebridgePluginUiServer { return { ok: true }; }); + this.onRequest('/rules/export', async () => { + return this._store.getDwmRules(); + }); + + this.onRequest('/rules/import', async ({ rules, mode }) => { + if (!Array.isArray(rules) || rules.length === 0) throw new Error('No valid rules found in import data'); + + if (mode === 'replace') { + for (const r of this._store.getDwmRules()) this._store.deleteDwmRule(r.id); + } + + const existing = this._store.getDwmRules(); + const existingNames = new Set(existing.map((r) => (r.name ?? '').toLowerCase())); + let imported = 0, skipped = 0; + + for (const rule of rules) { + // Strip old identity fields — store will assign fresh id + timestamps + const { id: _id, createdAt: _ca, updatedAt: _ua, ...ruleData } = rule; + if (mode === 'merge' && existingNames.has((ruleData.name ?? '').toLowerCase())) { + skipped++; + continue; + } + this._store.createDwmRule(ruleData); + imported++; + } + + return { ok: true, imported, skipped }; + }); + // ── Scheduler heartbeat ─────────────────────────────────────────────────── this.onRequest('/scheduler/status', async () => { const hb = this._store.getHeartbeat();