Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ For releases before v3.3.5, see the [Full Change Log](https://github.com/asantag

### Changed

- Updated the `aioWiserHeatAPI` dependency from v1.7.3 to v1.7.5, using its native room heat-demand and button-panel battery support. Release builds remain pinned to the released API version, while normal dev-channel builds use the API `dev` branch - PR [#754](https://github.com/asantaga/wiserHomeAssistantPlatform/pull/754).
- Updated the `aioWiserHeatAPI` dependency from v1.7.3 to v1.7.5, using its native room heat-demand and button-panel battery support. Dev builds prefer a published API prerelease and fall back to the released API pin; local builds can explicitly use a manually installed API, while final integration releases always use the released pin - PR [#754](https://github.com/asantaga/wiserHomeAssistantPlatform/pull/754).

### Fixed

Expand Down
25 changes: 18 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ key`.
## Building a package

```sh
# Local development package
python3 scripts/build.py --channel dev

# Local development package using a manually installed API
python3 scripts/build.py --channel dev --use-installed-api

# Stable release package
python3 scripts/build.py --release --channel stable

Expand All @@ -132,13 +138,18 @@ integration; without that option it offers stable releases only. See
[Adding frontend cards and panels](docs/frontend-registry.md) for the registry
format and shared panel API.

The source manifest pins the released `aioWiserHeatAPI` version. During a dev
channel build, the package installs the upstream API `dev` branch from GitHub.
Stable packages, including explicit `--release` builds, retain the released
version pin. The `--release` option controls frontend asset selection only. API
artifacts are built separately in the `aioWiserHeatAPI` repository; they are
not bundled in the integration package. The source manifest itself is never
modified by the build.
The source manifest pins the released `aioWiserHeatAPI` version. A dev-channel
build uses the newest published API prerelease that is newer than the released
pin; if none is available, or PyPI cannot be checked, it retains the released
pin. Pass `--use-installed-api` for local testing with a manually installed API;
this removes the API requirement from the packaged manifest so Home Assistant
does not replace the installed version. The option cannot be combined with
`--release`. A final release package (`--release --channel stable`) always
retains the released pin.

API artifacts are built separately in the `aioWiserHeatAPI` repository and are
never bundled in the integration package. All API requirement changes apply
only to the staged package; the source manifest is never modified by the build.

The output is `dist/wiser.zip`, with `dist/card-releases.json` recording the
frontend artifacts and checksums included. If an individual card or panel
Expand Down
9 changes: 9 additions & 0 deletions docs/frontend-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ GitHub/
python3 scripts/build.py --channel dev
```

This normal dev build selects a published API prerelease when one is available,
falling back to the released API pin. To use an API wheel installed manually in
Home Assistant instead, build with `--use-installed-api`; this deliberately
omits the API requirement from the packaged manifest:

```sh
python3 scripts/build.py --channel dev --use-installed-api
```

To test installation through Home Assistant instead, edit the installed `custom_components/wiser/frontend/cards.json` and run **Check for updates** on any Wiser frontend update entity. The shared coordinator reloads and validates the file, creates an update entity for the new definition, and offers it as installed version `0.0.0`. This does not require an integration rebuild or Home Assistant restart.

To build a package containing the definitions but none of the JavaScript bundles, run:
Expand Down
95 changes: 82 additions & 13 deletions scripts/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
from pathlib import Path
import shutil
import tempfile
from urllib.request import Request, urlopen
from zipfile import ZIP_DEFLATED, ZipFile

from packaging.version import InvalidVersion, Version

from fetch_card_releases import (
fetch_cards,
repository_overrides,
Expand All @@ -16,17 +19,11 @@

ROOT = Path(__file__).resolve().parents[1]
API_PACKAGE = "aioWiserHeatAPI"
API_DEV_FALLBACK_REQUIREMENT = (
f"{API_PACKAGE}@git+https://github.com/msp1974/aioWiserHeatAPI.git@dev"
)

API_RELEASES_URL = "https://pypi.org/pypi/aioWiserHeatAPI/json"

def set_api_requirement(manifest_path, channel, release=False):
"""Use API dev only for non-release dev-channel packages."""
if release or channel != "dev":
return

manifest = json.loads(manifest_path.read_text("utf-8"))
def api_requirement(manifest):
"""Return the released API requirement and its position."""
requirements = manifest.get("requirements", [])
matches = [
index
Expand All @@ -35,9 +32,64 @@ def set_api_requirement(manifest_path, channel, release=False):
]
if len(matches) != 1:
raise ValueError(
f"Expected exactly one {API_PACKAGE} requirement in {manifest_path}"
f"Expected exactly one {API_PACKAGE} requirement"
)
requirements[matches[0]] = API_DEV_FALLBACK_REQUIREMENT
requirement = requirements[matches[0]]
prefix = f"{API_PACKAGE}=="
if not requirement.startswith(prefix):
raise ValueError(f"Expected a released {API_PACKAGE} version pin")
version = Version(requirement.removeprefix(prefix))
if version.is_prerelease or version.is_devrelease:
raise ValueError(f"Expected a stable {API_PACKAGE} version pin")
return requirements, matches[0], version


def latest_api_prerelease(released_version):
"""Return the newest usable API prerelease newer than the released pin."""
request = Request(API_RELEASES_URL, headers={"User-Agent": "wiser-packager"})
try:
with urlopen(request, timeout=30) as response:
releases = json.load(response).get("releases", {})
except (OSError, ValueError):
return None

candidates = []
for value, files in releases.items():
try:
version = Version(value)
except InvalidVersion:
continue
if (
version > released_version
and version.is_prerelease
and any(not file.get("yanked", False) for file in files)
):
candidates.append(version)
return max(candidates) if candidates else None


def set_api_requirement(
manifest_path,
channel,
release=False,
use_installed_api=False,
):
"""Select API handling for local, prerelease, and release packages."""
if release and use_installed_api:
raise ValueError("--use-installed-api cannot be combined with --release")

manifest = json.loads(manifest_path.read_text("utf-8"))
requirements, index, released_version = api_requirement(manifest)

if use_installed_api:
# Explicit local testing uses whichever API is already installed.
requirements.pop(index)
elif channel == "dev":
# Dev builds prefer a published API prerelease. If none is available
# (or PyPI is unavailable), retain the released version pin.
if prerelease := latest_api_prerelease(released_version):
requirements[index] = f"{API_PACKAGE}=={prerelease}"

manifest_path.write_text(f"{json.dumps(manifest, indent=2)}\n", encoding="utf-8")


Expand All @@ -49,6 +101,7 @@ def build(
repositories=None,
release=False,
without_frontend=False,
use_installed_api=False,
):
"""Stage a fresh integration and replace the ZIP only after a successful build."""
# Stable builds and explicit releases must never package local card builds.
Expand All @@ -65,6 +118,7 @@ def build(
staging / "manifest.json",
channel,
release=release,
use_installed_api=use_installed_api,
)
manifest = validate_registry(
json.loads((staging / "frontend/cards.json").read_text("utf-8"))
Expand Down Expand Up @@ -100,8 +154,14 @@ def build(
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--channel", choices=["dev", "stable"], default="dev")
parser.add_argument("--release", action="store_true",
help="Use only GitHub release assets, including for the dev channel")
parser.add_argument(
"--release",
action="store_true",
help=(
"Build a release package using published frontend assets and "
"release-aware API selection"
),
)
parser.add_argument("--local-root", type=Path, default=ROOT.parent,
help="Directory containing sibling card repositories")
parser.add_argument("--output", type=Path, default=ROOT / "dist/wiser.zip")
Expand All @@ -111,6 +171,14 @@ def main():
action="store_true",
help="Package cards.json without any card or panel JavaScript bundles",
)
parser.add_argument(
"--use-installed-api",
action="store_true",
help=(
"Omit the API requirement and use the API already installed in "
"Home Assistant (local builds only)"
),
)
args = parser.parse_args()
try:
report = build(
Expand All @@ -119,6 +187,7 @@ def main():
repository_overrides(args.repository),
release=args.release,
without_frontend=args.without_frontend,
use_installed_api=args.use_installed_api,
)
except Exception as error:
parser.exit(1, f"Build failed: {error}\n")
Expand Down
1 change: 1 addition & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Runtime helpers used by card-update tests; Home Assistant itself is stubbed.
aiohttp>=3.11,<4
awesomeversion>=24.6,<26
packaging>=24,<27
Loading
Loading