Add static color for simple devices, change auto-shutdown to auto-restore
- Add `static_color` capability to Adalight provider with `set_color()` method
- Add `static_color` field to Device model, DeviceState, and API schemas
- Add GET/PUT `/devices/{id}/color` API endpoints
- Change auto-shutdown behavior: restore device to idle state instead of
powering off (WLED uses snapshot/restore, Adalight sends static color
or black frame)
- Rename `_auto_shutdown_device_if_idle` to `_restore_device_idle_state`
- Add inline color picker on device cards for devices with static_color
- Add auto_shutdown toggle to device settings modal
- Update labels from "Auto Shutdown" to "Auto Restore" (en + ru)
- Remove backward-compat KC aliases from ProcessorManager
- Align card action buttons to bottom with flex column layout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -667,6 +667,7 @@ function createDeviceCard(device) {
|
||||
${ledCount ? `<span class="card-meta" title="${t('device.led_count')}">💡 ${ledCount}</span>` : ''}
|
||||
${state.device_led_type ? `<span class="card-meta">🔌 ${state.device_led_type.replace(/ RGBW$/, '')}</span>` : ''}
|
||||
<span class="card-meta" title="${state.device_rgbw ? 'RGBW' : 'RGB'}"><span class="channel-indicator"><span class="ch" style="background:#e53935"></span><span class="ch" style="background:#43a047"></span><span class="ch" style="background:#1e88e5"></span>${state.device_rgbw ? '<span class="ch" style="background:#eee"></span>' : ''}</span></span>
|
||||
${(device.capabilities || []).includes('static_color') ? `<span class="card-meta static-color-control" data-color-wrap="${device.id}"><input type="color" class="static-color-picker" value="${device.static_color ? rgbToHex(...device.static_color) : '#000000'}" data-device-color="${device.id}" onchange="saveDeviceStaticColor('${device.id}', this.value)" title="${t('device.static_color.hint')}"><button class="btn-clear-color" onclick="clearDeviceStaticColor('${device.id}')" title="${t('device.static_color.clear')}" ${!device.static_color ? 'style="display:none"' : ''}>✕</button></span>` : ''}
|
||||
</div>
|
||||
${(device.capabilities || []).includes('brightness_control') ? `
|
||||
<div class="brightness-control${_deviceBrightnessCache[device.id] == null ? ' brightness-loading' : ''}" data-brightness-wrap="${device.id}">
|
||||
@@ -814,6 +815,9 @@ async function showSettings(deviceId) {
|
||||
baudRateGroup.style.display = 'none';
|
||||
}
|
||||
|
||||
// Populate auto shutdown toggle
|
||||
document.getElementById('settings-auto-shutdown').checked = !!device.auto_shutdown;
|
||||
|
||||
// Snapshot initial values for dirty checking
|
||||
settingsInitialValues = {
|
||||
name: device.name,
|
||||
@@ -823,6 +827,7 @@ async function showSettings(deviceId) {
|
||||
device_type: device.device_type,
|
||||
capabilities: caps,
|
||||
state_check_interval: '30',
|
||||
auto_shutdown: !!device.auto_shutdown,
|
||||
};
|
||||
|
||||
// Show modal
|
||||
@@ -855,6 +860,7 @@ function isSettingsDirty() {
|
||||
document.getElementById('settings-device-name').value !== settingsInitialValues.name ||
|
||||
_getSettingsUrl() !== settingsInitialValues.url ||
|
||||
document.getElementById('settings-health-interval').value !== settingsInitialValues.state_check_interval ||
|
||||
document.getElementById('settings-auto-shutdown').checked !== settingsInitialValues.auto_shutdown ||
|
||||
ledCountDirty
|
||||
);
|
||||
}
|
||||
@@ -890,8 +896,8 @@ async function saveDeviceSettings() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Update device info (name, url, optionally led_count, baud_rate)
|
||||
const body = { name, url };
|
||||
// Update device info (name, url, auto_shutdown, optionally led_count, baud_rate)
|
||||
const body = { name, url, auto_shutdown: document.getElementById('settings-auto-shutdown').checked };
|
||||
const ledCountInput = document.getElementById('settings-led-count');
|
||||
if ((settingsInitialValues.capabilities || []).includes('manual_led_count') && ledCountInput.value) {
|
||||
body.led_count = parseInt(ledCountInput.value, 10);
|
||||
@@ -973,6 +979,56 @@ async function fetchDeviceBrightness(deviceId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Static color helpers
|
||||
function rgbToHex(r, g, b) {
|
||||
return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null;
|
||||
}
|
||||
|
||||
async function saveDeviceStaticColor(deviceId, hexValue) {
|
||||
const rgb = hexToRgb(hexValue);
|
||||
try {
|
||||
await fetch(`${API_BASE}/devices/${deviceId}/color`, {
|
||||
method: 'PUT',
|
||||
headers: { ...getHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ color: rgb })
|
||||
});
|
||||
// Show clear button
|
||||
const wrap = document.querySelector(`[data-color-wrap="${deviceId}"]`);
|
||||
if (wrap) {
|
||||
const clearBtn = wrap.querySelector('.btn-clear-color');
|
||||
if (clearBtn) clearBtn.style.display = '';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to set static color:', err);
|
||||
showToast('Failed to set static color', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDeviceStaticColor(deviceId) {
|
||||
try {
|
||||
await fetch(`${API_BASE}/devices/${deviceId}/color`, {
|
||||
method: 'PUT',
|
||||
headers: { ...getHeaders(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ color: null })
|
||||
});
|
||||
// Reset picker to black and hide clear button
|
||||
const picker = document.querySelector(`[data-device-color="${deviceId}"]`);
|
||||
if (picker) picker.value = '#000000';
|
||||
const wrap = document.querySelector(`[data-color-wrap="${deviceId}"]`);
|
||||
if (wrap) {
|
||||
const clearBtn = wrap.querySelector('.btn-clear-color');
|
||||
if (clearBtn) clearBtn.style.display = 'none';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to clear static color:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Add device modal
|
||||
let _discoveryScanRunning = false;
|
||||
let _discoveryCache = {}; // { deviceType: [...devices] } — per-type discovery cache
|
||||
|
||||
@@ -297,6 +297,18 @@
|
||||
<input type="number" id="settings-health-interval" min="5" max="600" value="30">
|
||||
</div>
|
||||
|
||||
<div class="form-group settings-toggle-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="settings.auto_shutdown">Auto Restore:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="settings.auto_shutdown.hint">Restore device to idle state when targets stop or server shuts down</small>
|
||||
<label class="settings-toggle">
|
||||
<input type="checkbox" id="settings-auto-shutdown">
|
||||
<span class="settings-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="settings-error" class="error-message" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -158,6 +158,9 @@
|
||||
"device.health.online": "Online",
|
||||
"device.health.offline": "Offline",
|
||||
"device.health.checking": "Checking...",
|
||||
"device.static_color": "Idle Color",
|
||||
"device.static_color.hint": "Color shown when device is idle",
|
||||
"device.static_color.clear": "Clear idle color",
|
||||
"device.tutorial.start": "Start tutorial",
|
||||
"device.tip.metadata": "Device info (LED count, type, color channels) is auto-detected from the device",
|
||||
"device.tip.brightness": "Slide to adjust device brightness",
|
||||
@@ -184,6 +187,8 @@
|
||||
"settings.button.cancel": "Cancel",
|
||||
"settings.health_interval": "Health Check Interval (s):",
|
||||
"settings.health_interval.hint": "How often to check the device status (5-600 seconds)",
|
||||
"settings.auto_shutdown": "Auto Restore:",
|
||||
"settings.auto_shutdown.hint": "Restore device to idle state when targets stop or server shuts down",
|
||||
"settings.button.save": "Save Changes",
|
||||
"settings.saved": "Settings saved successfully",
|
||||
"settings.failed": "Failed to save settings",
|
||||
|
||||
@@ -158,6 +158,9 @@
|
||||
"device.health.online": "Онлайн",
|
||||
"device.health.offline": "Недоступен",
|
||||
"device.health.checking": "Проверка...",
|
||||
"device.static_color": "Цвет ожидания",
|
||||
"device.static_color.hint": "Цвет, когда устройство в режиме ожидания",
|
||||
"device.static_color.clear": "Очистить цвет ожидания",
|
||||
"device.tutorial.start": "Начать обучение",
|
||||
"device.tip.metadata": "Информация об устройстве (кол-во LED, тип, цветовые каналы) определяется автоматически",
|
||||
"device.tip.brightness": "Перетащите для регулировки яркости",
|
||||
@@ -184,6 +187,8 @@
|
||||
"settings.button.cancel": "Отмена",
|
||||
"settings.health_interval": "Интервал Проверки (с):",
|
||||
"settings.health_interval.hint": "Как часто проверять статус устройства (5-600 секунд)",
|
||||
"settings.auto_shutdown": "Авто-восстановление:",
|
||||
"settings.auto_shutdown.hint": "Восстанавливать устройство в режим ожидания при остановке целей или сервера",
|
||||
"settings.button.save": "Сохранить Изменения",
|
||||
"settings.saved": "Настройки успешно сохранены",
|
||||
"settings.failed": "Не удалось сохранить настройки",
|
||||
|
||||
@@ -231,6 +231,8 @@ section {
|
||||
padding: 12px 20px 20px;
|
||||
position: relative;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
@@ -739,6 +741,40 @@ section {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Static color picker — inline in card-subtitle */
|
||||
.static-color-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.static-color-picker {
|
||||
width: 22px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.btn-clear-color {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #777;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
border-radius: 3px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.btn-clear-color:hover {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -779,6 +815,54 @@ ul.section-tip li {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.settings-toggle-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.settings-toggle input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.settings-toggle-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--border-color);
|
||||
border-radius: 9px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.settings-toggle-slider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.settings-toggle input:checked + .settings-toggle-slider {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.settings-toggle input:checked + .settings-toggle-slider::after {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
|
||||
Reference in New Issue
Block a user