diff --git a/design/logos/brands/custom_integrations/nestquest/icon.png b/custom_components/nestquest/brand/icon.png similarity index 100% rename from design/logos/brands/custom_integrations/nestquest/icon.png rename to custom_components/nestquest/brand/icon.png diff --git a/design/logos/brands/custom_integrations/nestquest/icon@2x.png b/custom_components/nestquest/brand/icon@2x.png similarity index 100% rename from design/logos/brands/custom_integrations/nestquest/icon@2x.png rename to custom_components/nestquest/brand/icon@2x.png diff --git a/design/logos/brands/custom_integrations/nestquest/logo.png b/custom_components/nestquest/brand/logo.png similarity index 100% rename from design/logos/brands/custom_integrations/nestquest/logo.png rename to custom_components/nestquest/brand/logo.png diff --git a/design/logos/brands/custom_integrations/nestquest/logo@2x.png b/custom_components/nestquest/brand/logo@2x.png similarity index 100% rename from design/logos/brands/custom_integrations/nestquest/logo@2x.png rename to custom_components/nestquest/brand/logo@2x.png diff --git a/design/logos/BRAND-ICONS.md b/design/logos/BRAND-ICONS.md new file mode 100644 index 0000000..0617720 --- /dev/null +++ b/design/logos/BRAND-ICONS.md @@ -0,0 +1,48 @@ +# NestQuest brand icons in Home Assistant + +## Where the icon comes from + +Since Home Assistant 2026.3, a custom integration can ship its own brand +images in `custom_components//brand/`, and Home Assistant serves them +ahead of the brands CDN (`brands.home-assistant.io`). NestQuest ships: + +| File | Size | +|------|------| +| `custom_components/nestquest/brand/icon.png` | 256×256 | +| `custom_components/nestquest/brand/icon@2x.png` | 512×512 | +| `custom_components/nestquest/brand/logo.png` | 256×256 | +| `custom_components/nestquest/brand/logo@2x.png` | 512×512 | + +HACS installs the whole `custom_components/nestquest/` directory, so these +images arrive with the integration. On Home Assistant 2026.3 or newer they +appear on the integrations page and the integration tile, through Home +Assistant's brand endpoint. Older Home Assistant versions ignore the directory +and show the generic placeholder icon. + +The `custom_integrations` folder of the `home-assistant/brands` repository is +legacy for this purpose; NestQuest does not depend on it. + +## Known caveat: the HACS store card + +The HACS store card (the repository listing inside HACS, shown before and +after installing) is rendered by HACS itself, not by the integration. It may +still take its image from the brands CDN or fall back to the avatar of the +repository owner (the `Maniacs-Mansion` organization). Whether HACS reads the +local `brand/` directory for that card has not been verified, so that icon +may stay generic. The repository page inside HACS shows the README, which +embeds the logo from `design/logos/NestQuest-NoBG.png`. + +## Regenerating + +The PNGs are generated offline and deterministically (pure Node, zlib only) +from `design/logos/NestQuest-NoBG.png`, box-filter downscaled with +transparency kept. From the repository root: + +```bash +node design/logos/generate-brand-icons.mjs # write the four PNGs +node design/logos/generate-brand-icons.mjs --check # exit 1 if any drifted +``` + +`tests/test_brand_icons.py` runs the `--check` and asserts the four files +exist with the expected sizes, so a missing or hand-edited image fails the +test suite. diff --git a/design/logos/brands/custom_integrations/nestquest/README.md b/design/logos/brands/custom_integrations/nestquest/README.md deleted file mode 100644 index 0f72028..0000000 --- a/design/logos/brands/custom_integrations/nestquest/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Home Assistant brands files for NestQuest - -These files are prepared for a pull request to -[`github.com/home-assistant/brands`](https://github.com/home-assistant/brands), -placed at `custom_integrations/nestquest/`: - -| File | Size | -|------|------| -| `icon.png` | 256×256 | -| `icon@2x.png` | 512×512 | -| `logo.png` | 256×256 | -| `logo@2x.png` | 512×512 | - -Home Assistant and HACS load integration icons from `brands.home-assistant.io`, -which is built from that repository. Once the pull request is merged, the -NestQuest icon shows on the HACS card and on the Home Assistant integrations -page. Nothing in this repository can change that icon directly. - -The alternative (HACS only) is to set the avatar of the GitHub -`Maniacs-Mansion` organization to the NestQuest logo; HACS falls back to the -repository owner's avatar when no brands icon exists. - -## Regenerating - -The PNGs are generated offline and deterministically (pure Node, zlib only) -from `design/logos/NestQuest-NoBG.png`, box-filter downscaled with -transparency kept. From the repository root: - -```bash -node design/logos/brands/generate-brand-icons.mjs -``` diff --git a/design/logos/brands/generate-brand-icons.mjs b/design/logos/generate-brand-icons.mjs similarity index 79% rename from design/logos/brands/generate-brand-icons.mjs rename to design/logos/generate-brand-icons.mjs index 1ff26f5..00e3562 100644 --- a/design/logos/brands/generate-brand-icons.mjs +++ b/design/logos/generate-brand-icons.mjs @@ -1,10 +1,12 @@ -// Generates the Home Assistant brands files under -// custom_integrations/nestquest/ from the transparent logo -// (design/logos/NestQuest-NoBG.png), box-filter downscaled with alpha kept. -// Adapted from admin/scripts/generate-icons.mjs for RGBA input/output. +// Generates the integration's local brand images in +// custom_components/nestquest/brand/ (read by Home Assistant 2026.3+) from the +// transparent logo (design/logos/NestQuest-NoBG.png), box-filter downscaled +// with alpha kept. Adapted from admin/scripts/generate-icons.mjs for RGBA. // Pure Node (zlib only) — no network, no image dependencies. -// Run (from the repository root): node design/logos/brands/generate-brand-icons.mjs -import { readFileSync, writeFileSync } from "node:fs"; +// Run (from the repository root): +// node design/logos/generate-brand-icons.mjs # write the PNGs +// node design/logos/generate-brand-icons.mjs --check # exit 1 if any drifted +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { deflateSync, inflateSync } from "node:zlib"; import { fileURLToPath } from "node:url"; @@ -149,16 +151,37 @@ function resize({ width, height, px }, size) { return out; } -const logo = decodePng(readFileSync(new URL("../NestQuest-NoBG.png", import.meta.url))); -const outDir = new URL("custom_integrations/nestquest/", import.meta.url); +const logo = decodePng(readFileSync(new URL("NestQuest-NoBG.png", import.meta.url))); +const outDir = new URL("../../custom_components/nestquest/brand/", import.meta.url); const files = [ ["icon.png", 256], ["icon@2x.png", 512], ["logo.png", 256], ["logo@2x.png", 512], ]; +const check = process.argv.includes("--check"); +let drifted = false; +if (!check) mkdirSync(outDir, { recursive: true }); for (const [name, size] of files) { const path = fileURLToPath(new URL(name, outDir)); - writeFileSync(path, encodePng(size, resize(logo, size))); - console.log(`wrote ${path}`); + const png = encodePng(size, resize(logo, size)); + if (check) { + let current = null; + try { + current = readFileSync(path); + } catch { + // missing counts as drift + } + if (!current || !current.equals(png)) { + drifted = true; + console.error(`drift: ${path} differs from a fresh generation`); + } + } else { + writeFileSync(path, png); + console.log(`wrote ${path}`); + } +} +if (drifted) { + console.error("run `node design/logos/generate-brand-icons.mjs` and commit the result"); + process.exit(1); } diff --git a/tests/test_brand_icons.py b/tests/test_brand_icons.py new file mode 100644 index 0000000..0295e8b --- /dev/null +++ b/tests/test_brand_icons.py @@ -0,0 +1,55 @@ +"""The integration's local brand images (Home Assistant 2026.3+). + +``custom_components/nestquest/brand/`` ships through HACS with the +integration, and Home Assistant serves those images ahead of the brands CDN. +They are generated offline from ``design/logos/NestQuest-NoBG.png`` by +``design/logos/generate-brand-icons.mjs``; these checks fail if the directory +goes missing, changes shape, or drifts from a fresh generation. +""" +from __future__ import annotations + +import shutil +import struct +import subprocess +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).parent.parent +BRAND_DIR = REPO_ROOT / "custom_components" / "nestquest" / "brand" +GENERATOR = REPO_ROOT / "design" / "logos" / "generate-brand-icons.mjs" +EXPECTED = { + "icon.png": 256, + "icon@2x.png": 512, + "logo.png": 256, + "logo@2x.png": 512, +} +PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" + + +def test_brand_dir_holds_exactly_the_four_images() -> None: + assert sorted(p.name for p in BRAND_DIR.iterdir()) == sorted(EXPECTED) + + +@pytest.mark.parametrize(("name", "size"), sorted(EXPECTED.items())) +def test_brand_image_is_square_rgba_png(name: str, size: int) -> None: + data = (BRAND_DIR / name).read_bytes() + assert data[:8] == PNG_SIGNATURE + assert data[12:16] == b"IHDR" + width, height, depth, colour = struct.unpack(">IIBB", data[16:26]) + assert (width, height) == (size, size) + assert (depth, colour) == (8, 6) # 8-bit RGBA keeps the transparency + + +def test_generator_reproduces_the_committed_images() -> None: + node = shutil.which("node") + if node is None: + pytest.skip("needs node") + result = subprocess.run( + [node, str(GENERATOR), "--check"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr