Remove idle color feature, simplify power to turn-off only, fix settings serial port bug

- Remove static/idle color from entire stack (storage, API, processing, UI, CSS, locales)
- Simplify device power button to turn-off only (send black frame, no toggle)
- Send black frame on serial port close (AdalightClient.close)
- Fix settings modal serial port dropdown showing WLED devices due to stale deviceType

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 04:04:28 +03:00
parent 1f6c913343
commit 8a0730d91b
12 changed files with 29 additions and 278 deletions

View File

@@ -22,7 +22,6 @@ from wled_controller.api.schemas.devices import (
DeviceUpdate, DeviceUpdate,
DiscoveredDeviceResponse, DiscoveredDeviceResponse,
DiscoverDevicesResponse, DiscoverDevicesResponse,
StaticColorUpdate,
) )
from wled_controller.core.processing.processor_manager import ProcessorManager from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore from wled_controller.storage import DeviceStore
@@ -45,7 +44,6 @@ def _device_to_response(device) -> DeviceResponse:
enabled=device.enabled, enabled=device.enabled,
baud_rate=device.baud_rate, baud_rate=device.baud_rate,
auto_shutdown=device.auto_shutdown, auto_shutdown=device.auto_shutdown,
static_color=list(device.static_color) if device.static_color else None,
capabilities=sorted(get_device_capabilities(device.device_type)), capabilities=sorted(get_device_capabilities(device.device_type)),
created_at=device.created_at, created_at=device.created_at,
updated_at=device.updated_at, updated_at=device.updated_at,
@@ -399,13 +397,6 @@ async def set_device_brightness(
if ds: if ds:
ds.hardware_brightness = bri ds.hardware_brightness = bri
# If device is idle with a static color, re-send it at the new brightness
if ds and ds.static_color is not None and not manager.is_device_processing(device_id):
try:
await manager.send_static_color(device_id, ds.static_color)
except Exception:
pass
return {"brightness": bri} return {"brightness": bri}
except Exception as e: except Exception as e:
logger.error(f"Failed to set brightness for {device_id}: {e}") logger.error(f"Failed to set brightness for {device_id}: {e}")
@@ -465,11 +456,7 @@ async def set_device_power(
# For serial devices, use the cached idle client to avoid port conflicts # For serial devices, use the cached idle client to avoid port conflicts
ds = manager._devices.get(device_id) ds = manager._devices.get(device_id)
if device.device_type in ("adalight", "ambiled") and ds: if device.device_type in ("adalight", "ambiled") and ds:
if on: if not on:
# Restore idle state (static color or stay dark)
if ds.static_color is not None:
await manager.send_static_color(device_id, ds.static_color)
else:
await manager._send_clear_pixels(device_id) await manager._send_clear_pixels(device_id)
ds.power_on = on ds.power_on = on
else: else:
@@ -484,61 +471,3 @@ async def set_device_power(
raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}") raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}")
# ===== STATIC COLOR ENDPOINTS =====
@router.get("/api/v1/devices/{device_id}/color", tags=["Settings"])
async def get_device_color(
device_id: str,
_auth: AuthRequired,
store: DeviceStore = Depends(get_device_store),
):
"""Get the static idle color for a device."""
device = store.get_device(device_id)
if not device:
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
if "static_color" not in get_device_capabilities(device.device_type):
raise HTTPException(status_code=400, detail="Static color is not supported for this device type")
return {"color": list(device.static_color) if device.static_color else None}
@router.put("/api/v1/devices/{device_id}/color", tags=["Settings"])
async def set_device_color(
device_id: str,
body: StaticColorUpdate,
_auth: AuthRequired,
store: DeviceStore = Depends(get_device_store),
manager: ProcessorManager = Depends(get_processor_manager),
):
"""Set or clear the static idle color for a device."""
device = store.get_device(device_id)
if not device:
raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
if "static_color" not in get_device_capabilities(device.device_type):
raise HTTPException(status_code=400, detail="Static color is not supported for this device type")
color = None
if body.color is not None:
if len(body.color) != 3 or not all(isinstance(c, int) and 0 <= c <= 255 for c in body.color):
raise HTTPException(status_code=400, detail="color must be [R, G, B] with values 0-255")
color = tuple(body.color)
store.set_static_color(device_id, color)
# Update runtime state
ds = manager._devices.get(device_id)
if ds:
ds.static_color = color
# If device is idle, apply the change immediately
if not manager.is_device_processing(device_id):
try:
if color is not None:
await manager.send_static_color(device_id, color)
else:
await manager.clear_device(device_id)
except Exception as e:
logger.warning(f"Failed to apply color change immediately: {e}")
return {"color": list(color) if color else None}

View File

@@ -28,15 +28,6 @@ class DeviceUpdate(BaseModel):
auto_shutdown: Optional[bool] = Field(None, description="Turn off device when server stops") auto_shutdown: Optional[bool] = Field(None, description="Turn off device when server stops")
class StaticColorUpdate(BaseModel):
"""Request to set or clear the static idle color."""
color: Optional[List[int]] = Field(
None,
description="RGB color [R, G, B] with values 0-255, or null to clear",
)
class Calibration(BaseModel): class Calibration(BaseModel):
"""Calibration configuration for pixel-to-LED mapping.""" """Calibration configuration for pixel-to-LED mapping."""
@@ -102,7 +93,6 @@ class DeviceResponse(BaseModel):
enabled: bool = Field(description="Whether device is enabled") enabled: bool = Field(description="Whether device is enabled")
baud_rate: Optional[int] = Field(None, description="Serial baud rate") baud_rate: Optional[int] = Field(None, description="Serial baud rate")
auto_shutdown: bool = Field(default=False, description="Restore device to idle state when targets stop") auto_shutdown: bool = Field(default=False, description="Restore device to idle state when targets stop")
static_color: Optional[List[int]] = Field(None, description="Static idle color [R, G, B]")
capabilities: List[str] = Field(default_factory=list, description="Device type capabilities") capabilities: List[str] = Field(default_factory=list, description="Device type capabilities")
created_at: datetime = Field(description="Creation timestamp") created_at: datetime = Field(description="Creation timestamp")
updated_at: datetime = Field(description="Last update timestamp") updated_at: datetime = Field(description="Last update timestamp")

View File

@@ -100,7 +100,14 @@ class AdalightClient(LEDClient):
raise RuntimeError(f"Failed to open serial port {self._port}: {e}") raise RuntimeError(f"Failed to open serial port {self._port}: {e}")
async def close(self) -> None: async def close(self) -> None:
"""Close the serial port.""" """Send black frame and close the serial port."""
if self._connected and self._serial and self._serial.is_open and self._led_count > 0:
try:
black = np.zeros((self._led_count, 3), dtype=np.uint8)
frame = self._build_frame(black, brightness=255)
await asyncio.to_thread(self._serial.write, frame)
except Exception as e:
logger.debug(f"Failed to send black frame on close: {e}")
self._connected = False self._connected = False
if self._serial and self._serial.is_open: if self._serial and self._serial.is_open:
try: try:

View File

@@ -2,10 +2,10 @@
Subclasses only need to override ``device_type`` and ``create_client()``. Subclasses only need to override ``device_type`` and ``create_client()``.
All common serial-device logic (COM port validation, discovery, health All common serial-device logic (COM port validation, discovery, health
checks, power control via black frames, static colour) lives here. checks, power control via black frames) lives here.
""" """
from typing import List, Tuple from typing import List
import numpy as np import numpy as np
@@ -28,8 +28,7 @@ class SerialDeviceProvider(LEDDeviceProvider):
# manual_led_count: user must specify LED count (can't auto-detect) # manual_led_count: user must specify LED count (can't auto-detect)
# power_control: can blank LEDs by sending all-black pixels # power_control: can blank LEDs by sending all-black pixels
# brightness_control: software brightness (multiplies pixel values before sending) # brightness_control: software brightness (multiplies pixel values before sending)
# static_color: can send a solid colour frame return {"manual_led_count", "power_control", "brightness_control"}
return {"manual_led_count", "power_control", "brightness_control", "static_color"}
async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth: async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
# Generic serial port health check — enumerate COM ports # Generic serial port health check — enumerate COM ports
@@ -116,31 +115,3 @@ class SerialDeviceProvider(LEDDeviceProvider):
finally: finally:
await client.close() await client.close()
async def set_color(self, url: str, color: Tuple[int, int, int], **kwargs) -> None:
"""Send a solid color frame to the device.
Accepts optional kwargs:
client: An already-connected LEDClient (e.g. cached idle client).
brightness (int): Software brightness 0-255 (default 255).
led_count (int), baud_rate (int | None).
"""
led_count = kwargs.get("led_count", 0)
if led_count <= 0:
raise ValueError(f"led_count is required to send color frame to {self.device_type} device")
brightness = kwargs.get("brightness", 255)
frame = np.full((led_count, 3), color, dtype=np.uint8)
existing_client = kwargs.get("client")
if existing_client:
await existing_client.send_pixels(frame, brightness=brightness)
else:
baud_rate = kwargs.get("baud_rate")
client = self.create_client(url, led_count=led_count, baud_rate=baud_rate)
try:
await client.connect()
await client.send_pixels(frame, brightness=brightness)
finally:
await client.close()
logger.info(f"{self.device_type} set_color: sent solid {color} to {url}")

View File

@@ -47,8 +47,6 @@ class DeviceState:
hardware_brightness: Optional[int] = None hardware_brightness: Optional[int] = None
# Auto-restore: restore device to idle state when targets stop # Auto-restore: restore device to idle state when targets stop
auto_shutdown: bool = False auto_shutdown: bool = False
# Static idle color for devices without a rich editor (e.g. Adalight)
static_color: Optional[Tuple[int, int, int]] = None
# Calibration test mode (works independently of target processing) # Calibration test mode (works independently of target processing)
test_mode_active: bool = False test_mode_active: bool = False
test_mode_edges: Dict[str, Tuple[int, int, int]] = field(default_factory=dict) test_mode_edges: Dict[str, Tuple[int, int, int]] = field(default_factory=dict)
@@ -159,7 +157,6 @@ class ProcessorManager:
baud_rate: Optional[int] = None, baud_rate: Optional[int] = None,
software_brightness: int = 255, software_brightness: int = 255,
auto_shutdown: bool = False, auto_shutdown: bool = False,
static_color: Optional[Tuple[int, int, int]] = None,
): ):
"""Register a device for health monitoring.""" """Register a device for health monitoring."""
if device_id in self._devices: if device_id in self._devices:
@@ -173,7 +170,6 @@ class ProcessorManager:
baud_rate=baud_rate, baud_rate=baud_rate,
software_brightness=software_brightness, software_brightness=software_brightness,
auto_shutdown=auto_shutdown, auto_shutdown=auto_shutdown,
static_color=static_color,
) )
self._devices[device_id] = state self._devices[device_id] = state
@@ -634,22 +630,6 @@ class ProcessorManager:
return proc.device_id return proc.device_id
return None return None
async def send_static_color(self, device_id: str, color: Tuple[int, int, int]) -> None:
"""Send a solid color to a device via its provider."""
ds = self._devices.get(device_id)
if not ds:
raise ValueError(f"Device {device_id} not found")
try:
provider = get_provider(ds.device_type)
client = await self._get_idle_client(device_id)
await provider.set_color(
ds.device_url, color,
led_count=ds.led_count, baud_rate=ds.baud_rate, client=client,
brightness=ds.software_brightness,
)
except Exception as e:
logger.error(f"Failed to send static color for {device_id}: {e}")
async def clear_device(self, device_id: str) -> None: async def clear_device(self, device_id: str) -> None:
"""Clear LED output on a device (send black / power off).""" """Clear LED output on a device (send black / power off)."""
ds = self._devices.get(device_id) ds = self._devices.get(device_id)
@@ -663,9 +643,8 @@ class ProcessorManager:
async def _restore_device_idle_state(self, device_id: str) -> None: async def _restore_device_idle_state(self, device_id: str) -> None:
"""Restore a device to its idle state when all targets stop. """Restore a device to its idle state when all targets stop.
- If a static color is configured, send it.
- For WLED: do nothing — stop() already restored the snapshot. - For WLED: do nothing — stop() already restored the snapshot.
- For other devices without static color: power off (black frame). - For other devices: power off (send black frame).
""" """
ds = self._devices.get(device_id) ds = self._devices.get(device_id)
if not ds or not ds.auto_shutdown: if not ds or not ds.auto_shutdown:
@@ -675,14 +654,7 @@ class ProcessorManager:
return return
try: try:
if ds.static_color is not None: if ds.device_type != "wled":
await self.send_static_color(device_id, ds.static_color)
logger.info(
f"Auto-restore: sent static color {ds.static_color} "
f"to {ds.device_type} device {device_id}"
)
elif ds.device_type != "wled":
# Non-WLED without static color: power off (send black frame)
await self._send_clear_pixels(device_id) await self._send_clear_pixels(device_id)
logger.info(f"Auto-restore: powered off {ds.device_type} device {device_id}") logger.info(f"Auto-restore: powered off {ds.device_type} device {device_id}")
else: else:

