diff --git a/.claude/skills/bump-pyhive/SKILL.md b/.claude/skills/bump-pyhive/SKILL.md index 234640b..f4c5da0 100644 --- a/.claude/skills/bump-pyhive/SKILL.md +++ b/.claude/skills/bump-pyhive/SKILL.md @@ -1,54 +1,71 @@ --- name: bump-pyhive -description: Update the pyhive-integration (apyhiveapi) requirement in the Hive custom component's manifest.json, review what changed in the library, and verify the component against it. Use when a pyhive bump PR is open, a new pyhive release is out, or the user wants to test a pyhive beta/dev build. +description: Update the pyhive-integration (apyhiveapi) requirement in the Hive custom component's manifest.json, review what changed in the library, and verify the component against it. Use when a pyhive bump PR is open, a new pyhive release or beta is out, before cutting a component beta, or the user wants to test a pyhive beta/dev build. --- # Bump pyhive-integration `pyhive-integration` (imported as `apyhiveapi`) is pinned in `custom_components/hive/manifest.json` `requirements`. Dependabot can't bump it, so `.github/workflows/pyhive_bump.yml` opens `deps/pyhive-integration-` PRs instead. +Read the principles in `CLAUDE.md` first. In short: +- This repo may pin a **newer** `pyhive-integration` than core hive does, including pre-releases. That's intended. +- Keep the pin exact (`==`). Don't switch to `>=` to quiet hassfest; `.github/scripts/hassfest.py` already treats that difference as a warning. +- A pre-release pin can only ship in a component **beta**. A stable component release needs a stable pin. + ## 1. Pick the target ```bash -jq -r '.requirements[]' custom_components/hive/manifest.json # current +jq -r '.requirements[]' custom_components/hive/manifest.json # current pin curl -fsS https://pypi.org/pypi/pyhive-integration/json | jq -r '.info.version' # latest stable -curl -fsS https://pypi.org/pypi/pyhive-integration/json | jq -r '.releases | keys[]' | tail -10 -gh pr list --label dependencies --search "pyhive-integration in:title" # existing bot PR? +# newest version overall, pre-releases included (sorted as versions, not by upload date) +curl -fsS https://pypi.org/pypi/pyhive-integration/json | jq -r '.releases | keys[]' \ + | .venv/bin/python -c 'import sys; from packaging.version import Version; print(max(sys.stdin.read().split(), key=Version))' +gh pr list --search "pyhive-integration in:title" --state open # existing PR? ``` -If there's already a bot PR for the target, work on that branch (`gh pr checkout `) instead of making a new change. +- For a beta (or when the user asks for "latest"), the target is the newest version overall. +- For a stable release, the target is the latest stable. +- Also note core's pin, from `.venv/lib/python*/site-packages/homeassistant/components/hive/manifest.json`. Core's re-exported platforms are written against it, so a big gap needs a closer look. + +If there's already an open PR for the target, work on that branch (`gh pr checkout `) instead of making a new change. ## 2. Review what changed -Diff the two versions' sources and look closely at anything this component calls: +`uv pip download` doesn't exist, so fetch the wheels from PyPI's JSON API into the scratchpad and unpack them there: ```bash -S=$(mktemp -d) +S=/pyhive for v in ; do - uv pip download --no-deps "pyhive-integration==$v" -d "$S/$v" 2>/dev/null || pip download --no-deps "pyhive-integration==$v" -d "$S/$v" + url=$(curl -fsS "https://pypi.org/pypi/pyhive-integration/$v/json" | jq -r '[.urls[]|select(.packagetype=="bdist_wheel")][0].url') + mkdir -p "$S/dl-$v" "$S/src-$v" && curl -fsSL -o "$S/dl-$v/pkg.whl" "$url" && unzip -qo "$S/dl-$v/pkg.whl" -d "$S/src-$v" done +diff -rq "$S/src-/apyhiveapi" "$S/src-/apyhiveapi" ``` -Unpack both and diff `apyhiveapi/`. Things to check: -- `Auth` methods used in `config_flow.py` and `__init__.py`: `login`, `sms_2fa`, `device_registration`, `get_device_data`, `is_device_registered`, `forget_device`. -- `Hive`, `session.startSession`, `session.deviceList`, `updateInterval`. -- `helper/const.py`: `PRODUCTS`, `DEVICES` and `sensor_commands`. A new or renamed `hiveType` needs a matching `SENSOR_TYPES` entry in `sensor.py`, or core's platform descriptions, before it shows up in HA. -- Exceptions in `helper/hive_exceptions.py`. -- The getters that tests mock: `getSensor`, `getClimate`, `getWaterHeater`, `getLight`, `getSwitch` and `getScheduleNowNextLater`. +Check anything this component, or core's re-exported platforms, calls: +- `Auth` methods used in `config_flow.py` and `__init__.py`: `login`, `sms_2fa`, `device_registration`, `get_device_data`, `is_device_registered`, `forget_device`. Also the attributes the flow sets: `access_token`, `device_group_key`, `device_key`. +- `Hive`, `session.startSession`, `session.deviceList`, `session.updateData`, `updateInterval`. +- Every `hive..(` call in the component and in core's hive platforms (`grep -hoE "hive\.[a-z_]+\.[A-Za-z_]+\(" ...`). Confirm each exists on the target with a compatible signature, at runtime rather than by reading code. +- How devices are accessed (`device["hiveType"]`, `.get("deviceData")`). 2.x devices are dataclasses with a legacy-key mapping. +- `PRODUCTS`, `DEVICES` and the sensor dispatch. A new or renamed `hiveType` needs a matching `SENSOR_TYPES` entry in `sensor.py`, or core's platform descriptions, before it shows up in HA. +- Exceptions in `helper/hive_exceptions.py`, especially anything `startSession` can now raise. `async_setup_entry` must map it: connection problems to `ConfigEntryNotReady`, credential or config problems to `ConfigEntryAuthFailed`. -Also check whether the version HA core pins (`homeassistant/components/hive/manifest.json` in `.venv`) differs a lot. Core's re-exported platforms are written against that version. +Also check that `pyhive-integration` isn't in HA's `homeassistant/package_constraints.txt`. If it ever is, HA won't install a different version, and principle 2 needs rethinking. ## 3. Apply and verify - Change only the `pyhive-integration==` pin in `manifest.json`. Don't change the component `version`; that happens at release. - Install and test: ```bash - VIRTUAL_ENV=.venv uv pip install "pyhive-integration==" + VIRTUAL_ENV=.venv uv pip install --refresh-package pyhive-integration "pyhive-integration==" script/test script/lint --check + python3 .github/scripts/hassfest.py # needs Docker; the pyhive line should be a warning ``` + (`--refresh-package` matters for a release published in the last few hours.) +- The tests mock the library. On 2.x, also run an offline session from the bundled `data/data.json` (username `use@file.com`) and call the real getters for every platform. - Where the diff showed behaviour changes, add tests for them. -- For a beta or dev build, suggest also releasing a component beta (see `/release`) so users can try it. +- For a pre-release target, the next step is a component beta (`/release`). For a stable target, it can go in the next stable release. ## 4. Report Report: -- the version change +- the version change, and how it compares with core's pin - a short summary of the library changes that matter to this component, with any risks -- the test results -- whether it's ready to merge +- the test, lint and hassfest results +- whether it's ready to merge, and whether it can ship stable or only as a beta If a bot PR has no CI runs, remind the user to close and reopen it, or to set up the `BOT_TOKEN` secret. Don't commit, push, or merge unless asked. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md index bbb03a4..4b8c4b9 100644 --- a/.claude/skills/release/SKILL.md +++ b/.claude/skills/release/SKILL.md @@ -1,13 +1,15 @@ --- name: release -description: Cut a beta or stable release of the Hive custom component - bump manifest.json, publish the GitHub release with a matching tag, and verify the Release workflow. Use when the user asks to release, ship, tag, or publish a version or beta. +description: Cut a beta or stable release of the Hive custom component - check for the newest pyhive-integration (betas included) when releasing a beta, bump manifest.json, publish the GitHub release with a matching tag, and verify the Release workflow. Use when the user asks to release, ship, tag, or publish a version or beta. --- # Release -Follow MAINTAINING.md → "Releasing". The rules that matter: -- The tag must equal `version` in `custom_components/hive/manifest.json`, for example `2026.10.0` or `2026.10.0b1`. +Follow MAINTAINING.md → "Releasing", and the principles in `CLAUDE.md`. The rules that matter: +- **Betas are the fast, opt-in channel.** They're how new `pyhive-integration` builds and fixes reach users before core HA ships them. Stable releases go to every HACS user. +- The tag must equal `version` in `custom_components/hive/manifest.json`, for example `2026.10.0` or `2026.10.0b1`. Use a lowercase `b`, with no dot before it. - Beta tags (`bN`) are always published as pre-releases. Stable tags never are. +- A stable release must pin a stable `pyhive-integration`. A pre-release pin only ships in a beta. `check_release.py` fails the Release run otherwise, but by then the release is already public, so check before publishing. - Publishing a release is visible to every HACS user, and so is deleting one. **Confirm the exact tag and whether it is a pre-release with the user before running `gh release create`.** Never commit, push, or open PRs unless the user asks. ## Steps @@ -19,23 +21,35 @@ Follow MAINTAINING.md → "Releasing". The rules that matter: - Same month: bump the patch number. - Beta: add `b1`, or the next free `bN`. -2. **Check it's safe to ship.** +2. **Check pyhive-integration.** + - **Beta:** always look up the newest version on PyPI, pre-releases included: + ```bash + curl -fsS https://pypi.org/pypi/pyhive-integration/json | jq -r '.releases | keys[]' \ + | .venv/bin/python -c 'import sys; from packaging.version import Version; print(max(sys.stdin.read().split(), key=Version))' + jq -r '.requirements[]' custom_components/hive/manifest.json + ``` + If it's newer than the pin, run `/bump-pyhive` for it first, and get that merged to `master`, unless the user decides to skip it. Check for an open `deps/pyhive-integration-*` PR before starting a new one. + - **Stable:** the pin must not be a pre-release. If `master` pins a pyhive beta, stop: either wait for a stable pyhive, or move the pin to the latest stable with `/bump-pyhive` first. + +3. **Check it's safe to ship.** - `gh run list -w ci.yml -b master -L 1` and `gh run list -w compat.yml -L 1` must both be green. If either is red, stop and report. For a red Compatibility run, suggest `/triage-compat`. + - A hassfest **warning** about `pyhive-integration` differing from core hive is expected (see `CLAUDE.md`). Only errors count. - `script/test` and `script/lint --check` must pass locally. - Check `git log ..origin/master --oneline`. If there's nothing to release, say so. -3. **Bump the manifest.** Change only the `version` value in `manifest.json`. Don't touch `requirements`; that's `/bump-pyhive`. The change must reach `master` before tagging: +4. **Bump the manifest.** Change only the `version` value in `manifest.json`. Don't touch `requirements`; that's `/bump-pyhive`. The change must reach `master` before tagging: - If the user asks, make a branch `release/`, commit it, and open a PR. - Otherwise hand the change over for them to commit. - - Wait until it's merged, and check with `git fetch && git show origin/master:custom_components/hive/manifest.json | jq -r .version`. + - Wait until it's merged, then check with `git fetch && git show origin/master:custom_components/hive/manifest.json | jq -r '.version, .requirements[]'`. -4. **Publish** (only after the user confirms the tag): +5. **Publish** (only after the user confirms the tag): ```bash gh release create --target master --generate-notes --prerelease # beta gh release create --target master --generate-notes --latest # stable ``` + For a beta, add the `pyhive-integration` version to the notes, and say whether it's ahead of core's. -5. **Verify.** +6. **Verify.** - Watch the Release workflow: ```bash gh run watch $(gh run list -w release.yml -L1 --json databaseId -q '.[0].databaseId') --exit-status @@ -43,6 +57,6 @@ Follow MAINTAINING.md → "Releasing". The rules that matter: - Then confirm `hive.zip` is attached: `gh release view --json assets -q '.assets[].name'`. - If the check job fails, the release is already visible to HACS users. Tell the user, and offer `gh release delete --cleanup-tag` (it needs their OK), then a corrected release. -6. **Clean up** after a stable release that replaces betas. Offer `gh workflow run delete_beta.yml`. It keeps the newest pre-release and never touches stable releases. +7. **Clean up** after a stable release that replaces betas. Offer `gh workflow run delete_beta.yml`. It keeps the newest pre-release and never touches stable releases. -Report the release URL, the Release run result and anything left for the user to do. +Report the release URL, the `pyhive-integration` version it ships, the Release run result and anything left for the user to do. diff --git a/.claude/skills/repo-health/SKILL.md b/.claude/skills/repo-health/SKILL.md index 553c4b3..c303011 100644 --- a/.claude/skills/repo-health/SKILL.md +++ b/.claude/skills/repo-health/SKILL.md @@ -29,6 +29,9 @@ Compare what you find with MAINTAINING.md: - **Unreleased commits on master:** suggest a release if there are user-facing changes. - **Branch protection:** the required checks should be `HACS validation`, `Hassfest`, `Lint` and `Tests`, with code owner review on. - **Secrets:** `BOT_TOKEN` should be present. +- **Hassfest:** a warning that `pyhive-integration` differs from core hive is expected (see `CLAUDE.md`), so don't report it as a problem. +- **pyhive-integration:** compare the manifest pin with core hive's pin and with the newest version on PyPI, pre-releases included. A newer pyhive beta than the pin is a candidate for the next component beta (`/bump-pyhive`, then `/release`). A pre-release pin on `master` blocks stable releases until it's moved to a stable version. +- **Latest beta release:** the tag should match the `version` in its manifest (`git show :custom_components/hive/manifest.json | jq -r .version`). - **Pinned HA:** compare the HA version in the `requirements_test.txt` comment with the latest stable on PyPI (`curl -fsS https://pypi.org/pypi/homeassistant/json | jq -r .info.version`). A gap of more than one minor release means the Dependabot PR is stuck. Report as a short table with ✅ / ⚠️ / ❌ and one line each. Then list the actions in priority order, each with the skill to use (`/triage-compat`, `/bump-pyhive`, `/release`) or the settings page to change. diff --git a/.claude/skills/triage-compat/SKILL.md b/.claude/skills/triage-compat/SKILL.md index 3448d35..d1645df 100644 --- a/.claude/skills/triage-compat/SKILL.md +++ b/.claude/skills/triage-compat/SKILL.md @@ -5,6 +5,8 @@ description: Diagnose and fix a failing nightly Compatibility workflow (compat.y # Triage Compatibility +Principle 1 in `CLAUDE.md` applies: this component extends core's hive integration and reuses it wherever possible, so fixes adapt to core rather than fork it. + Background: most platform files (`binary_sensor.py`, `climate.py`, `light.py`, `switch.py`, `water_heater.py`, `entity.py`) re-export `homeassistant.components.hive.*`. `__init__.py`, `sensor.py` and `config_flow.py` are this repo's own code, but they rely on HA helpers and on core's `HiveEntity`. HA changes break things here without any commit in this repo. ## 1. Find what failed @@ -20,7 +22,7 @@ Map it using the table in MAINTAINING.md → "Compatibility is red": - **beta** fails: must be fixed before the next monthly HA release. - **dev** smoke import fails: an import was renamed or removed upstream. - The resolver step fails with "No pytest-homeassistant-custom-component release pins…": the plugin isn't published yet. Rerun later with `gh run rerun --failed`. There's nothing to fix. -- **HACS/hassfest** fails: follow the validator's message. +- **HACS/hassfest** fails: follow the validator's message. A `[REQUIREMENTS]` message about `pyhive-integration` differing from core hive is expected, and `.github/scripts/hassfest.py` reports it as a warning (see `CLAUDE.md`). If it's failing on that, hassfest has reworded the message, so update the `ALLOWED` pattern in that script. Don't loosen the pin. ## 2. Reproduce locally Use a separate venv so `.venv` (which matches CI's pinned HA) stays untouched: diff --git a/.github/scripts/check_release.py b/.github/scripts/check_release.py index c7f1c4b..cc901b6 100644 --- a/.github/scripts/check_release.py +++ b/.github/scripts/check_release.py @@ -4,13 +4,17 @@ HACS installs by tag, so the tag must match the manifest version (compared as versions, so tag 2026.3.1.b1 matches manifest 2026.3.1b1). A beta version must be -a GitHub pre-release, otherwise HACS offers it to every user. +a GitHub pre-release, otherwise HACS offers it to every user. A stable release +must also pin a stable pyhive-integration: pre-release libraries only ship in the +opt-in beta channel (see CLAUDE.md). """ import json from pathlib import Path import sys +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name from packaging.version import InvalidVersion, Version MANIFEST = ( @@ -19,7 +23,8 @@ def main(tag: str, prerelease: bool) -> int: - manifest_version = json.loads(MANIFEST.read_text(encoding="utf-8"))["version"] + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + manifest_version = manifest["version"] errors = [] try: tag_version = Version(tag) @@ -42,6 +47,15 @@ def main(tag: str, prerelease: bool) -> int: f"{tag} is marked as a pre-release but isn't a beta version " "(expected something like 2026.9.0b1)." ) + if not tag_version.is_prerelease: + for req in map(Requirement, manifest.get("requirements", [])): + if canonicalize_name(req.name) == "pyhive-integration" and any( + Version(spec.version).is_prerelease for spec in req.specifier + ): + errors.append( + f"{tag} is a stable release but pins {req}. Stable releases go " + "to every HACS user, so ship pre-release pyhive in a beta instead." + ) for error in errors: print(f"::error::{error}") diff --git a/.github/scripts/hassfest.py b/.github/scripts/hassfest.py new file mode 100644 index 0000000..09fc159 --- /dev/null +++ b/.github/scripts/hassfest.py @@ -0,0 +1,69 @@ +"""Run hassfest, allowing only the errors this repo causes on purpose. + + python .github/scripts/hassfest.py + +This component replaces Home Assistant core's hive integration and may pin a +different pyhive-integration than core does (see CLAUDE.md). hassfest checks a +custom integration's requirements against every core integration's pins, including +core hive's own, so it reports that difference as an error. Those errors are shown +as warnings here. Any other error, or a failure with no error lines, still fails. +""" + +from pathlib import Path +import re +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[2] +IMAGE = "ghcr.io/home-assistant/hassfest" + +# Packages whose core pin belongs to the core integration this repo replaces. +OVERRIDDEN_PACKAGES = {"pyhive-integration"} + +# The two hassfest errors about differing from a core integration's pin. Errors +# about Home Assistant's own dependencies or package constraints are not here: +# those can break the whole installation, so they must fail. +ALLOWED = re.compile( + r"\[REQUIREMENTS\] Requirement (?P[A-Za-z0-9][A-Za-z0-9._-]*)\S* " + r"(pins a package Home Assistant depends on" + r"|is incompatible with \S+, which Home Assistant depends on\.)" +) + + +def is_allowed(line: str) -> bool: + """Return True if a hassfest error line is an expected override difference.""" + match = ALLOWED.search(line) + return bool(match) and match["package"].lower() in OVERRIDDEN_PACKAGES + + +def main() -> int: + result = subprocess.run( + ["docker", "run", "--rm", "-v", f"{ROOT}:/github/workspace", IMAGE], + capture_output=True, + text=True, + check=False, + ) + output = result.stdout + result.stderr + print(output) + if result.returncode == 0: + return 0 + + errors = [line for line in output.splitlines() if "[ERROR]" in line] + if not errors: + print( + f"::error::hassfest exited with {result.returncode} without an error line" + ) + return result.returncode + + blocking = [line for line in errors if not is_allowed(line)] + for line in errors: + level = "error" if line in blocking else "warning" + print(f"::{level}::{line.lstrip('* ').strip()}") + if blocking: + return 1 + print("Only expected differences from core hive's requirements; passing.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cce84ae..45d3efe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - # home-assistant/actions has no maintained releases; master tracks the - # current hassfest rules, which is what we want to validate against. - - uses: home-assistant/actions/hassfest@master + # Runs the same image as home-assistant/actions/hassfest (latest rules), + # but lets our pyhive-integration pin differ from core hive's. See CLAUDE.md. + - run: python3 .github/scripts/hassfest.py lint: name: Lint @@ -81,7 +81,7 @@ jobs: steps: - run: | for workflow in compat.yml stale.yml pyhive_bump.yml; do - state=$(gh workflow view "$workflow" --json state --jq .state) + state=$(gh api "repos/$GH_REPO/actions/workflows/$workflow" --jq .state) if [ "$state" = "disabled_inactivity" ]; then gh workflow enable "$workflow" echo "::notice::Re-enabled $workflow (was disabled for inactivity)" diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml index f0c0f59..2377650 100644 --- a/.github/workflows/compat.yml +++ b/.github/workflows/compat.yml @@ -27,7 +27,7 @@ jobs: with: category: integration ignore: brands - - uses: home-assistant/actions/hassfest@master + - run: python3 .github/scripts/hassfest.py tests: name: Tests (HA ${{ matrix.channel }}) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..195765e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +# Hive Custom Component + +A HACS custom component that replaces Home Assistant core's `hive` integration +(same `domain`). Procedures live in [MAINTAINING.md](MAINTAINING.md), setup in +[CONTRIBUTING.md](CONTRIBUTING.md), and step-by-step skills in `.claude/skills/`. + +## Why this repo exists + +Two principles decide most questions here. Apply them to code, CI, releases and +the skills. + +### 1. Extend core, reuse it wherever possible + +The component adds capability on top of core's hive integration, such as the extra +attribute sensors in `sensor.py`. It is not a fork. + +- Re-export core's platforms (`binary_sensor.py`, `climate.py`, `light.py`, + `switch.py`, `water_heater.py`, `entity.py`) instead of copying them. Only write + local code for something core doesn't do, and keep it as small as possible. +- When core changes, adapt to it. Replacing a re-export with a local module is a + last resort, and the user should know. +- A fix or feature that would help everyone belongs upstream in core or + `pyhive-integration` as well. This repo can ship it first. + +### 2. The opt-in fast channel + +Everyone on core's integration gets the official, slower release train. Installing +this component is an opt-in to get changes sooner, and its HACS betas go further +still, as a testing channel. + +- `manifest.json` may pin a **newer `pyhive-integration` than core does**, including + pre-releases. That divergence is the point of the repo, not a mistake. +- The pin stays exact (`==`). Users get the version we tested. +- **Pre-release `pyhive-integration` only ships in component betas** (pre-releases, + which HACS offers only to users who opt in). A stable component release must pin a + stable `pyhive-integration`. `.github/scripts/check_release.py` enforces this. +- When cutting a component beta, always check PyPI for the newest + `pyhive-integration`, pre-releases included, and bump to it first if it's newer + than the pin (`/bump-pyhive`, then `/release`). +- Never let the fast channel break people. Stable releases need green CI and a + green Compatibility run. + +## What this means for CI + +hassfest compares a custom integration's requirements with every core +integration's pins, including core hive's own. With principle 2 that always gives a +`[REQUIREMENTS]` error for `pyhive-integration`: "pins a package Home Assistant +depends on" when we match core, or "is incompatible with …" when we're ahead. +`.github/scripts/hassfest.py` runs the official hassfest image and turns only those +`pyhive-integration` errors into warnings. Every other error still fails, including +conflicts with Home Assistant's own dependencies or package constraints, because +those can break a user's whole installation. Don't loosen the pin to `>=` to +silence it. + +## Working here + +- `script/setup`, `script/test`, `script/lint` (`--check` in CI). `.venv` must be + Python 3.14 (`script/setup --recreate`). +- Tests mock `apyhiveapi`. When the library changes, also exercise the real code: + from 2.0 the library ships `apyhiveapi/data/data.json`, and + `startSession({"username": "use@file.com"})` runs a session from it offline. +- The tag of a release must equal `version` in `manifest.json`. Beta tags (`bN`) + are always pre-releases, and stable tags never are. +- Don't commit, push, release or merge unless asked. Releases are visible to every + HACS user. diff --git a/MAINTAINING.md b/MAINTAINING.md index 7cc963d..ad16d25 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -4,28 +4,56 @@ This is the runbook for keeping the component released, compatible and healthy. For development setup, see [CONTRIBUTING.md](CONTRIBUTING.md). Each procedure also has a Claude Code skill in `.claude/skills/`, listed at the end. +## Principles + +The full version is in [CLAUDE.md](CLAUDE.md). Everything below follows from these two: + +1. **Extend core, reuse it wherever possible.** This component replaces core's + `hive` integration to add capability, but re-exports core's platforms instead of + copying them. Local code only covers what core doesn't do. +2. **An opt-in fast channel.** Users on core get the official release train. + Installing this component opts in to changes sooner, and its betas are the + testing channel. It may pin a newer `pyhive-integration` than core, including + pre-releases, but **pre-release pyhive only ships in component betas**. Stable + releases pin stable pyhive. + ## What runs automatically | When | Workflow | What it does | If it fails | |---|---|---|---| -| Every PR and every push to `master` | `ci.yml` | HACS validation, hassfest, lint and tests. A push to master also re-enables scheduled workflows that GitHub turned off. | Fix it in the PR before merging | +| Every PR and every push to `master` | `ci.yml` | HACS validation, hassfest (through `.github/scripts/hassfest.py`, see [Hassfest and the pyhive pin](#hassfest-and-the-pyhive-pin)), lint and tests. A push to master also re-enables scheduled workflows that GitHub turned off. | Fix it in the PR before merging | | Daily, 04:17 UTC | `compat.yml` | Tests against the newest HA stable and beta, plus an import check against HA `dev` | [Compatibility is red](#compatibility-is-red) | | Daily, 05:29 UTC | `stale.yml` | Labels issues and PRs inactive for 60 days, then closes issues after 30 more days and PRs after 14 | Add the `pinned` label to keep something open | | Monday, 06:23 UTC | `pyhive_bump.yml` | Opens a PR if a newer `pyhive-integration` is on PyPI | [Dependency PRs](#dependency-prs) | | Weekly | Dependabot | PRs for GitHub Actions versions and for `requirements_test.txt` | [Dependency PRs](#dependency-prs) | -| A GitHub release is published | `release.yml` | Checks the tag matches `manifest.json`, then attaches `hive.zip` | [Releasing](#releasing) | +| A GitHub release is published | `release.yml` | Checks the tag matches `manifest.json`, and that a stable release doesn't pin pre-release pyhive, then attaches `hive.zip` | [Releasing](#releasing) | | Manual | `delete_beta.yml` | Deletes old pre-releases and their tags, keeping the newest | Not applicable | ## Releasing -Versions are `YEAR.MONTH.PATCH`, for example `2026.10.0`. Betas add `bN`, for example `2026.10.0b1`. -HACS installs by tag, so **the tag must equal `version` in `manifest.json`**. +Versions are `YEAR.MONTH.PATCH`, for example `2026.10.0`. Betas add `bN`, for example +`2026.10.0b1` (lowercase `b`, no dot before it). HACS installs by tag, so **the tag must +equal `version` in `manifest.json`**. + +Betas are the normal way to get something out quickly: HACS only offers them to users +who opt in. Stable releases go to everyone. + +1. **Check `pyhive-integration`:** + - **Beta:** look up the newest version on PyPI, pre-releases included, and if it's + newer than the pin, bump to it first ([Dependency PRs](#dependency-prs)): + + ```bash + curl -fsS https://pypi.org/pypi/pyhive-integration/json | jq -r '.releases | keys[]' \ + | .venv/bin/python -c 'import sys; from packaging.version import Version; print(max(sys.stdin.read().split(), key=Version))' + ``` -1. Open a PR that sets `version` in `custom_components/hive/manifest.json`. Merge it + - **Stable:** the pin must be a stable version. If `master` pins a pyhive beta, wait + for a stable pyhive, or move the pin back to the latest stable first. +2. Open a PR that sets `version` in `custom_components/hive/manifest.json`. Merge it once CI passes. -2. Check that the last Compatibility run on `master` passed, so you aren't shipping +3. Check that the last Compatibility run on `master` passed, so you aren't shipping something current HA versions break. -3. Publish the release from the same version: +4. Publish the release from the same version: ```bash # beta: HACS offers it only to users who opt into betas @@ -34,12 +62,14 @@ HACS installs by tag, so **the tag must equal `version` in `manifest.json`**. gh release create 2026.10.0 --target master --latest --generate-notes ``` -4. Watch the **Release** run: `gh run watch $(gh run list -w release.yml -L1 --json databaseId -q '.[0].databaseId')`. + For a beta, mention the `pyhive-integration` version in the release notes. +5. Watch the **Release** run: `gh run watch $(gh run list -w release.yml -L1 --json databaseId -q '.[0].databaseId')`. - It checks the tag *after* publishing. If it fails, the release is already visible to HACS users. Delete it straight away with `gh release delete --cleanup-tag`, fix the cause, and release again. - A beta tag must be a pre-release, and a pre-release must have a beta tag. -5. After a stable release that replaces a run of betas, run **Delete Beta Releases**: + - A stable release must not pin a pre-release `pyhive-integration`. +6. After a stable release that replaces a run of betas, run **Delete Beta Releases**: `gh workflow run delete_beta.yml`. ## Compatibility is red @@ -54,7 +84,7 @@ break the component with no change here. Open the failing run | **Tests (HA beta)** | The next HA release (first Wednesday of the month) will break it | Fix before that release. Ship a beta if needed | | **Import smoke test (HA dev)** | Something the component imports was renamed or removed in core | Adapt before it reaches beta | | **Find Home Assistant … release** step | The test plugin for a brand-new HA release isn't on PyPI yet | Usually fixes itself within a day. Rerun it | -| **HACS and hassfest** | New validation rules | Update the manifest or repo structure as it says | +| **HACS and hassfest** | New validation rules | Update the manifest or repo structure as it says. A pyhive `[REQUIREMENTS]` warning is expected; see [Hassfest and the pyhive pin](#hassfest-and-the-pyhive-pin) | To reproduce locally, install the HA version that failed into a separate venv: @@ -70,9 +100,34 @@ required manifest keys. The Home Assistant developer blog ## Dependency PRs -- **`pyhive-integration`** (from `pyhive_bump.yml`): read the release notes linked in the - PR. If CI hasn't run, close and reopen the PR (see the `BOT_TOKEN` note below). Merge - it once green. Release it on its own, or together with the next change. +- **`pyhive-integration`** (from `pyhive_bump.yml`, or `/bump-pyhive`): read the release + notes linked in the PR. If CI hasn't run, close and reopen the PR (see the `BOT_TOKEN` + note below). Merge it once green. + - Keep the pin exact (`==`), even when it's ahead of core's. + - The weekly run only proposes stable versions. For a pyhive beta, run the workflow + with its version (`gh workflow run pyhive_bump.yml -f version=`) or use + `/bump-pyhive`. + - A stable pyhive can go out in the next release. A pyhive beta can only go out in a + component beta. + - The tests mock the library. For anything beyond a patch release, also run an + offline session against the library's bundled data (see `/bump-pyhive`). + +### Hassfest and the pyhive pin + +hassfest checks a custom integration's requirements against the pins of every core +integration, including core hive's own `pyhive-integration`. Because this component +replaces core hive, and may pin a different version on purpose, that check always +reports an error, worded either as "pins a package Home Assistant depends on" or as +"is incompatible with …, which Home Assistant depends on". + +`.github/scripts/hassfest.py` runs the official hassfest image and shows only those +`pyhive-integration` errors as warnings. Any other error still fails, including a +conflict with Home Assistant's own dependencies or package constraints, because those +would break a user's installation. If hassfest rewords these messages, update the +`ALLOWED` pattern in the script. Don't switch the pin to `>=` to silence it. + +If `pyhive-integration` ever appears in HA's `homeassistant/package_constraints.txt`, +HA will refuse to install a different version, and the fast channel needs rethinking. - **`pytest-homeassistant-custom-component`** (Dependabot): this moves the HA version CI tests against. Update the comment in `requirements_test.txt` to say which HA version it pins. If HA has raised its minimum Python version, see below. @@ -114,9 +169,10 @@ users on older HA versions. - `sensor.py` has update branches for `CurrentTemperature`, `Heating_Boost` and `Hotwater_Boost` that never run, because no sensor description matches those types. `get_current_temp_sa()` also calls `heating.minmaxTemperature`, - `currentTemperature` and `targetTemperature`, but only the first of those exists - in pyhive. Pointing it at `Current_Temperature` would therefore crash. Either - delete this code or fix it along with the pyhive method names. + `currentTemperature` and `targetTemperature`. In pyhive 1.x only the first of + those exists, so pointing it at `Current_Temperature` would crash there. 2.x adds + the other two as aliases. Either delete this code or fix it along with the pyhive + method names. - `async_setup_hive_entry` raises `UnknownHiveError` from the user step. That path is unreachable, because the step checks for `AuthenticationResult` first. @@ -127,7 +183,7 @@ section above. | Skill | Use it to | |---|---| -| `/release` | Cut a beta or stable release, from the version bump through to checking the Release run | +| `/release` | Cut a beta or stable release, from the pyhive check and version bump through to checking the Release run | | `/triage-compat` | Find out why Compatibility is red, reproduce it locally and fix it | -| `/bump-pyhive` | Move to a new `pyhive-integration`, and check whether its changes affect this component | +| `/bump-pyhive` | Move to a new `pyhive-integration` (stable or beta), and check whether its changes affect this component | | `/repo-health` | Check workflows, recent runs, open bot PRs, branch protection and secrets in one report | diff --git a/README.md b/README.md index cb4b5d0..7cc4cb1 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,17 @@ sensors e.g `Living Room Lamp Battery Level` * Current State - Heating/Hotwater only * Boost State - Heating/Hotwater only +## How this relates to the built-in Hive integration + +This component replaces Home Assistant's built-in Hive integration. It reuses the +built-in code wherever it can and adds the extras above on top. + +Think of it as an opt-in fast channel. Fixes and new versions of the Hive library +([`pyhive-integration`](https://pypi.org/project/pyhive-integration/)) usually reach +this component before they reach Home Assistant itself. If you want to help test the +newest changes, turn on beta releases for this repository in HACS. Betas may use +pre-release versions of the library. Stable releases only use stable ones. + For details on what the Home Assistant Hive component supports, please see below: [**>> Hive Component Documentation <<**](https://www.home-assistant.io/integrations/hive/)