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:
@@ -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}
|
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}")
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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')}">✕</button>
|
<button class="card-remove-btn" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">✕</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"' : ''}>✕</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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "Перетащите для регулировки яркости",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user