feat: roadmap batch (2026-06-19) — solar/linear-light/dither/nanoleaf + integrations
Eight roadmap features from the 2026-06-19 review, each a full vertical (backend + tests + frontend + i18n en/ru/zh); ~67 new unit tests: - automations: SolarRule sunrise/sunset trigger (new utils/solar.py, shared with the daylight cycle; window logic mirrors TimeOfDayRule) - ci: best-effort arm64 multi-arch Docker manifest via QEMU + docker manifest (release.yml; amd64 path untouched, continue-on-error) - game-integration: wire the orphaned LoLPoller via a LoLPollManager + a shared runtime_state module (poll lifecycle on enable/CRUD/startup/shutdown) - ui: color-harmony gradient generator (complementary/analogous/triadic/...) - effects: audio-reactive palette modulation (new audio_energy_tap; brightness/ saturation modulation across all 12 procedural effects) - capture: linear-light blending + spatio-temporal dithering, opt-in per calibration (new utils/linear_light.py, utils/dither.py) - devices: Nanoleaf extControl v2 per-panel UDP streaming (per_panel mode) Also bundles the pending 2026-06-18 production-review fixes and other in-progress work already in the working tree (manual-trigger rule, etc.), since they share files and could not be cleanly separated. Gate: ruff + tsc clean; pytest 2654 passed / 2 skipped. The single failing test (automation manual_trigger handler coverage) is a separate in-progress item owned elsewhere, intentionally left as-is.
This commit is contained in:
@@ -131,3 +131,43 @@ def test_build_frame_total_length(led_count):
|
||||
frame = client._build_frame(pixels, brightness=255)
|
||||
|
||||
assert len(frame) == 6 + led_count * 3
|
||||
|
||||
|
||||
async def test_close_settles_before_port_close(monkeypatch):
|
||||
"""close() must let the board paint the black frame before resetting it.
|
||||
|
||||
The black frame has to be written AND given settle time before
|
||||
``serial.close()`` toggles DTR (Arduino auto-reset). If the reset wins the
|
||||
race the strip latches its last lit frame and "stays on". This guards the
|
||||
ordering: write → flush → sleep(settle) → close.
|
||||
"""
|
||||
import concurrent.futures
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ledgrab.core.devices import adalight_client as mod
|
||||
|
||||
client = _make_client(led_count=3)
|
||||
events: list[str] = []
|
||||
|
||||
serial = MagicMock()
|
||||
serial.is_open = True
|
||||
serial.write.side_effect = lambda *_a, **_k: events.append("write")
|
||||
serial.flush.side_effect = lambda *_a, **_k: events.append("flush")
|
||||
serial.close.side_effect = lambda *_a, **_k: events.append("close")
|
||||
|
||||
client._serial = serial
|
||||
client._connected = True
|
||||
client._tx_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
||||
|
||||
async def fake_sleep(_seconds):
|
||||
events.append(f"sleep:{_seconds}")
|
||||
|
||||
monkeypatch.setattr(mod.asyncio, "sleep", fake_sleep)
|
||||
|
||||
await client.close()
|
||||
|
||||
# Black frame is written and flushed, the board is given settle time, and
|
||||
# ONLY THEN is the port closed (which resets the board).
|
||||
assert events == ["write", "flush", f"sleep:{mod.BLACK_FRAME_SETTLE_DELAY}", "close"]
|
||||
assert mod.BLACK_FRAME_SETTLE_DELAY > 0
|
||||
|
||||
Reference in New Issue
Block a user