// ========== 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 '' +
'' +
(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('');
}