Fix autorestore logic and protocol badge per device type

Autorestore fixes:
- Snapshot WLED state before connect() mutates it (lor, AudioReactive)
- Gate restore on auto_shutdown setting (was unconditional)
- Remove misleading auto_restore capability from serial provider
- Default auto_shutdown to false for all new devices

Protocol badge fixes:
- Show correct protocol per device type (OpenRGB SDK, MQTT, WebSocket)
- Was showing "Serial" for all non-WLED devices

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 10:04:40 +03:00
parent 96bd3bd0f0
commit 8061c26bef
9 changed files with 44 additions and 12 deletions

View File

@@ -33,7 +33,7 @@ Priority: `P1` quick win · `P2` moderate · `P3` large effort
- [ ] `P2` **WebSocket event bus** — Broadcast all state changes over a single WS channel
- Complexity: low-medium — ProcessorManager already emits events; add a WS endpoint that fans out JSON events to connected clients
- Impact: medium — enables real-time dashboards, mobile apps, and third-party integrations
- [ ] `P3` **Notification reactive** — Flash/pulse on OS notifications (optional app filter)
- [x] `P3` **Notification reactive** — Flash/pulse on OS notifications (optional app filter)
- Complexity: large — OS-level notification listener (platform-specific: Win32 `WinToast`/`pystray`, macOS `pyobjc`); needs a new "effect source" type that triggers color pulses
- Impact: low-medium — fun but niche; platform-dependent maintenance burden
@@ -60,4 +60,4 @@ Priority: `P1` quick win · `P2` moderate · `P3` large effort
- Impact: medium-high — essential for setups with many devices/targets; enables quick filtering (e.g. "bedroom", "desk", "gaming")
- [x] `P3` **PWA / mobile layout** — Mobile-first layout + "Add to Home Screen" manifest
- [ ] `P1` **Collapse dashboard running target stats** — Show only FPS chart by default; uptime, errors, and pipeline timings in an expandable section collapsed by default
- [ ] `P1` **Review protocol badge on LED target cards** — Review and improve the protocol badge display on LED target cards
- [x] `P1` **Review protocol badge on LED target cards** — Review and improve the protocol badge display on LED target cards

View File

