diff --git a/public/css/portal.css b/public/css/portal.css index 3edd74b6..fb1b44d9 100644 --- a/public/css/portal.css +++ b/public/css/portal.css @@ -395,6 +395,17 @@ body::before{ .midea-login-hint a:hover{filter:brightness(1.05)} .midea-msg{font-size:13px;color:var(--muted);padding:10px 0;line-height:1.5} +/* ============================================================ + SMART HOME WIDGET + ============================================================ */ +.c-sh-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px} +.c-sh-card{background:var(--surface-2,#16212e);border:1px solid var(--line,rgba(255,255,255,.08));border-radius:12px;padding:12px} +.c-sh-name{font-weight:600;font-size:14px;margin-bottom:10px} +.c-sh-sw{border:1px solid var(--line-2,rgba(255,255,255,.14));border-radius:8px;padding:6px 12px;cursor:pointer;background:var(--surface-3,#1b2836);color:inherit} +.c-sh-sw.on{background:linear-gradient(145deg,var(--green,#4ade80),#15924f);border-color:transparent;color:#fff} +.c-sh-bri{width:100%;margin-top:10px} +.c-sh-msg{margin-top:10px;font-size:13px;color:var(--muted,#90a1b3)} + /* ============================================================ REDUCED MOTION ============================================================ */ diff --git a/public/css/smarthome.css b/public/css/smarthome.css index 0c136ab5..a75d3a13 100644 --- a/public/css/smarthome.css +++ b/public/css/smarthome.css @@ -26,3 +26,10 @@ input[type=range].sh-bri::-webkit-slider-thumb{-webkit-appearance:none;width:19p .sh-sensorval{font-weight:800;font-size:26px;margin-top:10px} .sh-empty{color:var(--faint,#5f6f7e);font-size:13px;padding:20px 0} #sh-test-result{font-size:13px;font-weight:600;padding:8px 12px;border-radius:8px;margin-bottom:12px} +.sh-owner-sub{font-size:12px;color:var(--muted,#90a1b3);margin-bottom:10px} +.sh-owner-list{margin-top:10px;max-height:300px;overflow:auto;display:flex;flex-direction:column;gap:4px} +.sh-owner-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;cursor:pointer} +.sh-owner-row:hover{background:var(--surface-3,#1b2836)} +.sh-owner-row input{width:16px;height:16px} +.sh-owner-btn{margin-top:10px;font-size:12px;color:var(--accent,#8b9cff);background:none;border:none;cursor:pointer;padding:0;display:flex;align-items:center;gap:6px} +.sh-owner-chips{font-size:12px;color:var(--muted,#90a1b3);margin-top:6px} diff --git a/public/js/portal.js b/public/js/portal.js index 0e51581b..bac659f6 100644 --- a/public/js/portal.js +++ b/public/js/portal.js @@ -678,6 +678,52 @@ if (patch) mideaControl(cardEl, patch); } + // ─── Smart Home widget ────────────────────────────────────────────────────── + function shControl(id, patch, card) { + fetch('/api/v1/portal/smarthome/' + Number(id) + '/state', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ patch: patch }) + }).then(function (r) { return r.json(); }).then(function (j) { + if (j && j.reason === 'login_required') { showSmarthomeLogin(card); } + }).catch(function () {}); + } + function showSmarthomeLogin(card) { + var msg = document.getElementById('smarthomeMsg'); + if (msg) { msg.style.display = 'block'; msg.textContent = PT.smarthomeLoginToControl || 'Login required'; } + } + function renderSmarthomeCard(d) { + var el = document.createElement('div'); el.className = 'c-sh-card'; + var st = d.state || {}, caps = d.capabilities || {}; + var name = document.createElement('div'); name.className = 'c-sh-name'; name.textContent = d.name || ''; el.appendChild(name); + if (d.kind === 'scene') { + var b = document.createElement('button'); b.className = 'btn btn-sm'; b.textContent = PT.smarthomeActivate || 'Activate'; + b.addEventListener('click', function () { shControl(d.id, {}, el); }); + el.appendChild(b); return el; + } + var sw = document.createElement('button'); sw.className = 'c-sh-sw' + (st.on ? ' on' : ''); + sw.textContent = PT.smarthomePower || 'Power'; + sw.addEventListener('click', function () { var on = !sw.classList.contains('on'); sw.classList.toggle('on', on); shControl(d.id, { on: on }, el); }); + el.appendChild(sw); + if (caps.bri) { + var range = document.createElement('input'); range.type = 'range'; range.min = 0; range.max = 100; range.value = (st.bri != null ? st.bri : 0); range.className = 'c-sh-bri'; + range.addEventListener('change', function () { shControl(d.id, { bri: Number(range.value) }, el); }); + el.appendChild(range); + } + return el; + } + function hydrateSmarthome() { + var card = document.querySelector('.c-smarthome'); + if (!card) return; + fetch('/api/v1/portal/smarthome').then(function (r) { return r.json(); }).then(function (j) { + var list = document.getElementById('smarthome-list'); if (!list) return; + list.innerHTML = ''; + if (!j || !j.data || !j.data.devices || !j.data.devices.length) { card.style.display = 'none'; return; } + card.style.display = ''; + j.data.devices.forEach(function (d) { list.appendChild(renderSmarthomeCard(d)); }); + }).catch(function () { card.style.display = 'none'; }); + } + function hydrateMidea() { var card = document.querySelector('.c-midea'); if (!card) return; @@ -737,5 +783,6 @@ hydrateServices(); hydratePihole(); hydrateMidea(); + hydrateSmarthome(); })(); diff --git a/public/js/settings.js b/public/js/settings.js index 7b72db4e..32bbb90b 100644 --- a/public/js/settings.js +++ b/public/js/settings.js @@ -1770,11 +1770,12 @@ var widgetServices = document.getElementById('portal-widget-services'); var widgetPihole = document.getElementById('portal-widget-pihole'); var widgetMidea = document.getElementById('portal-widget-midea'); + var widgetSmarthome = document.getElementById('portal-widget-smarthome'); var trustToggle = document.getElementById('portal-trust-owner-mapping'); var autoappearToggle = document.getElementById('portal-autoappear'); if (!enabledToggle) return; - [enabledToggle, widgetDevice, widgetTraffic, widgetServices, widgetPihole, widgetMidea, trustToggle, autoappearToggle].forEach(function (el) { + [enabledToggle, widgetDevice, widgetTraffic, widgetServices, widgetPihole, widgetMidea, widgetSmarthome, trustToggle, autoappearToggle].forEach(function (el) { if (el) el.addEventListener('click', function () { el.classList.toggle('on'); el.dispatchEvent(new Event('change')); @@ -1795,6 +1796,7 @@ setToggle(widgetServices, d.widgets && d.widgets.services); setToggle(widgetPihole, d.widgets && d.widgets.pihole); setToggle(widgetMidea, d.widgets && d.widgets.midea); + setToggle(widgetSmarthome, d.widgets && d.widgets.smarthome); setToggle(trustToggle, d.trustOwnerMapping); setToggle(autoappearToggle, d.autoappear !== false); if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('portal'); @@ -1802,7 +1804,7 @@ console.error('Failed to load portal settings:', err); }); - var portalFields = [enabledToggle, widgetDevice, widgetTraffic, widgetServices, widgetPihole, widgetMidea, trustToggle, autoappearToggle].filter(Boolean); + var portalFields = [enabledToggle, widgetDevice, widgetTraffic, widgetServices, widgetPihole, widgetMidea, widgetSmarthome, trustToggle, autoappearToggle].filter(Boolean); SettingsAutosave.bind({ cluster: 'portal', fields: portalFields, @@ -1815,6 +1817,7 @@ 'portal-widget-services': widgetServices ? widgetServices.classList.contains('on') : true, 'portal-widget-pihole': widgetPihole ? widgetPihole.classList.contains('on') : true, 'portal-widget-midea': widgetMidea ? widgetMidea.classList.contains('on') : true, + 'portal-widget-smarthome': widgetSmarthome ? widgetSmarthome.classList.contains('on') : true, 'portal-trust-owner-mapping': trustToggle ? trustToggle.classList.contains('on') : false, 'portal-autoappear': autoappearToggle ? autoappearToggle.classList.contains('on') : true, }; @@ -1828,6 +1831,7 @@ services: widgetServices ? widgetServices.classList.contains('on') : true, pihole: widgetPihole ? widgetPihole.classList.contains('on') : true, midea: widgetMidea ? widgetMidea.classList.contains('on') : true, + smarthome: widgetSmarthome ? widgetSmarthome.classList.contains('on') : true, }, trust_owner_mapping: trustToggle ? trustToggle.classList.contains('on') : false, autoappear: autoappearToggle ? autoappearToggle.classList.contains('on') : true, diff --git a/public/js/smarthome.js b/public/js/smarthome.js index 91e6b930..49a1f25c 100644 --- a/public/js/smarthome.js +++ b/public/js/smarthome.js @@ -100,6 +100,16 @@ wrap.querySelector('input').addEventListener('change', (e) => send(r.id, { ct: Number(e.target.value) }).catch(() => {})); body.appendChild(wrap); } + if (r.kind === 'light' || r.kind === 'plug' || r.kind === 'group') { + const own = document.createElement('div'); + const names = (r.owners || []).map((o) => o.username); + own.innerHTML = `