"""Parse controller JSON (v1) and apply brightness, presets, OTA patterns, etc.""" import json import socket from binary_envelope import parse_binary_envelope from utils import convert_and_reorder_colors try: import uos as os except ImportError: import os def process_data(payload, settings, presets, controller_ip=None): """Read one controller message; binary v1 envelope or JSON v1, then apply fields.""" data = None if isinstance(payload, (bytes, bytearray)): data = parse_binary_envelope(payload) if data is None: try: data = json.loads(payload) except (ValueError, TypeError): return else: try: data = json.loads(payload) except (ValueError, TypeError): return print(payload) if data.get("v", "") != "1": return if "b" in data: apply_brightness(data, settings, presets) if "presets" in data: apply_presets(data, settings, presets) if "clear_presets" in data: apply_clear_presets(data, presets) if "select" in data: apply_select(data, settings, presets) if "default" in data: apply_default(data, settings, presets) if "manifest" in data: apply_patterns_ota(data, presets, controller_ip=controller_ip) if "save" in data and ("presets" in data or "default" in data): presets.save() if "save" in data and "clear_presets" in data: presets.save() if "save" in data and "b" in data: settings.save() def apply_brightness(data, settings, presets): try: presets.b = max(0, min(255, int(data["b"]))) settings["brightness"] = presets.b except (TypeError, ValueError): pass def apply_presets(data, settings, presets): presets_map = data["presets"] for id, preset_data in presets_map.items(): if not preset_data: continue color_key = "c" if "c" in preset_data else ("colors" if "colors" in preset_data else None) if color_key is not None: try: preset_data[color_key] = convert_and_reorder_colors( preset_data[color_key], settings ) except (TypeError, ValueError, KeyError): continue presets.edit(id, preset_data) print(f"Edited preset {id}: {preset_data.get('name', '')}") def apply_select(data, settings, presets): select_map = data["select"] device_name = settings["name"] select_list = select_map.get(device_name, []) if not select_list: return preset_name = select_list[0] step = select_list[1] if len(select_list) > 1 else None presets.select(preset_name, step=step) def apply_clear_presets(data, presets): clear_value = data.get("clear_presets") if isinstance(clear_value, bool): should_clear = clear_value elif isinstance(clear_value, int): should_clear = bool(clear_value) elif isinstance(clear_value, str): should_clear = clear_value.lower() in ("true", "1", "yes", "on") else: should_clear = False if not should_clear: return presets.delete_all() print("Cleared all presets.") def apply_default(data, settings, presets): targets = data.get("targets") or [] default_name = data["default"] if ( settings["name"] in targets and isinstance(default_name, str) and default_name in presets.presets ): settings["default"] = default_name settings.save() def _parse_http_url(url): """Parse http://host[:port]/path into (host, port, path).""" if not isinstance(url, str): raise ValueError("url must be a string") if not url.startswith("http://"): raise ValueError("only http:// URLs are supported") remainder = url[7:] slash_idx = remainder.find("/") if slash_idx == -1: host_port = remainder path = "/" else: host_port = remainder[:slash_idx] path = remainder[slash_idx:] if ":" in host_port: host, port_s = host_port.rsplit(":", 1) port = int(port_s) else: host = host_port port = 80 if not host: raise ValueError("missing host") return host, port, path def _http_get_raw(url, timeout_s=10.0): host, port, path = _parse_http_url(url) req = ( "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" % (path, host) ).encode("utf-8") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.settimeout(timeout_s) sock.connect((host, int(port))) sock.send(req) data = b"" while True: chunk = sock.recv(1024) if not chunk: break data += chunk finally: try: sock.close() except Exception: pass sep = b"\r\n\r\n" if sep not in data: raise OSError("invalid HTTP response") head, body = data.split(sep, 1) status_line = head.split(b"\r\n", 1)[0] if b" 200 " not in status_line: raise OSError("HTTP status not OK: %s" % status_line.decode("utf-8")) return body def _http_get_json(url, timeout_s=10.0): body = _http_get_raw(url, timeout_s=timeout_s) return json.loads(body.decode("utf-8")) def _http_get_text(url, timeout_s=10.0, controller_ip=None): # Support relative URLs from controller messages. if isinstance(url, str) and url.startswith("/"): if not controller_ip: raise OSError("controller IP unavailable for relative URL") url = "http://%s%s" % (controller_ip, url) try: body = _http_get_raw(url, timeout_s=timeout_s) return body.decode("utf-8") except Exception: # Fallback for mDNS/unresolvable host: retry against current controller IP. if not controller_ip or not isinstance(url, str) or not url.startswith("http://"): raise _host, _port, path = _parse_http_url(url) fallback = "http://%s:%d%s" % (controller_ip, _port, path) body = _http_get_raw(fallback, timeout_s=timeout_s) return body.decode("utf-8") def _safe_pattern_filename(name): if not isinstance(name, str): return False if not name.endswith(".py"): return False if "/" in name or "\\" in name or ".." in name: return False return True def apply_patterns_ota(data, presets, controller_ip=None): manifest_payload = data.get("manifest") if not manifest_payload: return try: if isinstance(manifest_payload, dict): manifest = manifest_payload elif isinstance(manifest_payload, str): manifest = _http_get_json(manifest_payload, timeout_s=20.0) else: print("patterns_ota: invalid manifest payload type") return files = manifest.get("files", []) if not isinstance(files, list) or not files: print("patterns_ota: no files in manifest") return try: os.mkdir("patterns") except OSError: pass updated = 0 for item in files: if not isinstance(item, dict): continue name = item.get("name") url = item.get("url") inline_code = item.get("code") if not _safe_pattern_filename(name): continue if isinstance(inline_code, str): code = inline_code elif isinstance(url, str): code = _http_get_text(url, timeout_s=20.0, controller_ip=controller_ip) else: continue with open("patterns/" + name, "w") as f: f.write(code) updated += 1 if updated > 0: presets.reload_patterns() print("patterns_ota: updated", updated, "pattern file(s)") else: print("patterns_ota: no valid files downloaded") except Exception as e: print("patterns_ota failed:", e)