Backend performance and code quality improvements

Performance (hot path):
- Fix double brightness: removed duplicate scaling from 9 device clients
  (wled, adalight, ambiled, openrgb, hue, spi, chroma, gamesense, usbhid,
  espnow) — processor loop is now the single source of brightness
- Bounded send_timestamps deque with maxlen, removed 3 cleanup loops
- Running FPS sum O(1) instead of sum()/len() O(n) per frame
- datetime.now(timezone.utc) → time.monotonic() with lazy conversion
- Device info refresh interval 30 → 300 iterations
- Composite: gate layer_snapshots copy on preview client flag
- Composite: versioned sub_streams snapshot (copy only on change)
- Composite: pre-resolved blend methods (dict lookup vs getattr)
- ApiInput: np.copyto in-place instead of astype allocation

Code quality:
- BaseJsonStore: RLock on get/delete/get_all/count (was created but unused)
- EntityNotFoundError → proper 404 responses across 15 route files
- Remove 21 defensive getattr(x,'tags',[]) — field guaranteed on all models
- Fix Dict[str,any] → Dict[str,Any] in template/audio_template stores
- Log 4 silenced exceptions (automation engine, metrics, system)
- ValueStream.get_value() now @abstractmethod
- Config.from_yaml: add encoding="utf-8"
- OutputTargetStore: remove 25-line _load override, use _legacy_json_keys
- BaseJsonStore: add _legacy_json_keys for migration support
- Remove unnecessary except Exception→500 from postprocessing list endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 15:06:29 +03:00
parent 1f047d6561
commit cdba98813b
37 changed files with 296 additions and 137 deletions
@@ -168,9 +168,7 @@ class AdalightClient(LEDClient):
else:
arr = np.array(pixels, dtype=np.uint16)
if brightness < 255:
arr = arr * brightness // 255
# Note: brightness already applied by processor loop (_cached_brightness)
np.clip(arr, 0, 255, out=arr)
rgb_bytes = arr.astype(np.uint8).tobytes()
return self._header + rgb_bytes
@@ -40,9 +40,7 @@ class AmbiLEDClient(AdalightClient):
else:
arr = np.array(pixels, dtype=np.uint16)
if brightness < 255:
arr = arr * brightness // 255
# Note: brightness already applied by processor loop (_cached_brightness)
# Clamp to 0250: values >250 are command bytes in AmbiLED protocol
np.clip(arr, 0, 250, out=arr)
rgb_bytes = arr.astype(np.uint8).tobytes()
@@ -145,7 +145,7 @@ class ChromaClient(LEDClient):
else:
pixel_arr = np.array(pixels, dtype=np.uint8)
bri_scale = brightness / 255.0
# Note: brightness already applied by processor loop (_cached_brightness)
device_info = CHROMA_DEVICES.get(self._chroma_device_type)
if not device_info:
return False
@@ -156,10 +156,7 @@ class ChromaClient(LEDClient):
# Chroma uses BGR packed as 0x00BBGGRR integers
colors = []
for i in range(n):
r, g, b = pixel_arr[i]
r = int(r * bri_scale)
g = int(g * bri_scale)
b = int(b * bri_scale)
r, g, b = int(pixel_arr[i][0]), int(pixel_arr[i][1]), int(pixel_arr[i][2])
colors.append(r | (g << 8) | (b << 16))
# Pad to max_leds if needed
@@ -115,7 +115,8 @@ class ESPNowClient(LEDClient):
else:
pixel_bytes = bytes(c for rgb in pixels for c in rgb)
frame = _build_frame(self._peer_mac, pixel_bytes, brightness)
# Note: brightness already applied by processor loop; pass 255 to firmware
frame = _build_frame(self._peer_mac, pixel_bytes, 255)
try:
self._serial.write(frame)
except Exception as e:
@@ -187,7 +187,7 @@ class GameSenseClient(LEDClient):
else:
pixel_arr = np.array(pixels, dtype=np.uint8)
bri_scale = brightness / 255.0
# Note: brightness already applied by processor loop (_cached_brightness)
# Use average color for single-zone devices, or first N for multi-zone
if len(pixel_arr) == 0:
@@ -195,9 +195,9 @@ class GameSenseClient(LEDClient):
# Compute average color for the zone
avg = pixel_arr.mean(axis=0)
r = int(avg[0] * bri_scale)
g = int(avg[1] * bri_scale)
b = int(avg[2] * bri_scale)
r = int(avg[0])
g = int(avg[1])
b = int(avg[2])
event_data = {
"game": GAME_NAME,
@@ -46,13 +46,13 @@ def _build_entertainment_frame(
header[15] = 0x00 # reserved
# Light data
bri_scale = brightness / 255.0
# Note: brightness already applied by processor loop (_cached_brightness)
data = bytearray()
for idx, (r, g, b) in enumerate(lights):
light_id = idx # 0-based light index in entertainment group
r16 = int(r * bri_scale * 257) # scale 0-255 to 0-65535
g16 = int(g * bri_scale * 257)
b16 = int(b * bri_scale * 257)
r16 = int(r * 257) # scale 0-255 to 0-65535
g16 = int(g * 257)
b16 = int(b * 257)
data += struct.pack(">BHHH", light_id, r16, g16, b16)
return bytes(header) + bytes(data)
@@ -302,9 +302,7 @@ class OpenRGBLEDClient(LEDClient):
return
self._last_sent_pixels = pixel_array.copy()
# Apply brightness scaling after dedup
if brightness < 255:
pixel_array = (pixel_array.astype(np.uint16) * brightness >> 8).astype(np.uint8)
# Note: brightness already applied by processor loop (_cached_brightness)
# Separate mode: resample full pixel array independently per zone
if self._zone_mode == "separate" and len(self._target_zones) > 1:
@@ -162,7 +162,7 @@ class SPIClient(LEDClient):
if not self._connected:
return
bri_scale = brightness / 255.0
# Note: brightness already applied by processor loop (_cached_brightness)
if isinstance(pixels, np.ndarray):
pixel_arr = pixels
@@ -176,7 +176,7 @@ class SPIClient(LEDClient):
except ImportError:
return
self._strip.setBrightness(brightness)
self._strip.setBrightness(255)
for i in range(min(len(pixel_arr), self._led_count)):
r, g, b = pixel_arr[i]
self._strip.setPixelColor(i, Color(int(r), int(g), int(b)))
@@ -185,7 +185,7 @@ class SPIClient(LEDClient):
elif self._spi:
# SPI bitbang path: convert RGB to WS2812 wire format
# Each bit is encoded as 3 SPI bits: 1=110, 0=100
scaled = (pixel_arr[:self._led_count].astype(np.float32) * bri_scale).astype(np.uint8)
scaled = pixel_arr[:self._led_count]
# GRB order for WS2812
grb = scaled[:, [1, 0, 2]]
raw_bytes = grb.tobytes()
@@ -100,7 +100,7 @@ class USBHIDClient(LEDClient):
else:
pixel_list = list(pixels)
bri_scale = brightness / 255.0
# Note: brightness already applied by processor loop (_cached_brightness)
# Build HID reports — split across multiple reports if needed
# Each report: [REPORT_ID][CMD][OFFSET_LO][OFFSET_HI][COUNT][R G B R G B ...]
@@ -119,9 +119,9 @@ class USBHIDClient(LEDClient):
for i, (r, g, b) in enumerate(chunk):
base = 5 + i * 3
report[base] = int(r * bri_scale)
report[base + 1] = int(g * bri_scale)
report[base + 2] = int(b * bri_scale)
report[base] = int(r)
report[base + 1] = int(g)
report[base + 2] = int(b)
reports.append(bytes(report))
offset += len(chunk)
@@ -378,9 +378,7 @@ class WLEDClient(LEDClient):
True if successful
"""
try:
if brightness < 255:
pixels = (pixels.astype(np.uint16) * brightness >> 8).astype(np.uint8)
# Note: brightness already applied by processor loop (_cached_brightness)
logger.debug(f"Sending {len(pixels)} LEDs via DDP")
self._ddp_client.send_pixels_numpy(pixels)
logger.debug(f"Successfully sent pixel colors via DDP")
@@ -419,7 +417,7 @@ class WLEDClient(LEDClient):
# Build WLED JSON state
payload = {
"on": True,
"bri": int(brightness),
"bri": 255, # brightness already applied by processor loop
"seg": [
{
"id": segment_id,
@@ -461,9 +459,7 @@ class WLEDClient(LEDClient):
else:
pixel_array = np.array(pixels, dtype=np.uint8)
if brightness < 255:
pixel_array = (pixel_array.astype(np.uint16) * brightness >> 8).astype(np.uint8)
# Note: brightness already applied by processor loop (_cached_brightness)
self._ddp_client.send_pixels_numpy(pixel_array)
# ===== LEDClient abstraction methods =====