Add profile/preset/sequence JSON import and export; map preset mode to wire n6 with a mode dropdown for multi-mode patterns; zone edit shows presets or sequences only with content_kind on save; update catalogue and tests for merged pattern names. Co-authored-by: Cursor <cursoragent@cursor.com>
268 lines
8.5 KiB
Python
268 lines
8.5 KiB
Python
"""
|
|
Message builder for LED driver API communication.
|
|
|
|
Builds JSON messages according to the LED driver API specification
|
|
for sending presets and select commands over the transport (e.g. serial).
|
|
"""
|
|
|
|
import json
|
|
|
|
|
|
def build_message(presets=None, select=None, save=False, default=None):
|
|
"""
|
|
Build an API message (presets and/or select) as a JSON string.
|
|
|
|
Args:
|
|
presets: Dictionary mapping preset names to preset objects, or None
|
|
select: Dictionary mapping device names to select lists, or None
|
|
|
|
Returns:
|
|
JSON string ready to send over the transport
|
|
|
|
Example:
|
|
message = build_message(
|
|
presets={
|
|
"red_blink": {
|
|
"pattern": "blink",
|
|
"colors": ["#FF0000"],
|
|
"delay": 200,
|
|
"brightness": 255,
|
|
"auto": True
|
|
}
|
|
},
|
|
select={
|
|
"device1": ["red_blink"]
|
|
}
|
|
)
|
|
"""
|
|
message = {
|
|
"v": "1"
|
|
}
|
|
|
|
if presets:
|
|
message["presets"] = presets
|
|
# When sending presets, optionally include a save flag so the
|
|
# led-driver can persist them.
|
|
if save:
|
|
message["save"] = True
|
|
|
|
if select:
|
|
message["select"] = select
|
|
|
|
if default is not None:
|
|
message["default"] = default
|
|
|
|
return json.dumps(message)
|
|
|
|
|
|
def build_select_message(device_name, preset_name, step=None):
|
|
"""
|
|
Build a select message for a single device.
|
|
|
|
Args:
|
|
device_name: Name of the device
|
|
preset_name: Name of the preset to select
|
|
step: Optional step value for synchronization
|
|
|
|
Returns:
|
|
Dictionary with select field ready to use in build_message
|
|
|
|
Example:
|
|
select = build_select_message("device1", "rainbow_preset", step=10)
|
|
message = build_message(select=select)
|
|
"""
|
|
select_list = [preset_name]
|
|
if step is not None:
|
|
select_list.append(step)
|
|
|
|
return {device_name: select_list}
|
|
|
|
|
|
def _hex_from_background_raw(bg_raw):
|
|
"""Coerce ``background`` / ``bg`` field to a ``#RRGGBB`` string (driver wire format)."""
|
|
if isinstance(bg_raw, str):
|
|
bg = bg_raw if bg_raw.startswith("#") else f"#{bg_raw}"
|
|
return bg
|
|
if isinstance(bg_raw, (list, tuple)) and len(bg_raw) == 3:
|
|
return f"#{int(bg_raw[0]):02x}{int(bg_raw[1]):02x}{int(bg_raw[2]):02x}"
|
|
return "#000000"
|
|
|
|
|
|
def resolve_preset_background_hex(preset_data, palette_colors=None):
|
|
"""
|
|
Resolved background as ``#RRGGBB``. When ``palette_colors`` is a non-empty list and
|
|
``background_palette_ref`` is set, uses that palette index; otherwise stored ``background`` / ``bg``.
|
|
"""
|
|
if not isinstance(preset_data, dict):
|
|
return "#000000"
|
|
pal = list(palette_colors) if isinstance(palette_colors, list) else []
|
|
ref = preset_data.get("background_palette_ref", preset_data.get("backgroundPaletteRef"))
|
|
if pal and ref is not None:
|
|
try:
|
|
idx = int(ref)
|
|
except (TypeError, ValueError):
|
|
idx = None
|
|
else:
|
|
if isinstance(idx, int) and 0 <= idx < len(pal):
|
|
c = pal[idx]
|
|
if isinstance(c, str) and c.strip().startswith("#"):
|
|
s = c.strip()
|
|
if len(s) == 7 and all(ch in "0123456789abcdefABCDEF" for ch in s[1:]):
|
|
return s.upper()
|
|
bg_raw = preset_data.get("background", preset_data.get("bg", "#000000"))
|
|
return _hex_from_background_raw(bg_raw)
|
|
|
|
|
|
def wire_n6(preset_data, default=0):
|
|
"""Resolve style mode for the wire (``n6``); preset may store ``mode`` or ``n6``."""
|
|
if not isinstance(preset_data, dict):
|
|
return default
|
|
if preset_data.get("mode") is not None:
|
|
try:
|
|
return max(0, int(preset_data["mode"]))
|
|
except (TypeError, ValueError):
|
|
pass
|
|
try:
|
|
return max(0, int(preset_data.get("n6", default) or 0))
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def build_preset_dict(preset_data, palette_colors=None):
|
|
"""
|
|
Convert preset data to API-compliant format.
|
|
|
|
Args:
|
|
preset_data: Dictionary with preset fields (may include name, pattern, colors, etc.)
|
|
palette_colors: Optional list of ``#RRGGBB`` strings for ``background_palette_ref`` resolution.
|
|
|
|
Returns:
|
|
Dictionary with preset in API-compliant format (without name field)
|
|
|
|
Example:
|
|
preset = build_preset_dict({
|
|
"name": "red_blink",
|
|
"pattern": "blink",
|
|
"colors": ["#FF0000"],
|
|
"delay": 200,
|
|
"brightness": 255,
|
|
"auto": True,
|
|
"n1": 0,
|
|
"n2": 0,
|
|
"n3": 0,
|
|
"n4": 0,
|
|
"n5": 0,
|
|
"n6": 0
|
|
})
|
|
"""
|
|
# Ensure colors are in hex format
|
|
colors = preset_data.get("colors", preset_data.get("c", ["#FFFFFF"]))
|
|
if colors:
|
|
# Convert RGB tuples to hex strings if needed
|
|
if isinstance(colors[0], list) and len(colors[0]) == 3:
|
|
# RGB tuple format [r, g, b]
|
|
colors = [f"#{r:02x}{g:02x}{b:02x}" for r, g, b in colors]
|
|
elif not isinstance(colors[0], str):
|
|
# Handle other formats - convert to hex
|
|
colors = ["#FFFFFF"]
|
|
# Ensure all colors start with #
|
|
colors = [c if c.startswith("#") else f"#{c}" for c in colors]
|
|
else:
|
|
colors = ["#FFFFFF"]
|
|
|
|
def _coerce_auto(raw):
|
|
if isinstance(raw, bool):
|
|
return raw
|
|
if raw is None:
|
|
return True
|
|
if isinstance(raw, int):
|
|
return raw != 0
|
|
if isinstance(raw, str):
|
|
lowered = raw.strip().lower()
|
|
if lowered in ("false", "0", "no", "off"):
|
|
return False
|
|
if lowered in ("true", "1", "yes", "on"):
|
|
return True
|
|
return True
|
|
|
|
auto_raw = preset_data.get("auto", preset_data.get("a", True))
|
|
auto_bool = _coerce_auto(auto_raw)
|
|
|
|
bg = resolve_preset_background_hex(preset_data, palette_colors)
|
|
|
|
# Build payload using the short keys expected by led-driver
|
|
preset = {
|
|
"p": preset_data.get("pattern", preset_data.get("p", "off")),
|
|
"c": colors,
|
|
"d": preset_data.get("delay", preset_data.get("d", 100)),
|
|
"b": preset_data.get("brightness", preset_data.get("b", preset_data.get("br", 127))),
|
|
"a": auto_bool,
|
|
"bg": bg,
|
|
"n1": preset_data.get("n1", 0),
|
|
"n2": preset_data.get("n2", 0),
|
|
"n3": preset_data.get("n3", 0),
|
|
"n4": preset_data.get("n4", 0),
|
|
"n5": preset_data.get("n5", 0),
|
|
"n6": wire_n6(preset_data),
|
|
}
|
|
|
|
return preset
|
|
|
|
|
|
def build_presets_dict(presets_data, palette_colors=None):
|
|
"""
|
|
Convert multiple presets to API-compliant format.
|
|
|
|
Args:
|
|
presets_data: Dictionary mapping preset names to preset data
|
|
palette_colors: Optional list of ``#RRGGBB`` strings for background palette ref resolution.
|
|
|
|
Returns:
|
|
Dictionary mapping preset names to API-compliant preset objects
|
|
|
|
Example:
|
|
presets = build_presets_dict({
|
|
"red_blink": {
|
|
"pattern": "blink",
|
|
"colors": ["#FF0000"],
|
|
"delay": 200
|
|
},
|
|
"blue_pulse": {
|
|
"pattern": "pulse",
|
|
"colors": ["#0000FF"],
|
|
"delay": 100
|
|
}
|
|
})
|
|
"""
|
|
result = {}
|
|
for preset_name, preset_data in presets_data.items():
|
|
result[preset_name] = build_preset_dict(preset_data, palette_colors)
|
|
return result
|
|
|
|
|
|
def build_select_dict(device_preset_mapping, step_mapping=None):
|
|
"""
|
|
Build a select dictionary mapping device names to select lists.
|
|
|
|
Args:
|
|
device_preset_mapping: Dictionary mapping device names to preset names
|
|
step_mapping: Optional dictionary mapping device names to step values
|
|
|
|
Returns:
|
|
Dictionary with select field ready to use in build_message
|
|
|
|
Example:
|
|
select = build_select_dict(
|
|
{"device1": "rainbow_preset", "device2": "pulse_preset"},
|
|
step_mapping={"device1": 10}
|
|
)
|
|
message = build_message(select=select)
|
|
"""
|
|
select = {}
|
|
for device_name, preset_name in device_preset_mapping.items():
|
|
select_list = [preset_name]
|
|
if step_mapping and device_name in step_mapping:
|
|
select_list.append(step_mapping[device_name])
|
|
select[device_name] = select_list
|
|
return select
|