// ========== IoT 设备控制面板 ========== function startIoTRefresh() { stopIoTRefresh(); STATE.iotInterval = setInterval(function() { if (STATE.activePanel === 'iot') renderIoTPanel(); }, 3000); } function stopIoTRefresh() { if (STATE.iotInterval) { clearInterval(STATE.iotInterval); STATE.iotInterval = null; } } async function fetchIoTDevices() { var data = await api('/api/iot/devices'); if (data.error) return { error: data }; var devices = []; if (Array.isArray(data)) devices = data; else if (data.devices) devices = data.devices; return { devices: devices }; } var IOT_DEVICE_TYPES = { 'ac': '❄️', 'light': '💡', 'curtain': '🪟', 'door_lock': '🔒', 'camera': '📷', 'sensor': '📡', 'speaker': '🔊', 'thermostat': '🌡️', }; var IOT_MODE_OPTIONS = ['cool', 'heat', 'auto', 'fan', 'dry']; var IOT_COLOR_OPTIONS = [ { name: '暖白', value: 'warm_white', bg: '#f5d0a9' }, { name: '冷白', value: 'cool_white', bg: '#d4e6fc' }, { name: '暖黄', value: 'warm_yellow', bg: '#fce4a6' }, { name: '蓝色', value: 'blue', bg: '#3b82f6' }, { name: '紫色', value: 'purple', bg: '#a855f7' }, { name: '绿色', value: 'green', bg: '#22c55e' }, ]; async function renderIoTPanel() { var result = await fetchIoTDevices(); var panel = document.getElementById('panel-iot'); // 更新操作栏 (只更新时间戳文本) document.getElementById('panel-actions').innerHTML = '' + '⏱ 每3秒自动刷新 · 最后更新: ' + new Date().toLocaleTimeString('zh-CN', {hour12: false}) + ''; if (result.error) { var hint = ''; if (result.error.errorType === 'iot_not_running') { hint = '
💡 提示: 请先在「服务管理」面板中启动 IoT Debug 服务'; } panel.innerHTML = '
⚠️
' + escHtml(result.error.error) + hint + '
'; STATE.iotInitialized = false; return; } var devices = result.devices; // 固定设备排列顺序: 先按类型,同类型再按 device_id var typeOrder = { 'sensor': 1, 'ac': 2, 'light': 3, 'curtain': 4, 'lock': 5, 'camera': 6, 'speaker': 7, 'thermostat': 8 }; devices.sort(function(a, b) { var oa = typeOrder[a.type] || 99; var ob = typeOrder[b.type] || 99; if (oa !== ob) return oa - ob; return (a.id || a.entity_id || '').localeCompare(b.id || b.entity_id || ''); }); var badge = document.getElementById('iot-badge'); if (badge) { badge.textContent = devices.length; badge.style.display = devices.length > 0 ? 'inline-block' : 'none'; } // Bug 7: 增量更新 — 首次渲染完整 DOM,后续只更新设备属性值 var grid = document.getElementById('iot-device-grid'); var firstRender = !STATE.iotInitialized; if (firstRender || !grid) { // 首次渲染: 创建完整结构 var html = '
' + '📡 模拟 IoT 设备 (' + devices.length + ')' + '通过 IoT 调试服务 (端口 8083) 管理' + '
' + devices.map(function(d) { return renderIoTDeviceCard(d); }).join('') + '
'; panel.innerHTML = html; STATE.iotInitialized = true; } else { // 增量更新: 只更新设备数量、最后更新时间 var countEl = document.getElementById('iot-device-count'); if (countEl) countEl.textContent = '📡 模拟 IoT 设备 (' + devices.length + ')'; // 对每个已有设备卡片做增量更新 devices.forEach(function(device) { updateIoTDeviceCardInPlace(device); }); } } // Bug 7 helper: 增量更新单个设备卡片的属性值,不重建 DOM function updateIoTDeviceCardInPlace(device) { var card = document.getElementById('iot-card-' + device.id); if (!card) return; var isOn = device.status === 'on'; // 更新卡片 class (on/off 边框) card.className = 'iot-device-card ' + (isOn ? 'on' : 'off'); // 更新状态圆点和文字 var statusDot = card.querySelector('.iot-status-dot'); if (statusDot) statusDot.className = 'iot-status-dot ' + (isOn ? 'on' : 'off'); var statusText = card.querySelector('.iot-device-status span:last-child'); if (statusText) { statusText.textContent = isOn ? '开启' : '关闭'; statusText.style.color = isOn ? 'var(--green)' : 'var(--text3)'; } // 更新开关按钮 var toggleBtn = card.querySelector('.iot-toggle-btn'); if (toggleBtn) { toggleBtn.className = 'iot-toggle-btn ' + (isOn ? 'on' : 'off'); toggleBtn.textContent = isOn ? '⏻ 关闭' : '⏻ 开启'; } // 更新设备属性值 (温度、亮度、位置等) var propValues = card.querySelectorAll('.iot-prop-value'); var type = device.type; if (type === 'ac') { // AC: 温度 slider + 模式按钮 var tempSlider = card.querySelector('.iot-prop-control input[type="range"]'); if (tempSlider) { tempSlider.value = device.temperature || 26; var tempVal = tempSlider.nextElementSibling; if (tempVal) tempVal.textContent = (device.temperature || 26) + '°C'; } // 更新模式按钮 var modeBtns = card.querySelectorAll('.iot-mode-btn'); modeBtns.forEach(function(btn) { var btnMode = btn.textContent.trim(); if (btnMode === (device.mode || 'cool')) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); } else if (type === 'light') { var brightSlider = card.querySelector('.iot-prop-control input[type="range"]'); if (brightSlider) { brightSlider.value = device.brightness || 80; var brightVal = brightSlider.nextElementSibling; if (brightVal) brightVal.textContent = (device.brightness || 80) + '%'; } // 更新颜色按钮 var colorBtns = card.querySelectorAll('.iot-color-btn'); colorBtns.forEach(function(btn) { // 颜色值从 onclick 属性解析 var onclick = btn.getAttribute('onclick') || ''; var match = onclick.match(/iotSetProperty\('[^']+',\s*'color',\s*'([^']+)'\)/); if (match && match[1] === (device.color || 'warm_white')) { btn.classList.add('active'); } else if (!match || match[1] !== (device.color || 'warm_white')) { btn.classList.remove('active'); } }); } else if (type === 'curtain') { var posSlider = card.querySelector('.iot-prop-control input[type="range"]'); if (posSlider) { posSlider.value = device.position != null ? device.position : 100; var posVal = posSlider.nextElementSibling; if (posVal) posVal.textContent = (device.position != null ? device.position : 100) + '%'; } } else if (device.temperature != null) { // sensor/thermostat: 只读温度 if (propValues.length > 0) propValues[0].textContent = device.temperature + (device.unit || '°C'); } // 更新电量 var batteryEls = card.querySelectorAll('.iot-prop-value'); batteryEls.forEach(function(el) { if (el.parentElement && el.parentElement.querySelector('.iot-prop-label') && el.parentElement.querySelector('.iot-prop-label').textContent.indexOf('🔋') !== -1) { if (device.battery != null) el.textContent = device.battery + '%'; } }); // 更新 AC 温度按钮的 onclick 引用 if (type === 'ac') { var acBtns = card.querySelectorAll('.iot-device-actions .btn-xs'); acBtns.forEach(function(btn) { var text = btn.textContent.trim(); var currentTemp = device.temperature || 26; if (text === '⬇ -2°C') { btn.setAttribute('onclick', "iotSetProperty('" + device.id + "', 'temperature', " + (currentTemp - 2) + ");refreshIoTDeviceCard('" + device.id + "')"); } else if (text === '⬆ +2°C') { btn.setAttribute('onclick', "iotSetProperty('" + device.id + "', 'temperature', " + (currentTemp + 2) + ");refreshIoTDeviceCard('" + device.id + "')"); } }); } } function renderIoTDeviceCard(device) { var isOn = device.status === 'on'; var icon = IOT_DEVICE_TYPES[device.type] || '📦'; var propsHtml = ''; if (device.type === 'ac') { propsHtml = '
' + '🌡️ 温度' + '
' + '' + '' + (device.temperature || 26) + '°C' + '
' + '
' + '
' + '🔄 模式' + '
' + IOT_MODE_OPTIONS.map(function(m) { var active = (device.mode || 'cool') === m ? ' active' : ''; return ''; }).join('') + '
' + '
'; } else if (device.type === 'light') { propsHtml = '
' + '💡 亮度' + '
' + '' + '' + (device.brightness || 80) + '%' + '
' + '
' + '
' + '🎨 颜色' + '
' + IOT_COLOR_OPTIONS.map(function(c) { var active = (device.color || 'warm_white') === c.value ? ' active' : ''; return ''; }).join('') + '
' + '
'; } else if (device.type === 'curtain') { propsHtml = '
' + '🪟 位置' + '
' + '' + '' + (device.position != null ? device.position : 100) + '%' + '
' + '
'; } else if (device.temperature != null) { propsHtml = '
' + '🌡️ 温度' + '' + device.temperature + (device.unit || '°C') + '' + '
'; } if (device.battery != null) { propsHtml += '
' + '🔋 电量' + '' + device.battery + '%' + '
'; } var actionsHtml = '
' + ''; if (device.type === 'ac') { var currentTemp = device.temperature || 26; actionsHtml += '' + ''; } actionsHtml += '' + '
' + ''; return '
' + '
' + '
' + '' + icon + '' + '
' + '
' + escHtml(device.name) + '
' + '
' + escHtml(device.type) + ' · ' + escHtml(device.id) + '
' + '
' + '
' + '
' + '' + '' + (isOn ? '开启' : '关闭') + '' + '
' + '
' + (propsHtml ? '
' + propsHtml + '
' : '') + actionsHtml + '
'; } async function iotToggle(deviceId) { console.log('[IoT] 切换设备开关: ' + deviceId); var data = await api('/api/iot/devices/' + deviceId + '/toggle', { method: 'POST' }); if (data.error) { showToast('切换失败: ' + data.error, 'error'); } else { var device = data.device || {}; showToast((device.name || deviceId) + ': ' + (device.status === 'on' ? '已开启' : '已关闭'), 'success'); renderIoTPanel(); } } async function iotSetProperty(deviceId, field, value) { console.log('[IoT] 设置设备属性: ' + deviceId + ' -> ' + field + ' = ' + value); var data = await api('/api/iot/devices/' + deviceId + '/set', { method: 'POST', body: JSON.stringify({ field: field, value: value }), }); if (data.error) { showToast('设置失败: ' + data.error, 'error'); } else { var device = data.device || {}; showToast((device.name || deviceId) + ': ' + field + ' = ' + value, 'success'); } } async function refreshIoTDeviceCard(deviceId) { var data = await api('/api/iot/devices/' + deviceId); if (data.error) return; var device = data.device; if (!device) return; var card = document.getElementById('iot-card-' + deviceId); if (!card) return; card.outerHTML = renderIoTDeviceCard(device); } async function iotShowHistory(deviceId) { var panel = document.getElementById('iot-history-' + deviceId); if (!panel) return; if (panel.style.display !== 'none') { panel.style.display = 'none'; return; } console.log('[IoT] 获取设备历史: ' + deviceId); var data = await api('/api/iot/devices/' + deviceId + '/history'); panel.style.display = ''; if (data.error) { panel.innerHTML = '
' + escHtml(data.error) + '
'; return; } var history = data.history || []; if (history.length === 0) { panel.innerHTML = '
暂无操作历史
'; return; } panel.innerHTML = history.slice(-20).reverse().map(function(h) { var timeStr = h.timestamp ? new Date(h.timestamp).toLocaleTimeString('zh-CN', {hour12: false}) : '—'; return '
' + '' + timeStr + '' + '' + escHtml(h.action || '操作') + '' + '' + escHtml(h.detail || h.value || '') + '' + '
'; }).join(''); }