Add power toggle button to LED device cards

WLED: native on/off via JSON API. Adalight: sends all-black frame
to blank LEDs (uses existing client if target is running, otherwise
opens temporary serial connection). Toggle button placed next to
delete button in card top-right corner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 19:18:39 +03:00
parent f4503d36b4
commit cc91ccd75a
9 changed files with 193 additions and 4 deletions

View File

@@ -650,7 +650,10 @@ function createDeviceCard(device) {
return `
<div class="card" data-device-id="${device.id}">
<button class="card-remove-btn" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">&#x2715;</button>
<div class="card-top-actions">
${(device.capabilities || []).includes('power_control') ? `<button class="card-top-btn card-power-btn" onclick="toggleDevicePower('${device.id}')" title="${t('device.button.power_toggle')}">⏻</button>` : ''}
<button class="card-remove-btn" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">&#x2715;</button>
</div>
<div class="card-header">
<div class="card-title">
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
@@ -686,6 +689,33 @@ function createDeviceCard(device) {
`;
}
async function toggleDevicePower(deviceId) {
try {
// Get current power state
const getResp = await fetch(`${API_BASE}/devices/${deviceId}/power`, { headers: getHeaders() });
if (getResp.status === 401) { handle401Error(); return; }
if (!getResp.ok) { showToast('Failed to get power state', 'error'); return; }
const current = await getResp.json();
const newState = !current.on;
// Toggle
const setResp = await fetch(`${API_BASE}/devices/${deviceId}/power`, {
method: 'PUT',
headers: { ...getHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ on: newState })
});
if (setResp.status === 401) { handle401Error(); return; }
if (setResp.ok) {
showToast(t(newState ? 'device.power.on_success' : 'device.power.off_success'), 'success');
} else {
const error = await setResp.json();
showToast(error.detail || 'Failed', 'error');
}
} catch (error) {
showToast('Failed to toggle power', 'error');
}
}
function attachDeviceListeners(deviceId) {
// Add any specific event listeners here if needed
}

View File

@@ -130,6 +130,9 @@
"device.button.calibrate": "Calibrate",
"device.button.remove": "Remove",
"device.button.webui": "Open Device Web UI",
"device.button.power_toggle": "Toggle Power",
"device.power.on_success": "Device turned on",
"device.power.off_success": "Device turned off",
"device.status.connected": "Connected",
"device.status.disconnected": "Disconnected",
"device.status.error": "Error",

View File

@@ -130,6 +130,9 @@
"device.button.calibrate": "Калибровка",
"device.button.remove": "Удалить",
"device.button.webui": "Открыть веб-интерфейс устройства",
"device.button.power_toggle": "Вкл/Выкл",
"device.power.on_success": "Устройство включено",
"device.power.off_success": "Устройство выключено",
"device.status.connected": "Подключено",
"device.status.disconnected": "Отключено",
"device.status.error": "Ошибка",

View File

@@ -263,6 +263,39 @@ section {
}
.card-top-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
align-items: center;
gap: 2px;
}
.card-top-actions .card-remove-btn {
position: static;
}
.card-power-btn {
background: none;
border: none;
color: #777;
font-size: 1rem;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px;
transition: color 0.2s, background 0.2s;
}
.card-power-btn:hover {
color: var(--primary-color);
background: rgba(76, 175, 80, 0.1);
}
.card-remove-btn {
position: absolute;
top: 10px;