Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ companion frontend PR if there is one.
- [ ] The code change is tested and works locally.
- [ ] Pre-commit hooks pass (`ruff`, `codespell`, yaml/json/python checks).
- [ ] Tests have been added or updated under `tests/` where applicable.
- [ ] `components.json` has **not** been hand-edited (regenerate via `script/sync_components.py` if a sync is needed).
- [ ] `components.index.json` / `definitions/components/*.json` have **not** been hand-edited (regenerate via `script/sync_components.py` if a sync is needed).
- [ ] Architecture-level changes are reflected in `docs/ARCHITECTURE.md` and/or `docs/API.md`.
51 changes: 40 additions & 11 deletions .github/workflows/sync-component-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ name: Sync component catalog

# Re-runs ``script/sync_components.py`` against the schema version
# matching the dashboard's installed ``esphome`` (or a specific
# version on manual dispatch) and opens a pull request when
# ``definitions/components.json`` changes.
# version on manual dispatch) and opens a pull request when the
# generated ``definitions/components.index.json`` or per-id body
# files under ``definitions/components/`` change.
#
# Triggers
# --------
Expand Down Expand Up @@ -107,7 +108,10 @@ jobs:
id: diff
run: |
set -euo pipefail
if git diff --quiet -- esphome_device_builder/definitions/components.json; then
if git diff --quiet -- \
esphome_device_builder/definitions/components.index.json \
esphome_device_builder/definitions/components/ \
&& [ -z "$(git ls-files --others --exclude-standard esphome_device_builder/definitions/components/)" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
Expand All @@ -124,16 +128,43 @@ jobs:
from collections import Counter
from pathlib import Path

NEW = Path("esphome_device_builder/definitions/components.json")
new_data = json.loads(NEW.read_text())
NEW_INDEX = Path("esphome_device_builder/definitions/components.index.json")
BODIES_DIR = Path("esphome_device_builder/definitions/components")

def load_bodies(index_blob: str, head_ref: str | None) -> tuple[dict, list[dict]]:
meta = json.loads(index_blob) if index_blob else {"components": []}
bodies: list[dict] = []
for entry in meta.get("components", []):
cid = entry.get("id")
if not cid:
continue
rel = BODIES_DIR / f"{cid}.json"
if head_ref is None:
if rel.is_file():
bodies.append({**entry, **json.loads(rel.read_text())})
else:
bodies.append(entry)
else:
try:
body_blob = subprocess.check_output(
["git", "show", f"{head_ref}:{rel}"],
text=True,
)
bodies.append({**entry, **json.loads(body_blob)})
except subprocess.CalledProcessError:
bodies.append(entry)
return meta, bodies

new_meta, new_components = load_bodies(NEW_INDEX.read_text(), None)
try:
old_blob = subprocess.check_output(
["git", "show", f"HEAD:{NEW}"],
old_index_blob = subprocess.check_output(
["git", "show", f"HEAD:{NEW_INDEX}"],
text=True,
)
old_data = json.loads(old_blob)
_, old_components = load_bodies(old_index_blob, "HEAD")
except subprocess.CalledProcessError:
old_data = {"components": []}
old_components = []
new_data = new_meta

def count_types(components: list[dict]) -> Counter:
counts: Counter[str] = Counter()
Expand All @@ -145,8 +176,6 @@ jobs:
walk(component.get("config_entries") or [])
return counts

old_components = old_data.get("components") or []
new_components = new_data.get("components") or []
old_ids = {c["id"] for c in old_components}
new_ids = {c["id"] for c in new_components}
added = sorted(new_ids - old_ids)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/sync-device-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: |
python -m pip install --upgrade pip
# The device sync only needs the base package (pyyaml +
# components.json reader). No esphome introspection here.
# component catalog reader). No esphome introspection here.
pip install -e '.[test]'

- name: Run sync_esphome_devices
Expand All @@ -65,7 +65,7 @@ jobs:

- name: Validate definitions
# Re-validates every board (curated + imported) against the
# JSON Schema and the components.json cross-references.
# JSON Schema and the component catalog cross-references.
run: python script/validate_definitions.py

- name: Regenerate boards.json
Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ When changing the sync script or catalog handling, watch for these:
| `esphome_device_builder/controllers/*.py` | One file per API surface (components, boards, labels, ...). Larger surfaces (devices, firmware, remote_build) are packages — same shape, with a `controller.py` for the main class plus per-concern submodules and an `__init__.py` re-exporting the controller. |
| `esphome_device_builder/models/*.py` | Data classes (mashumaro) — pure shape, no logic |
| `esphome_device_builder/api/ws.py` | WebSocket dispatch |
| `esphome_device_builder/definitions/components.json` | Generated; do not hand-edit |
| `esphome_device_builder/definitions/components.index.json` + `components/<id>.json` | Generated; do not hand-edit. Slim index loaded eagerly; per-id bodies hydrate lazily via `ComponentCatalog.get_body`. |
| `esphome_device_builder/definitions/boards/<id>/manifest.yaml` | Curated; hand-edited |
| `script/sync_components.py` | Regenerates the component catalog |
| `script/check_catalog.py` | Smoke test for popular components |
Expand All @@ -729,10 +729,10 @@ When changing the sync script or catalog handling, watch for these:

## Things not to do

- **Don't hand-edit `components.json`.** Regenerate via
`script/sync_components.py`. CI runs the sync nightly and opens a
PR with diff summary + smoke-test verification — that's the
intended update path.
- **Don't hand-edit `components.index.json` / `components/<id>.json`.**
Regenerate via `script/sync_components.py`. CI runs the sync nightly
and opens a PR with diff summary + smoke-test verification — that's
the intended update path.
- **Don't auto-merge catalog PRs.** Schema regressions and sync-
script bugs both surface as PR diffs; a human gate catches them
before they ship. The diff summary in the PR body is designed for
Expand Down
13 changes: 9 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ esphome_device_builder/
└── definitions/ # Data files
├── boards/ # board YAML manifests
├── components.json # components definitions (auto generated from schema.esphome.io)
├── components.index.json # slim component index (auto generated, loaded eagerly)
├── components/ # per-id component bodies (auto generated, hydrated lazily)
└── schemas/ # JSON schemas
```

Expand Down Expand Up @@ -203,9 +204,13 @@ firmware/install {configuration} → QUEUED → RUNNING → output... → COMPLE

## Component Catalog

`definitions/components.json` is generated by `script/sync_components.py`
from ESPHome's pre-built schema bundle (https://schema.esphome.io). Schema +
narrow live `esphome` introspection cover most fields; `multi_conf`,
`definitions/components.index.json` (slim) plus per-id bodies at
`definitions/components/<id>.json` are generated by `script/sync_components.py`
from ESPHome's pre-built schema bundle (https://schema.esphome.io). The
slim index loads eagerly at dashboard startup; bodies hydrate on demand
via `ComponentCatalog.get_body` through a bounded LRU so an idle
dashboard doesn't carry the per-field config_entries trees in RAM.
Schema + narrow live `esphome` introspection cover most fields; `multi_conf`,
`platform_defaults`, `supported_platforms`, type refinement (boolean / float
recovery), and `unit_of_measurement` autocomplete options come from the live
package. Component-level descriptions and titles fall back to the docs MDX
Expand Down
Loading
Loading