Add software brightness control for Adalight devices

Emulates hardware brightness by multiplying pixel values before serial
send. Stored per-device and persisted across restarts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 10:56:19 +03:00
parent 27c97c3141
commit 77dd342c4c
5 changed files with 31 additions and 4 deletions

View File

@@ -323,8 +323,9 @@ async def get_device_brightness(
device_id: str, device_id: str,
_auth: AuthRequired, _auth: AuthRequired,
store: DeviceStore = Depends(get_device_store), store: DeviceStore = Depends(get_device_store),
manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Get current brightness from the WLED device.""" """Get current brightness from the device."""
device = store.get_device(device_id) device = store.get_device(device_id)
if not device: if not device:
raise HTTPException(status_code=404, detail=f"Device {device_id} not found") raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
@@ -332,6 +333,8 @@ async def get_device_brightness(
raise HTTPException(status_code=400, detail=f"Brightness control is not supported for {device.device_type} devices") raise HTTPException(status_code=400, detail=f"Brightness control is not supported for {device.device_type} devices")
try: try:
if device.device_type == "adalight":
return {"brightness": device.software_brightness}
provider = get_provider(device.device_type) provider = get_provider(device.device_type)
bri = await provider.get_brightness(device.url) bri = await provider.get_brightness(device.url)
return {"brightness": bri} return {"brightness": bri}
@@ -346,8 +349,9 @@ async def set_device_brightness(
body: dict, body: dict,
_auth: AuthRequired, _auth: AuthRequired,
store: DeviceStore = Depends(get_device_store), store: DeviceStore = Depends(get_device_store),
manager: ProcessorManager = Depends(get_processor_manager),
): ):
"""Set brightness on the WLED device directly.""" """Set brightness on the device."""
device = store.get_device(device_id) device = store.get_device(device_id)
if not device: if not device:
raise HTTPException(status_code=404, detail=f"Device {device_id} not found") raise HTTPException(status_code=404, detail=f"Device {device_id} not found")
@@ -359,6 +363,14 @@ async def set_device_brightness(
raise HTTPException(status_code=400, detail="brightness must be an integer 0-255") raise HTTPException(status_code=400, detail="brightness must be an integer 0-255")
try: try:
if device.device_type == "adalight":
device.software_brightness = bri
device.updated_at = __import__("datetime").datetime.utcnow()
store.save()
# Update runtime state so the processing loop picks it up
if device_id in manager._devices:
manager._devices[device_id].software_brightness = bri
return {"brightness": bri}
provider = get_provider(device.device_type) provider = get_provider(device.device_type)
await provider.set_brightness(device.url, bri) await provider.set_brightness(device.url, bri)
return {"brightness": bri} return {"brightness": bri}

View File

@@ -22,10 +22,10 @@ class AdalightDeviceProvider(LEDDeviceProvider):
@property @property
def capabilities(self) -> set: def capabilities(self) -> set:
# No hardware brightness control, no standby required
# 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
return {"manual_led_count", "power_control"} # brightness_control: software brightness (multiplies pixel values before sending)
return {"manual_led_count", "power_control", "brightness_control"}
def create_client(self, url: str, **kwargs) -> LEDClient: def create_client(self, url: str, **kwargs) -> LEDClient:
from wled_controller.core.adalight_client import AdalightClient from wled_controller.core.adalight_client import AdalightClient

View File

@@ -166,6 +166,8 @@ class DeviceState:
baud_rate: Optional[int] = None baud_rate: Optional[int] = None
health: DeviceHealth = field(default_factory=DeviceHealth) health: DeviceHealth = field(default_factory=DeviceHealth)
health_task: Optional[asyncio.Task] = None health_task: Optional[asyncio.Task] = None
# Software brightness for devices without hardware brightness (e.g. Adalight)
software_brightness: int = 255
# 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)
@@ -281,6 +283,7 @@ class ProcessorManager:
calibration: Optional[CalibrationConfig] = None, calibration: Optional[CalibrationConfig] = None,
device_type: str = "wled", device_type: str = "wled",
baud_rate: Optional[int] = None, baud_rate: Optional[int] = None,
software_brightness: int = 255,
): ):
"""Register a device for health monitoring. """Register a device for health monitoring.
@@ -291,6 +294,7 @@ class ProcessorManager:
calibration: Calibration config (creates default if None) calibration: Calibration config (creates default if None)
device_type: LED device type (e.g. "wled") device_type: LED device type (e.g. "wled")
baud_rate: Serial baud rate (for adalight devices) baud_rate: Serial baud rate (for adalight devices)
software_brightness: Software brightness 0-255 (for devices without hardware brightness)
""" """
if device_id in self._devices: if device_id in self._devices:
raise ValueError(f"Device {device_id} already registered") raise ValueError(f"Device {device_id} already registered")
@@ -305,6 +309,7 @@ class ProcessorManager:
calibration=calibration, calibration=calibration,
device_type=device_type, device_type=device_type,
baud_rate=baud_rate, baud_rate=baud_rate,
software_brightness=software_brightness,
) )
self._devices[device_id] = state self._devices[device_id] = state
@@ -775,6 +780,8 @@ class ProcessorManager:
if not state.is_running or state.led_client is None: if not state.is_running or state.led_client is None:
break break
brightness_value = int(led_brightness * 255) brightness_value = int(led_brightness * 255)
if device_state and device_state.software_brightness < 255:
brightness_value = brightness_value * device_state.software_brightness // 255
if state.led_client.supports_fast_send: if state.led_client.supports_fast_send:
state.led_client.send_pixels_fast(state.previous_colors, brightness=brightness_value) state.led_client.send_pixels_fast(state.previous_colors, brightness=brightness_value)
else: else:
@@ -803,6 +810,8 @@ class ProcessorManager:
if not state.is_running or state.led_client is None: if not state.is_running or state.led_client is None:
break break
brightness_value = int(led_brightness * 255) brightness_value = int(led_brightness * 255)
if device_state and device_state.software_brightness < 255:
brightness_value = brightness_value * device_state.software_brightness // 255
t_send_start = time.perf_counter() t_send_start = time.perf_counter()
if state.led_client.supports_fast_send: if state.led_client.supports_fast_send:
state.led_client.send_pixels_fast(led_colors, brightness=brightness_value) state.led_client.send_pixels_fast(led_colors, brightness=brightness_value)

View File

@@ -157,6 +157,7 @@ async def lifespan(app: FastAPI):
calibration=device.calibration, calibration=device.calibration,
device_type=device.device_type, device_type=device.device_type,
baud_rate=device.baud_rate, baud_rate=device.baud_rate,
software_brightness=device.software_brightness,
) )
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

@@ -33,6 +33,7 @@ class Device:
enabled: bool = True, enabled: bool = True,
device_type: str = "wled", device_type: str = "wled",
baud_rate: Optional[int] = None, baud_rate: Optional[int] = None,
software_brightness: int = 255,
calibration: Optional[CalibrationConfig] = None, calibration: Optional[CalibrationConfig] = None,
created_at: Optional[datetime] = None, created_at: Optional[datetime] = None,
updated_at: Optional[datetime] = None, updated_at: Optional[datetime] = None,
@@ -44,6 +45,7 @@ class Device:
self.enabled = enabled self.enabled = enabled
self.device_type = device_type self.device_type = device_type
self.baud_rate = baud_rate self.baud_rate = baud_rate
self.software_brightness = software_brightness
self.calibration = calibration or create_default_calibration(led_count) self.calibration = calibration or create_default_calibration(led_count)
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()
@@ -63,6 +65,8 @@ class Device:
} }
if self.baud_rate is not None: if self.baud_rate is not None:
d["baud_rate"] = self.baud_rate d["baud_rate"] = self.baud_rate
if self.software_brightness != 255:
d["software_brightness"] = self.software_brightness
return d return d
@classmethod @classmethod
@@ -87,6 +91,7 @@ class Device:
enabled=data.get("enabled", True), enabled=data.get("enabled", True),
device_type=data.get("device_type", "wled"), device_type=data.get("device_type", "wled"),
baud_rate=data.get("baud_rate"), baud_rate=data.get("baud_rate"),
software_brightness=data.get("software_brightness", 255),
calibration=calibration, calibration=calibration,
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())),