"""Saved device profiles for the BT Keyboard streamer. A profile is a named list of devices, each ``{"address", "label"}``. Saving records the MAC + friendly name of every device currently in the streamer; loading re-adds and reconnects them in one click, naming each automatically. All profiles live in a single JSON file in the app config dir so they're in one place and trivially portable: { "profiles": { "": [ {"address": "AA:..", "label": "Left PC"}, ... ] } } """ import json import os import threading from utils.constants import APPDATA_DIR _FILE = os.path.join(APPDATA_DIR, "bt_kbd_profiles.json") _lock = threading.Lock() def _load_all() -> dict: try: with open(_FILE, "r", encoding="utf-8") as f: d = json.load(f) except (OSError, json.JSONDecodeError): return {} profs = d.get("profiles") if isinstance(d, dict) else None return profs if isinstance(profs, dict) else {} def _save_all(profs: dict) -> None: os.makedirs(APPDATA_DIR, exist_ok=True) tmp = _FILE + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump({"profiles": profs}, f, indent=2) os.replace(tmp, _FILE) def list_profiles() -> list: """Names of all saved profiles, sorted.""" with _lock: return sorted(_load_all().keys()) def load_profile(name: str) -> list: """Return a profile's devices as a list of {"address","label"} dicts. Tolerates a malformed/missing file by returning whatever is valid.""" with _lock: devs = _load_all().get(name, []) out = [] if isinstance(devs, list): for d in devs: if isinstance(d, dict) and d.get("address"): out.append({"address": str(d["address"]), "label": str(d.get("label") or "")}) return out def save_profile(name: str, devices, transport: str = "mesh") -> None: """Save ``devices`` (iterable of (address, label) tuples or dicts) under ``name``, replacing any existing profile of that name. ``transport`` is recorded per device ("mesh" for ESP-NOW STA MACs, "ble" for legacy Bleak BT MACs) so the loader can migrate old profiles. Extra keys are ignored by load_profile for back-compat.""" norm = [] for d in devices: if isinstance(d, dict): addr, lbl = d.get("address"), d.get("label") or "" else: addr, lbl = d if addr: norm.append({"address": str(addr), "label": str(lbl or ""), "transport": transport}) with _lock: profs = _load_all() profs[name] = norm _save_all(profs) def delete_profile(name: str) -> bool: with _lock: profs = _load_all() if name in profs: del profs[name] _save_all(profs) return True return False