Skip to content

Emit per-registry pin mode flags for the visual editor - #1163

Merged
bdraco merged 2 commits into
mainfrom
enhancement/pin-registry-modes
Jun 3, 2026
Merged

Emit per-registry pin mode flags for the visual editor#1163
bdraco merged 2 commits into
mainfrom
enhancement/pin-registry-modes

Conversation

@bdraco

@bdraco bdraco commented Jun 3, 2026

Copy link
Copy Markdown
Member

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's PIN_SCHEMA_REGISTRY allows only a subset: an I2C expander like pca9554 permits just input / output, a shift register sn74hc595 just output. 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.gpio pin field 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

  • Introspect ESPHome's live PIN_SCHEMA_REGISTRY at 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_modes walks each registered pin schema to its mode mapping (_pin_schema_mode_mapping unwraps vol.All / Schema) and reads the allowed flag keys.
  • Native target platforms are excluded. They allow every checkbox flag, so scoping a native pin is a no-op; only external providers restrict. Natives register under both the Platform enum (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, …).
  • Emit definitions/pin_registry_modes.index.json (_emit_pin_registry_modes_index, atomic temp-then-replace, mirroring sync_boards's featured-index emit). Skipped on a --limit-component debug run whose partial registry would clobber the committed artefact.
  • definitions/__init__.py gains load_pin_registry_modes_index() (missing / malformed -> {}, mirroring the board / featured loaders).
  • ComponentCatalog.load() caches it; components/get_pin_registry_modes exposes it. Documented in docs/API.md. Added to the wheel package-data.

The committed pin_registry_modes.index.json is 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):

Types of changes

  • Bugfix (non-breaking change which fixes an issue) — bugfix
  • New feature (non-breaking change which adds functionality) — new-feature
  • Enhancement to an existing feature — enhancement
  • Breaking change (fix or feature that would cause existing functionality to not work as expected) — breaking-change
  • Refactor (no behaviour change) — refactor
  • Documentation only — docs
  • Maintenance / chore — maintenance
  • CI / workflow change — ci
  • Dependencies bump — dependencies

Frontend coordination

  • No frontend change needed
  • Companion frontend PR: scopes the Mode checkboxes against components/get_pin_registry_modes (links this PR)

Checklist

  • 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.index.json / definitions/components/*.json have not been hand-edited.
  • Architecture-level changes are reflected in docs/ARCHITECTURE.md and/or docs/API.md.

Testing

  • tests/test_sync_components_pin_registry_modes.py_pin_schema_mode_mapping unwrap/scalar cases, _pin_registry_allowed_modes flag-key extraction, _build_pin_registry_modes against 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.pyload_pin_registry_modes_index missing / corrupt / reads-json.
  • tests/controllers/test_components.py — endpoint returns the cached map; load() populates it.
  • ruff check + ruff format --check clean; adjacent suites (test_sync_components_emit_split, test_event_payload_contracts, test_sync_components_pin_long_form) green.

Copilot AI review requested due to automatic review settings June 3, 2026 19:30
@github-actions github-actions Bot added the enhancement Improvement to an existing feature label Jun 3, 2026
@codspeed-hq

codspeed-hq Bot commented Jun 3, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 27 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing enhancement/pin-registry-modes (583caf3) with main (5e89f22)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@codecov-commenter

codecov-commenter commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.43%. Comparing base (5e89f22) to head (583caf3).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #1163   +/-   ##
=======================================
  Coverage   99.42%   99.43%           
=======================================
  Files         213      213           
  Lines       15957    15976   +19     
=======================================
+ Hits        15866    15885   +19     
  Misses         91       91           
Flag Coverage Δ
py3.12 99.41% <100.00%> (+<0.01%) ⬆️
py3.14 99.43% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...evice_builder/controllers/components/controller.py 100.00% <100.00%> (ø)
esphome_device_builder/definitions/__init__.py 98.21% <100.00%> (+0.16%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py to generate definitions/pin_registry_modes.index.json and ship it in the wheel.
  • Add a backend loader + cache and a new WS command components/get_pin_registry_modes to 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.

Comment thread esphome_device_builder/definitions/__init__.py Outdated
Comment thread tests/test_sync_components_pin_registry_modes.py Outdated
@esphbot

esphbot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

PR Review — Emit per-registry pin mode flags for the visual editor

Solid, well-tested enhancement — no blocking issues. Merge-ready once the optional diagnosability suggestions are weighed.

  • Clean separation: sync-time derivation (_build_pin_registry_modes), tolerant loader (load_pin_registry_modes_index), cached endpoint (get_pin_registry_modes), all mirroring established board/featured idioms.
  • Loader shape-validation and the test-docstring nit raised by Copilot are already addressed in 583caf3; malformed-artefact degradation to "show every flag" is correct and tested.
  • Native-platform exclusion correctly handles both Platform-enum and bare-string registrations; str(key) resolves to the enum value for esphome's StrEnum.
  • Remaining notes are suggestion-level: broad except/suppress blocks in the sync script drop import failures silently, and a full sync could emit a partial map without a sanity floor. Low risk given the human catalog gate.

🟢 Suggestions

1. Per-stem import failure is silently dropped (`script/sync_components.py`, L2475-2480)

contextlib.suppress(Exception) around loader.get_component(stem) means a genuine pin-provider that fails to import (an esphome refactor, transient import-time error) is silently absent from the emitted map. The only downstream guard checks whether the whole map is empty, so one missing provider produces no warning — the editor then over-permissively shows every Mode flag for that expander.

This is low risk because catalog PRs are human-gated (a shrinking diff is visible in review), but a _LOGGER.debug("...", stem) in the suppressed path — or narrowing to ImportError and logging unexpected types — would make a real provider regression diagnosable instead of invisible.

for stem in component_stems:
    with contextlib.suppress(Exception):
        loader.get_component(stem)
with contextlib.suppress(Exception):
    loader.get_component(stem)

Checklist

  • No hardcoded secrets
  • Unsafe deserialization (orjson on generated artefact)
  • Input validation / safe fallback on missing-malformed artefact
  • Error handling — broad excepts diagnosable — suggestion #1, #2
  • Test coverage for new branches
  • Generated artefact script-owned, not hand-edited
  • Tests verify behavior, not source text
  • Deterministic output (OPT_SORT_KEYS)

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.

for stem in component_stems:
    with contextlib.suppress(Exception):
        loader.get_component(stem)

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.


Automated review by Kōan6e91249
583caf3

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.
@bdraco
bdraco force-pushed the enhancement/pin-registry-modes branch from 841e142 to 6e91249 Compare June 3, 2026 19:38
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.
@bdraco

bdraco commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

Thanks @copilot-pull-request-reviewer — both addressed in 583caf3:

  • Loader shape validation: load_pin_registry_modes_index() now returns {} when the payload isn't a mapping, and drops any entry whose value isn't a list (keeping only string flags), so a malformed artefact degrades to "show every flag" instead of crashing startup. Added test_load_pin_registry_modes_tolerates_unexpected_shapes covering [1,2,3] and a mixed-type mapping.
  • Test module docstring: reflowed so the content starts on the line after the opening """ per CLAUDE.md.

Copilot finished work on behalf of bdraco June 3, 2026 19:46
@bdraco
bdraco merged commit 9d01d5e into main Jun 3, 2026
17 checks passed
@bdraco
bdraco deleted the enhancement/pin-registry-modes branch June 3, 2026 20:04
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

enhancement Improvement to an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants