Room page: colour swatches send their colour, not white

oklchToRgb read the colour back from the browser's CSS engine and expected
rgb(...). Current Chromium (the kiosk's 153) resolves an oklch colour to
oklch(...), so the match failed and every swatch fell back to [255, 255, 255]:
tapping a colour set the light to white. It now does the OKLab maths itself.

Also log a Home Assistant call that fails in the proxy: httpx only logs a
request once it has a response, so a call that timed out left no trace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 22:24:02 +02:00
parent de94bd4118
commit cf38ae6fe2
3 changed files with 65 additions and 12 deletions

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { DINOSAUR } from "../skins/dinosaur";
import { DOLPHIN } from "../skins/dolphin";
import { oklchToRgb } from "../oklch";
describe("oklchToRgb", () => {
it("converts the dolphin swatches", () => {
// Cross-checked against an independent OKLab implementation.
expect(DOLPHIN.lightSwatches.map((swatch) => oklchToRgb(swatch.oklch))).toEqual([
[240, 220, 177],
[239, 204, 54],
[241, 77, 76],
[0, 171, 237],
[66, 203, 128],
[177, 106, 224],
]);
});
it("gives every swatch of every skin its own colour", () => {
for (const skin of [DOLPHIN, DINOSAUR]) {
const colours = skin.lightSwatches.map((swatch) => oklchToRgb(swatch.oklch).join());
expect(new Set(colours).size).toBe(colours.length);
}
});
it("knows black and white", () => {
expect(oklchToRgb("oklch(0% 0 0)")).toEqual([0, 0, 0]);
expect(oklchToRgb("oklch(100% 0 0)")).toEqual([255, 255, 255]);
expect(oklchToRgb("oklch(1 0 0)")).toEqual([255, 255, 255]);
});
});

View File

@@ -1,15 +1,32 @@
/** Home Assistant's `light.turn_on` wants `rgb_color: [r, g, b]`, not oklch. Rather than
* hand-transcribing the oklch->sRGB math, this leans on the browser's own CSS engine:
* set the string as an element's color and read back what the browser resolved it to.
* Only ever called a handful of times (once per fixed swatch), never per render. */
/** Home Assistant's `light.turn_on` wants `rgb_color: [r, g, b]`, not oklch.
*
* Computed here rather than read back from the browser's CSS engine, which is what this
* used to do: current Chromium resolves an oklch colour to `oklch(...)`, not `rgb(...)`
* (CSS Color 4), so reading it back failed on the kiosk and every swatch became white.
* The maths is Björn Ottosson's OKLab -> linear sRGB, then the sRGB transfer curve;
* colours outside sRGB are clipped per channel. */
export function oklchToRgb(oklch: string): [number, number, number] {
const el = document.createElement("div");
el.style.color = oklch;
document.body.appendChild(el);
const resolved = getComputedStyle(el).color;
document.body.removeChild(el);
const match = /oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)\s*\)/.exec(oklch);
if (!match) throw new Error(`not an oklch(L C H) colour: ${oklch}`);
const lightness = Number(match[1]) / (match[2] ? 100 : 1);
const chroma = Number(match[3]);
const hue = (Number(match[4]) * Math.PI) / 180;
const match = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(resolved);
if (!match) return [255, 255, 255];
return [Number(match[1]), Number(match[2]), Number(match[3])];
const a = chroma * Math.cos(hue);
const b = chroma * Math.sin(hue);
const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
const linear = [
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
] as const;
const [red, green, blue] = linear.map((channel) => {
const clipped = Math.min(1, Math.max(0, channel));
const encoded = clipped <= 0.0031308 ? 12.92 * clipped : 1.055 * clipped ** (1 / 2.4) - 0.055;
return Math.round(encoded * 255);
});
return [red!, green!, blue!];
}