Skip to content

Add GitHub workflow for syncing HA device classes + constants#812

Draft
TheJulianJES wants to merge 17 commits into
devfrom
zigpy-bot/sync-device-classes
Draft

Add GitHub workflow for syncing HA device classes + constants#812
TheJulianJES wants to merge 17 commits into
devfrom
zigpy-bot/sync-device-classes

Conversation

@TheJulianJES

@TheJulianJES TheJulianJES commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

DRAFT. Other thoughts:

  • Separate out homeassistant + uv.lock change?
  • Split off backwards-compatible units/constant changes from recent HA versions?
  • Also keep older compare_constanty.py script?
  • Added complexity for PR body diff worth it?
  • Schedule?

Summary

Automates keeping ZHA's local, near-1:1 copies of Home Assistant enums in sync with HA. ZHA mirrors a set of HA enums — the unit enums in zha/units.py and the device-class / mode enums under zha/application/platforms/{binary_sensor,sensor,number}/device_class.py — including their docstrings and comments, which drift silently whenever HA changes them. The previous tools/compare_constants.py (from #810) could only detect drift for a subset, and only when run by hand.

This replaces that script with a syncing tool plus a scheduled workflow that copies the enums verbatim from Home Assistant and opens a pull request with the result.

How it works

tools/sync_constants.py copies each mirrored enum verbatim from the installed homeassistant package via inspect.getsource (so docstrings and in-class comments come along), using AST-located line edits. It parses ZHA's files as text and never imports them, so it only needs homeassistant installed.

  • Device-class / mode enums — replaced in place from HA (a fixed, refresh-only set; a brand-new HA one is added to the tool by hand).
  • Unit enums (UnitOf*) — existing ones refreshed, missing ones appended, and ones HA has removed deleted (ZHA's unit enums exist solely to mirror HA). If HA merely relocates an enum out of homeassistant.const while still exposing it, the tool fails loudly rather than deleting ZHA's copy.
  • python -m tools.sync_constants --check is a dry run (exits 1 on drift).

.github/workflows/sync-device-classes.yml runs daily (and on demand):

  • mints a GitHub App token, checks out ZHA and home-assistant/core@dev, installs HA (Python 3.14), runs the tool, and opens/updates a single pull request on a fixed sync/device-classes branch (each run updates the same PR).
  • The PR body is generated from the tool's per-enum change summary: added/removed members, NAME: old -> new value changes, whole added/removed enums, and a note when only docstrings/comments changed.

Backwards-compatibility constants

HA now derives constants like PERCENTAGE and CONCENTRATION_* from the new UnitOfDensity / UnitOfRatio enums. This PR adds those two enums to zha.units and moves the module-level constants ZHA exposes into a single hand-maintained "backwards-compatibility" section at the end of the file, deriving them the same way HA does — values are unchanged, so existing imports keep working. The tool never rewrites that section; only enums are synced.

Other changes

  • homeassistant is dropped from the testing extras (the workflow installs HA itself), shrinking uv.lock considerably.

Setup required before enabling

The workflow needs a GitHub App with contents + pull-requests write, exposed as the SYNC_APP_ID repository variable and SYNC_APP_PRIVATE_KEY secret. An App-authored PR is what lets CI run on the sync PR.

After merge

The first workflow run will open a sync/device-classes PR with the device-class docstring/comment updates accumulated on HA dev (intentionally not included here). Drift in the hand-maintained backwards-compat constants section is not auto-detected — that stays a manual update.

Runs tools/compare_constants.py on a daily schedule (and on demand) to
surface drift between ZHA's local copies of Home Assistant constants and
the canonical homeassistant package. Unlike ESPHome's equivalent, the
script only detects drift, so the job fails the run instead of opening a
fix-up pull request. Home Assistant is installed directly in the workflow
(Python 3.14), so it no longer needs to be a project dependency.
The device class sync workflow now installs homeassistant directly, so it
no longer needs to be a project testing dependency (added in #810). This
reverts the large uv.lock expansion that pulling in the full Home
Assistant dependency tree required.
The comparison script can now apply the safe, mechanical fixes in place
instead of only reporting drift: enum members HA has but ZHA is missing
are added, and enum-member / module-level-constant value mismatches are
corrected to HA's value. Edits are AST-located and match the file's
existing member style (contiguous unit enums vs. blank-separated
device-class enums), so ruff format leaves them untouched.

Ambiguous drift (ZHA-only symbols, type mismatches, entirely new enums
HA has that ZHA doesn't mirror) is still only reported, never written, so
a human decides those. This is what lets the sync workflow open a PR.
Rework the workflow from failing on drift to the ESPHome-style model:
run compare_constants.py --write, then open/update a pull request with the
safe fixes via peter-evans/create-pull-request. A final check step still
fails the run if drift remains that --write could not safely resolve, so
it is surfaced rather than silently dropped.
Swap the compare-and-surgically-fix approach for a straight copy: read
each mirrored enum from the installed homeassistant package with
inspect.getsource and write it verbatim into ZHA, docstrings and comments
included. This also catches docstring drift the member-level comparison
missed (e.g. SensorDeviceClass unit-of-measurement prose).

Device-class / mode enums are replaced in place; every UnitOf* enum HA
defines is copied into zha.units, appending any ZHA is missing. Only enums
are synced — module-level constants stay as ZHA's plain literals (HA
derives them from the enums, so copying verbatim would reorder the file
and create forward references) and ZHA-only symbols are never touched.
--check does a dry run.
Rework the workflow to copy from Home Assistant instead of surgically
fixing: check out home-assistant/core at dev, install it editable
alongside ZHA, run tools/sync_constants, ruff format, and open/update a
pull request. The PR uses a fixed sync/device-classes branch so each run
updates the same PR.

Authenticate with a GitHub App token (actions/create-github-app-token) so
the PR is authored by the bot account and triggers CI; the App ID and
private key are read from the SYNC_APP_ID variable and SYNC_APP_PRIVATE_KEY
secret.
…ection

Home Assistant added UnitOfDensity and UnitOfRatio and now derives the
CONCENTRATION_* and PERCENTAGE constants from them. Mirror the two enums
in zha.units and move the module-level constants ZHA exposes into a single
backwards-compatibility section at the end of the file, deriving the same
ones from the enums exactly as HA does (values are unchanged, so existing
imports keep working).

The constants sit after all enums so the enum references resolve, and the
section is maintained by hand: tools/sync_constants.py syncs the enums but
never rewrites it. ZHA-only constants (COUNT, KILOJOULES_PER_KG,
CONCENTRATION_PARTS_PER_CUBIC_METER) are kept and labelled.
The sync tool reads HA's enums and parses ZHA's source files as text
rather than importing the platform modules, so it no longer pulls in
zhaquirks. Installing zha-quirks (which now depends on ZHA) is unnecessary.
sync_constants now deletes ZHA unit enums HA no longer defines (they exist
only to mirror HA, so no allowlist is needed) alongside adding/refreshing
the current ones; enum-member removals already came through the verbatim
class copy.

New enums are inserted just above an explicit marker comment in zha.units
rather than merely 'after the last class', keeping them in the enum region
and out of the hand-maintained backwards-compatibility section below it.
The tool now parses zha/units.py as text instead of importing it, so a
prior sync that left a dangling reference can still be repaired. Whole
device-class enum removal raises a clear 'update DEVICE_CLASS_ENUMS' error
instead of a raw traceback.
Drop the stale zha-quirks requirement (the tool parses ZHA's files as
text rather than importing them, so neither zha nor zha-quirks needs to be
installed), and note that enums are added/refreshed/removed and that the
backwards-compatibility constants section is left alone.
The summary said the tool 'removes nothing', contradicting the enum
auto-removal it now performs. Clarify that only enums are added, changed,
or removed, and constants / ZHA-only symbols are never touched.
- Scope the pull request commit to zha/ (add-paths) so the HA checkout
  under lib/ can never be committed even if .gitignore changes.
- Don't install ZHA in the workflow; the tool only imports homeassistant
  and reads ZHA's files as text, and installing ZHA forced a joint
  resolution with HA-dev's pins for no benefit.
- Guard enum removal: only delete a ZHA UnitOf* enum when HA no longer
  exposes the name at all. If HA still exposes it (e.g. moved out of
  homeassistant.const but re-exported), raise instead of silently deleting.
- Add a concurrency group so a manual dispatch can't race the scheduled
  run on the shared sync/device-classes branch.
The sync tool already emits ruff-clean output and the pull request's own
pre-commit CI runs the repo-pinned ruff, so the separate uvx ruff step was
redundant and risked drifting from pyproject's pin. Instead, capture the
tool's summary of what changed and use it as the PR body (via body-path),
so each run's PR shows what it did; create-pull-request updates the body
of the existing PR on later runs.
Clarify in the README that only unit enums are auto-added/removed while
the device-class enums are a fixed, refresh-only set. Fix the
CONCENTRATION_PARTS_PER_CUBIC_METER comment: HA still ships it (as a
deprecated alias), so it is kept for backwards compatibility rather than
being ZHA-only.
The summary now lists, per enum, added/removed members and value changes
(NAME: old -> new), plus 'added/removed enum' for whole enums and a
'docstring/comment updates' note when only prose changed. This flows into
the workflow's PR body, so reviewers see what a sync did at a glance
rather than a coarse 'updated device classes'. Also drops the stale 'run
ruff format' line now that the workflow has no format step.
The step uses the default shell (bash -e, no pipefail), so piping the
sync tool into tee masked its exit code — a crash or one of the tool's
loud LookupError guards would have gone green and could close the open
sync PR with an empty diff. Enable pipefail so the pipeline fails.
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.29%. Comparing base (93d3e50) to head (3b356b9).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #812   +/-   ##
=======================================
  Coverage   97.29%   97.29%           
=======================================
  Files          55       55           
  Lines       10933    10942    +9     
=======================================
+ Hits        10637    10646    +9     
  Misses        296      296           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 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 introduces an automated way to keep ZHA’s local, near-1:1 mirrors of Home Assistant unit enums and device-class/mode enums in sync, replacing the prior manual drift-detection script and adding a scheduled GitHub workflow to open/update a dedicated sync PR.

Changes:

  • Add tools/sync_constants.py to copy mirrored enums verbatim from an installed Home Assistant checkout and provide a --check drift mode.
  • Add a scheduled workflow that installs HA dev, runs the sync tool, and opens/updates a single PR on sync/device-classes.
  • Update zha/units.py to include new UnitOfDensity / UnitOfRatio enums and move mirrored module-level constants into a dedicated backwards-compatibility section; remove the old tools/compare_constants.py and drop homeassistant from the testing extra.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
zha/units.py Adds new unit enums and consolidates mirrored module-level constants into a marked backcompat section.
tools/sync_constants.py New sync tool that replaces mirrored enum definitions from installed Home Assistant source and summarizes changes.
tools/README.md Documents how to run the new sync tool and the intended workflow behavior.
tools/compare_constants.py Removes the prior manual drift-detection script.
pyproject.toml Drops homeassistant from the testing optional extra.
.github/workflows/sync-device-classes.yml New scheduled workflow to run syncing against HA dev and maintain a single sync PR.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/sync-device-classes.yml
Comment thread .github/workflows/sync-device-classes.yml
- Request '>=3.14.2' for the venv so it can't resolve to a 3.14.x older
  than Home Assistant's minimum (uv accepts the lower-bound specifier).
- Split the app user-id lookup across two lines with an intermediate
  variable. The nested quotes worked (bash re-parses inside $(...)), but
  the flatter form is clearer and avoids the ambiguity a reviewer flagged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants