Emit per-registry pin mode flags for the visual editor - #1163
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1163 +/- ##
=======================================
Coverage 99.42% 99.43%
=======================================
Files 213 213
Lines 15957 15976 +19
=======================================
+ Hits 15866 15885 +19
Misses 91 91
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR extends the device-builder component catalog pipeline to derive and ship a per-pin-registry set of allowed mode flags (from ESPHome’s live PIN_SCHEMA_REGISTRY) so the Visual Editor can hide invalid long-form Mode checkboxes for a given pin provider (e.g. pca9554 vs native esp32).
Changes:
- Add sync-time introspection in
script/sync_components.pyto generatedefinitions/pin_registry_modes.index.jsonand ship it in the wheel. - Add a backend loader + cache and a new WS command
components/get_pin_registry_modesto expose the{registry_key: [mode_flag, ...]}map. - Add tests covering schema-unwrapping, extraction behavior, loader fallbacks, and controller caching/endpoint behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
script/sync_components.py |
Generates the per-registry mode-flag index from ESPHome’s pin schema registry and writes it atomically. |
esphome_device_builder/definitions/__init__.py |
Adds a loader for the new pin registry modes artefact with fallback behavior. |
esphome_device_builder/controllers/components/controller.py |
Loads/caches the map at startup and exposes it via components/get_pin_registry_modes. |
docs/API.md |
Documents the new components/get_pin_registry_modes API and its semantics. |
pyproject.toml |
Includes definitions/pin_registry_modes.index.json in package data. |
esphome_device_builder/definitions/pin_registry_modes.index.json |
Adds the generated registry→modes artefact to the repo/wheel. |
tests/test_sync_components_pin_registry_modes.py |
New unit/integration tests for deriving allowed mode flags from ESPHome schemas. |
tests/test_definitions_loader.py |
Adds tests for the loader’s missing/corrupt/valid JSON behaviors. |
tests/controllers/test_components.py |
Adds tests ensuring ComponentCatalog.load() caches the map and the endpoint returns it. |
PR Review — Emit per-registry pin mode flags for the visual editorSolid, well-tested enhancement — no blocking issues. Merge-ready once the optional diagnosability suggestions are weighed.
🟢 Suggestions1. Per-stem import failure is silently dropped (`script/sync_components.py`, L2475-2480)
This is low risk because catalog PRs are human-gated (a shrinking diff is visible in review), but a for stem in component_stems:
with contextlib.suppress(Exception):
loader.get_component(stem)Checklist
Silent Failure Analysis🟡 **MEDIUM** — broad suppression masking partial failure (`script/sync_components.py:2490-2496`)Risk: contextlib.suppress(Exception) with zero logging swallows every import error per stem, so if a real pin provider (e.g. pca9554) fails to import due to an unexpected esphome API change it silently drops from the map — and because the only warning fires when the map is entirely empty, this partial loss is invisible and the committed artefact degrades to 'show every flag' for that provider with no signal. Fix: Log the suppressed exception at debug level (or count failures and warn if any provider that should register a pin schema was dropped) so partial degradation is observable in the sync output. |
The long-form pin Mode checkboxes a value supports depend on its pin
provider: an I2C expander like pca9554 allows only input/output, a shift
register sn74hc595 only output, while a native esp32 pin allows all five.
The catalog attaches the generic esp32 flag set to every pin field, so
the editor offers flags ESPHome rejects on an expander pin.
The constraint is provider-keyed, not field-keyed (the generic gpio
field can hold any provider's value), so introspect ESPHome's live
PIN_SCHEMA_REGISTRY at sync time and emit a global
pin_registry_modes.index.json map of {provider_key: [allowed_modes]}.
Native target platforms allow every checkbox flag, so they're excluded
(scoping a native pin would be a no-op); only external providers, matched
against the provider key in the pin value, restrict the set.
The components controller loads it and exposes it over
components/get_pin_registry_modes; the frontend scopes the Mode
checkboxes against it (separate change). Missing artefact or a native pin
falls back to showing every flag.
841e142 to
6e91249
Compare
Validate the decoded payload is a mapping and drop entries whose value isn't a list (keeping only string flags), so a hand-mangled or shape-drifted artefact degrades to showing every flag rather than crashing startup. Addresses review on #1163.
|
Thanks @copilot-pull-request-reviewer — both addressed in 583caf3:
|
What does this implement/fix?
The long-form pin Mode checkboxes in the visual editor (
input/output/pullup/pulldown/open_drain) are offered for every pin, but each external pin provider in ESPHome'sPIN_SCHEMA_REGISTRYallows only a subset: an I2C expander likepca9554permits justinput/output, a shift registersn74hc595justoutput. The catalog attaches the generic esp32 flag set to every pin field, so the editor lets users build configs ESPHome rejects ([pullup] is an invalid option for [mode]).The constraint is provider-keyed, not field-keyed (the
switch.gpiopinfield is generic and can hold any provider's value, selected by the provider key in the value), so it can't be baked per-field. This emits it as a global map the frontend consults at render time.Implementation
PIN_SCHEMA_REGISTRYat sync time (_build_pin_registry_modes), reusing_get_esphome_loader(). Each provider registers on import, so every component is imported to populate the registry, then_pin_registry_allowed_modeswalks each registered pin schema to itsmodemapping (_pin_schema_mode_mappingunwrapsvol.All/Schema) and reads the allowed flag keys.Platformenum (esp32) and bare platform-name strings (rp2040, bk72xx), so the filter drops any key in{p.value for p in Platform}rather than relying on the key type. The result is a clean{provider_key: [allowed_modes]}map (19 providers: pca9554, pcf8574, mcp23xxx, sx1509, sn74hc165/595, tca9555, …).definitions/pin_registry_modes.index.json(_emit_pin_registry_modes_index, atomic temp-then-replace, mirroringsync_boards's featured-index emit). Skipped on a--limit-componentdebug run whose partial registry would clobber the committed artefact.definitions/__init__.pygainsload_pin_registry_modes_index()(missing / malformed ->{}, mirroring the board / featured loaders).ComponentCatalog.load()caches it;components/get_pin_registry_modesexposes it. Documented indocs/API.md. Added to the wheelpackage-data.The committed
pin_registry_modes.index.jsonis generated by the script; the nightly sync owns regeneration going forward (the catalog index/bodies are intentionally not regenerated in this PR).Related issue or feature (if applicable):
device-builder-frontendPR)Types of changes
bugfixnew-featureenhancementbreaking-changerefactordocsmaintenancecidependenciesFrontend coordination
components/get_pin_registry_modes(links this PR)Checklist
ruff,codespell, yaml/json/python checks).tests/where applicable.components.index.json/definitions/components/*.jsonhave not been hand-edited.docs/ARCHITECTURE.mdand/ordocs/API.md.Testing
tests/test_sync_components_pin_registry_modes.py—_pin_schema_mode_mappingunwrap/scalar cases,_pin_registry_allowed_modesflag-key extraction,_build_pin_registry_modesagainst the installed esphome (pca9554 / pcf8574 input-output, mcp23xxx +pullup, sn74hc595 output-only, sn74hc165 input-only), and exclusion of native target platforms (enum- and string-keyed).tests/test_definitions_loader.py—load_pin_registry_modes_indexmissing / corrupt / reads-json.tests/controllers/test_components.py— endpoint returns the cached map;load()populates it.ruff check+ruff format --checkclean; adjacent suites (test_sync_components_emit_split,test_event_payload_contracts,test_sync_components_pin_long_form) green.