Add openrgb-hue: Hue scene gradients for OpenRGB lights

Applies gradients derived from 103 bundled Philips Hue scenes to
OpenRGB-controlled LEDs, with six mapping modes (sequence, per-device,
per-zone, matrix, mirror, shuffle) and five animation modes (static,
scroll, pingpong, pulse, wave). Typer/Rich CLI with dry-run preview,
device/zone targeting, and a scenes-update command to refresh the
bundled dataset from its source gist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 12:40:13 +02:00
co-authored by Claude Sonnet 5
commit 71921b5600
24 changed files with 3131 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
from unittest.mock import patch
import pytest
from openrgb_hue.scenes import (
BUNDLED_CSV_PATH,
SceneNotFoundError,
get_scene,
list_scene_names,
load_scenes,
refresh_bundled_csv,
)
SAMPLE_CSV = (
"Scene,Light,Red,Green,Blue,Brightness\n"
"Alpha,light.hue_1,255,0,0,255\n"
"Alpha,light.hue_2,0,255,0,200\n"
"Beta,light.hue_1,0,0,255,100\n"
)
@pytest.fixture
def sample_csv_path(tmp_path):
path = tmp_path / "scenes.csv"
path.write_text(SAMPLE_CSV, encoding="utf-8")
return path
def test_load_scenes_groups_by_name_preserving_order(sample_csv_path):
scenes = load_scenes(sample_csv_path)
assert list(scenes.keys()) == ["Alpha", "Beta"]
assert len(scenes["Alpha"].lights) == 2
assert len(scenes["Beta"].lights) == 1
def test_list_scene_names(sample_csv_path):
assert list_scene_names(sample_csv_path) == ["Alpha", "Beta"]
def test_get_scene_case_insensitive(sample_csv_path):
scene = get_scene("alpha", sample_csv_path)
assert scene.name == "Alpha"
def test_get_scene_missing_raises_with_suggestion(sample_csv_path):
with pytest.raises(SceneNotFoundError, match="Alpha"):
get_scene("Alphaa", sample_csv_path)
def test_bundled_csv_has_103_scenes_of_10_lights_each():
scenes = load_scenes(BUNDLED_CSV_PATH)
assert len(scenes) == 103
assert all(len(scene.lights) == 10 for scene in scenes.values())
def test_refresh_bundled_csv_writes_validated_data(tmp_path):
dest = tmp_path / "scenes.csv"
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def read(self):
return SAMPLE_CSV.encode("utf-8")
with patch("openrgb_hue.scenes.urllib.request.urlopen", return_value=FakeResponse()):
result = refresh_bundled_csv(dest=dest, url="https://example.invalid/scenes.csv")
assert result == dest
assert dest.read_text(encoding="utf-8") == SAMPLE_CSV
def test_refresh_bundled_csv_rejects_invalid_data(tmp_path):
dest = tmp_path / "scenes.csv"
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def read(self):
return b"not,a,valid,csv\n"
with patch("openrgb_hue.scenes.urllib.request.urlopen", return_value=FakeResponse()):
with pytest.raises(ValueError):
refresh_bundled_csv(dest=dest, url="https://example.invalid/scenes.csv")
assert not dest.exists()