registry.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. """The zEPD model registry: loads drivers/*.json and merges each named model
  2. over default.json.
  3. Pure Python, no ESPHome dependency - see claude/DESCRIPTOR_REGISTRY_PLAN.md.
  4. Both display.py (at ESPHome compile time) and standalone host tooling
  5. (check_descriptors.py and friends) import this module directly.
  6. """
  7. from __future__ import annotations
  8. import json
  9. from pathlib import Path
  10. from typing import Any
  11. DRIVERS_DIR = Path(__file__).parent / "drivers"
  12. def _merge(base: Any, overlay: Any) -> Any:
  13. """Recursive dict merge. Named dict keys merge recursively; anything
  14. anonymous/positional (list elements) replaces whole. A key present in
  15. `overlay` but not in `base` is dropped silently - the accumulated
  16. `base` (built up from default.json first) is the implicit schema."""
  17. if isinstance(base, dict) and isinstance(overlay, dict):
  18. out = dict(base)
  19. for key, value in overlay.items():
  20. if key in out:
  21. out[key] = _merge(out[key], value)
  22. # else: unknown key, dropped - see unknown_keys() for a warning pass.
  23. return out
  24. return overlay
  25. def unknown_keys(base: dict, overlay: dict, prefix: str = "") -> list[str]:
  26. """Dotted-path keys present in `overlay` that don't exist anywhere in
  27. `base` - a likely typo, surfaced as a warning (not an error) by callers."""
  28. found: list[str] = []
  29. for key, value in overlay.items():
  30. path = f"{prefix}.{key}" if prefix else key
  31. if key not in base:
  32. found.append(path)
  33. elif isinstance(base.get(key), dict) and isinstance(value, dict):
  34. found.extend(unknown_keys(base[key], value, path))
  35. return found
  36. def load_default(drivers_dir: Path = DRIVERS_DIR) -> dict:
  37. with open(drivers_dir / "default.json") as f:
  38. return json.load(f)
  39. def load_model(name: str, drivers_dir: Path = DRIVERS_DIR, overlay: dict | None = None) -> dict:
  40. """Load one model: default.json, then drivers/<name>.json (skipped when
  41. name == "default"), then an optional YAML-provided overlay dict."""
  42. merged = load_default(drivers_dir)
  43. if name != "default":
  44. path = drivers_dir / f"{name}.json"
  45. with open(path) as f:
  46. model_json = json.load(f)
  47. unk = unknown_keys(merged, model_json)
  48. if unk:
  49. print(f"warning: {path}: unknown key(s), possible typo: {', '.join(unk)}")
  50. merged = _merge(merged, model_json)
  51. if overlay:
  52. unk = unknown_keys(merged, overlay)
  53. if unk:
  54. print(f"warning: model_overlay for {name!r}: unknown key(s), possible typo: {', '.join(unk)}")
  55. merged = _merge(merged, overlay)
  56. return merged
  57. def list_models(drivers_dir: Path = DRIVERS_DIR) -> list[str]:
  58. return sorted(p.stem for p in drivers_dir.glob("*.json") if p.stem != "default")