@@ -110,10 +110,10 @@ async def create_device(
detail=f"Failed to connect to {device_type} device at {device_url}: {e}"
)
# Resolve auto_shutdown default: True for adalight, False otherwise
# Resolve auto_shutdown default: False for all types
auto_shutdown = device_data.auto_shutdown
if auto_shutdown is None:
auto_shutdown = device_type == "adalight"
auto_shutdown = False
# Create device in storage
device = store.create_device(

View File

@@ -29,8 +29,7 @@ class SerialDeviceProvider(LEDDeviceProvider):
# power_control: can blank LEDs by sending all-black pixels
# brightness_control: software brightness (multiplies pixel values before sending)
# health_check: serial port availability probe
# auto_restore: blank LEDs when targets stop
return {"manual_led_count", "power_control", "brightness_control", "health_check", "auto_restore"}
return {"manual_led_count", "power_control", "brightness_control", "health_check"}
async def check_health(self, url: str, http_client, prev_health=None) -> DeviceHealth:
# Generic serial port health check — enumerate COM ports

View File

@@ -82,6 +82,7 @@ class WLEDClient(LEDClient):
self._client: Optional[httpx.AsyncClient] = None
self._ddp_client: Optional[DDPClient] = None
self._connected = False
self._pre_connect_state: Optional[dict] = None
async def connect(self) -> bool:
"""Establish connection to WLED device.
@@ -107,6 +108,9 @@ class WLEDClient(LEDClient):
)
self.use_ddp = True
# Snapshot device state BEFORE any mutations (for auto-restore)
self._pre_connect_state = await self.snapshot_device_state()
# Create DDP client if needed
if self.use_ddp:
self._ddp_client = DDPClient(self.host, rgbw=False)
@@ -465,7 +469,14 @@ class WLEDClient(LEDClient):
# ===== LEDClient abstraction methods =====
async def snapshot_device_state(self) -> Optional[dict]:
"""Snapshot WLED state before streaming (on, lor, AudioReactive)."""
"""Snapshot WLED state (on, lor, AudioReactive).
If connect() already captured a pre-mutation snapshot, returns that
instead of the current (potentially already-modified) state.
"""
# Return pre-connect snapshot if available (captured before DDP setup)
if hasattr(self, '_pre_connect_state') and self._pre_connect_state is not None:
return self._pre_connect_state
if not self._client:
return None
try:

View File

@@ -165,6 +165,7 @@ class ProcessorManager:
send_latency_ms=send_latency_ms,
rgbw=rgbw,
zone_mode=ds.zone_mode,
auto_shutdown=ds.auto_shutdown,
)
# ===== EVENT SYSTEM (state change notifications) =====

View File

@@ -75,6 +75,7 @@ class DeviceInfo:
send_latency_ms: int = 0
rgbw: bool = False
zone_mode: str = "combined"
auto_shutdown: bool = False
@dataclass

View File

@@ -197,8 +197,10 @@ class WledTargetProcessor(TargetProcessor):
self._task = None
await asyncio.sleep(0.05)
# Restore device state
# Restore device state (only if auto_shutdown is enabled)
if self._led_client and self._device_state_before:
device_info = self._ctx.get_device_info(self._device_id)
if device_info and device_info.auto_shutdown:
await self._led_client.restore_device_state(self._device_state_before)
self._device_state_before = None

View File

@@ -24,7 +24,7 @@ import {
ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP,
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
ICON_WARNING,
ICON_WARNING, ICON_PALETTE, ICON_WRENCH,
} from '../core/icons.js';
import { EntitySelect } from '../core/entity-palette.js';
import { wrapCard } from '../core/card-colors.js';
@@ -163,6 +163,24 @@ class TargetEditorModal extends Modal {
const targetEditorModal = new TargetEditorModal();
function _protocolBadge(device, target) {
const dt = device?.device_type;
if (!dt || dt === 'wled') {
const proto = target.protocol === 'http' ? 'HTTP' : 'DDP';
return `${target.protocol === 'http' ? ICON_GLOBE : ICON_RADIO} ${proto}`;
}
const map = {
openrgb: [ICON_PALETTE, 'OpenRGB SDK'],
adalight: [ICON_PLUG, t('targets.protocol.serial')],
ambiled: [ICON_PLUG, t('targets.protocol.serial')],
mqtt: [ICON_GLOBE, 'MQTT'],
ws: [ICON_GLOBE, 'WebSocket'],
mock: [ICON_WRENCH, 'Mock'],
};
const [icon, label] = map[dt] || [ICON_PLUG, dt];
return `${icon} ${label}`;
}
let _targetNameManuallyEdited = false;
function _autoGenerateTargetName() {
@@ -936,7 +954,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
<div class="stream-card-props">
<span class="stream-card-prop stream-card-link" title="${t('targets.device')}" onclick="event.stopPropagation(); navigateToCard('targets','led','led-devices','data-device-id','${target.device_id}')">${ICON_LED} ${escapeHtml(deviceName)}</span>
<span class="stream-card-prop" title="${t('targets.fps')}">${ICON_FPS} ${target.fps || 30}</span>
${device?.device_type === 'wled' || !device ? `<span class="stream-card-prop" title="${t('targets.protocol')}">${target.protocol === 'http' ? ICON_GLOBE : ICON_RADIO} ${(target.protocol || 'ddp').toUpperCase()}</span>` : `<span class="stream-card-prop" title="${t('targets.protocol')}">${ICON_PLUG} ${t('targets.protocol.serial')}</span>`}
<span class="stream-card-prop" title="${t('targets.protocol')}">${_protocolBadge(device, target)}</span>
<span class="stream-card-prop stream-card-prop-full${cssId ? ' stream-card-link' : ''}" title="${t('targets.color_strip_source')}"${cssId ? ` onclick="event.stopPropagation(); navigateToCard('targets','led','led-css','data-css-id','${cssId}')"` : ''}>${ICON_FILM} ${cssSummary}</span>
${bvs ? `<span class="stream-card-prop stream-card-prop-full stream-card-link" title="${t('targets.brightness_vs')}" onclick="event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${bvsId}')">${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}</span>` : ''}
${target.min_brightness_threshold > 0 ? `<span class="stream-card-prop" title="${t('targets.min_brightness_threshold')}">${ICON_SUN_DIM} &lt;${target.min_brightness_threshold} → off</span>` : ''}

View File

@@ -7,7 +7,7 @@
* - Navigation: network-first with offline fallback
*/
const CACHE_NAME = 'ledgrab-v21';
const CACHE_NAME = 'ledgrab-v22';
// Only pre-cache static assets (no auth required).
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.