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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) =====
|
||||
|
||||
@@ -75,6 +75,7 @@ class DeviceInfo:
|
||||
send_latency_ms: int = 0
|
||||
rgbw: bool = False
|
||||
zone_mode: str = "combined"
|
||||
auto_shutdown: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -197,9 +197,11 @@ 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:
|
||||
await self._led_client.restore_device_state(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
|
||||
|
||||
# Close LED connection
|
||||
|
||||
@@ -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} <${target.min_brightness_threshold} → off</span>` : ''}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user