""" 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