View File

@@ -213,7 +213,6 @@ async def lifespan(app: FastAPI):
baud_rate=device.baud_rate, baud_rate=device.baud_rate,
software_brightness=device.software_brightness, software_brightness=device.software_brightness,
auto_shutdown=device.auto_shutdown, auto_shutdown=device.auto_shutdown,
static_color=device.static_color,
) )
logger.info(f"Registered device: {device.name} ({device.id})") logger.info(f"Registered device: {device.name} ({device.id})")
except Exception as e: except Exception as e:

View File

@@ -423,39 +423,6 @@ section {
} }
/* Static color picker — inline in card-subtitle */ /* 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 { .section-header {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -30,8 +30,7 @@ import {
import { import {
showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal, showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal,
saveDeviceSettings, updateBrightnessLabel, saveCardBrightness, saveDeviceSettings, updateBrightnessLabel, saveCardBrightness,
saveDeviceStaticColor, clearDeviceStaticColor, turnOffDevice, removeDevice, loadDevices,
toggleDevicePower, removeDevice, loadDevices,
updateSettingsBaudFpsHint, updateSettingsBaudFpsHint,
} from './features/devices.js'; } from './features/devices.js';
import { import {
@@ -149,9 +148,7 @@ Object.assign(window, {
saveDeviceSettings, saveDeviceSettings,
updateBrightnessLabel, updateBrightnessLabel,
saveCardBrightness, saveCardBrightness,
saveDeviceStaticColor, turnOffDevice,
clearDeviceStaticColor,
toggleDevicePower,
removeDevice, removeDevice,
loadDevices, loadDevices,
updateSettingsBaudFpsHint, updateSettingsBaudFpsHint,

View File

@@ -69,7 +69,7 @@ export function createDeviceCard(device) {
return ` return `
<div class="card" data-device-id="${device.id}"> <div class="card" data-device-id="${device.id}">
<div class="card-top-actions"> <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>` : ''} ${(device.capabilities || []).includes('power_control') ? `<button class="card-top-btn card-power-btn" onclick="turnOffDevice('${device.id}')" title="${t('device.button.power_off')}"></button>` : ''}
<button class="card-remove-btn" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">&#x2715;</button> <button class="card-remove-btn" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">&#x2715;</button>
</div> </div>
<div class="card-header"> <div class="card-header">
@@ -85,7 +85,6 @@ export function createDeviceCard(device) {
${ledCount ? `<span class="card-meta" title="${t('device.led_count')}">💡 ${ledCount}</span>` : ''} ${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>` : ''} ${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> <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"' : ''}>&#x2715;</button></span>` : ''}
</div> </div>
${(device.capabilities || []).includes('brightness_control') ? ` ${(device.capabilities || []).includes('brightness_control') ? `
<div class="brightness-control${_deviceBrightnessCache[device.id] == null ? ' brightness-loading' : ''}" data-brightness-wrap="${device.id}"> <div class="brightness-control${_deviceBrightnessCache[device.id] == null ? ' brightness-loading' : ''}" data-brightness-wrap="${device.id}">
@@ -106,26 +105,21 @@ export function createDeviceCard(device) {
`; `;
} }
export async function toggleDevicePower(deviceId) { export async function turnOffDevice(deviceId) {
try { try {
const getResp = await fetchWithAuth(`/devices/${deviceId}/power`);
if (!getResp.ok) { showToast('Failed to get power state', 'error'); return; }
const current = await getResp.json();
const newState = !current.on;
const setResp = await fetchWithAuth(`/devices/${deviceId}/power`, { const setResp = await fetchWithAuth(`/devices/${deviceId}/power`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ on: newState }) body: JSON.stringify({ on: false })
}); });
if (setResp.ok) { if (setResp.ok) {
showToast(t(newState ? 'device.power.on_success' : 'device.power.off_success'), 'success'); showToast(t('device.power.off_success'), 'success');
} else { } else {
const error = await setResp.json(); const error = await setResp.json();
showToast(error.detail || 'Failed', 'error'); showToast(error.detail || 'Failed', 'error');
} }
} catch (error) { } catch (error) {
if (error.isAuth) return; if (error.isAuth) return;
showToast('Failed to toggle power', 'error'); showToast('Failed to turn off device', 'error');
} }
} }
@@ -162,6 +156,11 @@ export async function showSettings(deviceId) {
const device = await deviceResponse.json(); const device = await deviceResponse.json();
const isAdalight = isSerialDevice(device.device_type); const isAdalight = isSerialDevice(device.device_type);
const caps = device.capabilities || [];
// Set modal state before populating fields (so async helpers read correct type)
settingsModal.deviceType = device.device_type;
settingsModal.capabilities = caps;
document.getElementById('settings-device-id').value = device.id; document.getElementById('settings-device-id').value = device.id;
document.getElementById('settings-device-name').value = device.name; document.getElementById('settings-device-name').value = device.name;
@@ -181,7 +180,6 @@ export async function showSettings(deviceId) {
serialGroup.style.display = 'none'; serialGroup.style.display = 'none';
} }
const caps = device.capabilities || [];
const ledCountGroup = document.getElementById('settings-led-count-group'); const ledCountGroup = document.getElementById('settings-led-count-group');
if (caps.includes('manual_led_count')) { if (caps.includes('manual_led_count')) {
ledCountGroup.style.display = ''; ledCountGroup.style.display = '';
@@ -205,9 +203,6 @@ export async function showSettings(deviceId) {
} }
document.getElementById('settings-auto-shutdown').checked = !!device.auto_shutdown; document.getElementById('settings-auto-shutdown').checked = !!device.auto_shutdown;
settingsModal.deviceType = device.device_type;
settingsModal.capabilities = caps;
settingsModal.snapshot(); settingsModal.snapshot();
settingsModal.open(); settingsModal.open();
@@ -314,54 +309,6 @@ export async function fetchDeviceBrightness(deviceId) {
} }
} }
// Static color helpers
export function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
}
export 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;
}
export 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 })
});
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');
}
}
export 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 })
});
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);
}
}
// FPS hint helpers (shared with device-discovery) // FPS hint helpers (shared with device-discovery)
export function _computeMaxFps(baudRate, ledCount, deviceType) { export function _computeMaxFps(baudRate, ledCount, deviceType) {
if (!baudRate || !ledCount || ledCount < 1) return null; if (!baudRate || !ledCount || ledCount < 1) return null;

View File

@@ -131,8 +131,7 @@
"device.button.calibrate": "Calibrate", "device.button.calibrate": "Calibrate",
"device.button.remove": "Remove", "device.button.remove": "Remove",
"device.button.webui": "Open Device Web UI", "device.button.webui": "Open Device Web UI",
"device.button.power_toggle": "Toggle Power", "device.button.power_off": "Turn Off",
"device.power.on_success": "Device turned on",
"device.power.off_success": "Device turned off", "device.power.off_success": "Device turned off",
"device.status.connected": "Connected", "device.status.connected": "Connected",
"device.status.disconnected": "Disconnected", "device.status.disconnected": "Disconnected",
@@ -159,9 +158,6 @@
"device.health.online": "Online", "device.health.online": "Online",
"device.health.offline": "Offline", "device.health.offline": "Offline",
"device.health.checking": "Checking...", "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.tutorial.start": "Start tutorial",
"device.tip.metadata": "Device info (LED count, type, color channels) is auto-detected from the device", "device.tip.metadata": "Device info (LED count, type, color channels) is auto-detected from the device",
"device.tip.brightness": "Slide to adjust device brightness", "device.tip.brightness": "Slide to adjust device brightness",

View File

@@ -131,8 +131,7 @@
"device.button.calibrate": "Калибровка", "device.button.calibrate": "Калибровка",
"device.button.remove": "Удалить", "device.button.remove": "Удалить",
"device.button.webui": "Открыть веб-интерфейс устройства", "device.button.webui": "Открыть веб-интерфейс устройства",
"device.button.power_toggle": "Вкл/Выкл", "device.button.power_off": "Выключить",
"device.power.on_success": "Устройство включено",
"device.power.off_success": "Устройство выключено", "device.power.off_success": "Устройство выключено",
"device.status.connected": "Подключено", "device.status.connected": "Подключено",
"device.status.disconnected": "Отключено", "device.status.disconnected": "Отключено",
@@ -159,9 +158,6 @@
"device.health.online": "Онлайн", "device.health.online": "Онлайн",
"device.health.offline": "Недоступен", "device.health.offline": "Недоступен",
"device.health.checking": "Проверка...", "device.health.checking": "Проверка...",
"device.static_color": "Цвет ожидания",
"device.static_color.hint": "Цвет, когда устройство в режиме ожидания",
"device.static_color.clear": "Очистить цвет ожидания",
"device.tutorial.start": "Начать обучение", "device.tutorial.start": "Начать обучение",
"device.tip.metadata": "Информация об устройстве (кол-во LED, тип, цветовые каналы) определяется автоматически", "device.tip.metadata": "Информация об устройстве (кол-во LED, тип, цветовые каналы) определяется автоматически",
"device.tip.brightness": "Перетащите для регулировки яркости", "device.tip.brightness": "Перетащите для регулировки яркости",

View File

@@ -4,7 +4,7 @@ import json
import uuid import uuid
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional
from wled_controller.utils import get_logger from wled_controller.utils import get_logger
@@ -30,7 +30,6 @@ class Device:
baud_rate: Optional[int] = None, baud_rate: Optional[int] = None,
software_brightness: int = 255, software_brightness: int = 255,
auto_shutdown: bool = False, auto_shutdown: bool = False,
static_color: Optional[Tuple[int, int, int]] = None,
created_at: Optional[datetime] = None, created_at: Optional[datetime] = None,
updated_at: Optional[datetime] = None, updated_at: Optional[datetime] = None,
): ):
@@ -43,7 +42,6 @@ class Device:
self.baud_rate = baud_rate self.baud_rate = baud_rate
self.software_brightness = software_brightness self.software_brightness = software_brightness
self.auto_shutdown = auto_shutdown self.auto_shutdown = auto_shutdown
self.static_color = static_color
self.created_at = created_at or datetime.utcnow() self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow() self.updated_at = updated_at or datetime.utcnow()
# Preserved from old JSON for migration — not written back # Preserved from old JSON for migration — not written back
@@ -67,8 +65,6 @@ class Device:
d["software_brightness"] = self.software_brightness d["software_brightness"] = self.software_brightness
if self.auto_shutdown: if self.auto_shutdown:
d["auto_shutdown"] = True d["auto_shutdown"] = True
if self.static_color is not None:
d["static_color"] = list(self.static_color)
return d return d
@classmethod @classmethod
@@ -78,9 +74,6 @@ class Device:
Backward-compatible: reads legacy 'calibration' field and stores it Backward-compatible: reads legacy 'calibration' field and stores it
in _legacy_calibration for migration use only. in _legacy_calibration for migration use only.
""" """
static_color_raw = data.get("static_color")
static_color = tuple(static_color_raw) if static_color_raw else None
device = cls( device = cls(
device_id=data["id"], device_id=data["id"],
name=data["name"], name=data["name"],
@@ -91,7 +84,6 @@ class Device:
baud_rate=data.get("baud_rate"), baud_rate=data.get("baud_rate"),
software_brightness=data.get("software_brightness", 255), software_brightness=data.get("software_brightness", 255),
auto_shutdown=data.get("auto_shutdown", False), auto_shutdown=data.get("auto_shutdown", False),
static_color=static_color,
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())), created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())), updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
) )
@@ -250,18 +242,6 @@ class DeviceStore:
logger.info(f"Updated device {device_id}") logger.info(f"Updated device {device_id}")
return device return device
def set_static_color(
self, device_id: str, color: Optional[Tuple[int, int, int]]
) -> "Device":
"""Set or clear the static idle color for a device."""
device = self._devices.get(device_id)
if not device:
raise ValueError(f"Device {device_id} not found")
device.static_color = color
device.updated_at = datetime.utcnow()
self.save()
return device
def delete_device(self, device_id: str): def delete_device(self, device_id: str):
"""Delete device.""" """Delete device."""
if device_id not in self._devices: if device_id not in self._devices: