91 lines
2.8 KiB
Python
91 lines
2.8 KiB
Python
"""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": { "<name>": [ {"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
|