"""Render the integration's brand icons (radiator with a forecast curve). Home Assistant 2026.3+ shows custom_components//brand/ images in Settings -> Devices & services. Development-only: needs Pillow, which the HA test requirements already install. Run: python scripts/make_brand_icons.py """ from math import hypot from pathlib import Path from PIL import Image, ImageDraw OUT = Path(__file__).resolve().parents[1] / "custom_components" / "heating_mpc" / "brand" DESIGN = 512 # coordinates below are in a 512x512 design space SUPERSAMPLE = 8 THEMES = { "": {"radiator": "#F4511E", "forecast": "#1E88E5"}, # for light backgrounds "dark_": {"radiator": "#FF7043", "forecast": "#64B5F6"}, # for dark backgrounds } def bezier(p0, p1, p2, p3, steps=400): points = [] for i in range(steps + 1): t = i / steps u = 1 - t points.append(tuple(u**3 * a + 3 * u * u * t * b + 3 * u * t * t * c + t**3 * d for a, b, c, d in zip(p0, p1, p2, p3))) return points def stroke(draw, points, width, color): """Smooth round-capped stroke: overlapping discs along the path. Pillow's wide polylines show seams between many short segments. """ r = width / 2 for (x0, y0), (x1, y1) in zip(points, points[1:]): steps = max(1, int(hypot(x1 - x0, y1 - y0) / (r / 8))) for i in range(steps + 1): x, y = x0 + (x1 - x0) * i / steps, y0 + (y1 - y0) * i / steps draw.ellipse((x - r, y - r, x + r, y + r), fill=color) def split_dashes(points, dash, gap): """Cut a polyline into dash polylines of the given arc lengths.""" dashes, current, travelled, drawing = [], [points[0]], 0.0, True for (x0, y0), (x1, y1) in zip(points, points[1:]): travelled += hypot(x1 - x0, y1 - y0) if drawing: current.append((x1, y1)) limit = dash if drawing else gap if travelled >= limit: if drawing and len(current) > 1: dashes.append(current) current, travelled, drawing = [(x1, y1)], 0.0, not drawing if drawing and len(current) > 1: dashes.append(current) return dashes def render(colors): s = SUPERSAMPLE image = Image.new("RGBA", (DESIGN * s, DESIGN * s), (0, 0, 0, 0)) draw = ImageDraw.Draw(image) scale = lambda pts: [(x * s, y * s) for x, y in pts] # Radiator: five rounded columns joined by two headers. fins, left, right, top, bottom = 5, 12, 500, 250, 500 gap = 22 width = (right - left - gap * (fins - 1)) / fins for i in range(fins): x0 = left + i * (width + gap) draw.rounded_rectangle((x0 * s, top * s, (x0 + width) * s, bottom * s), radius=width / 2 * s, fill=colors["radiator"]) for y in (296, 420): draw.rectangle(((left + width / 2) * s, y * s, (right - width / 2) * s, (y + 34) * s), fill=colors["radiator"]) # Forecast: measured past (solid), "now" marker, predicted future (dashed). line = 40 curve = bezier((32, 192), (170, 192), (300, 146), (480, 32)) now = next(i for i, (x, _) in enumerate(curve) if x >= 220) stroke(draw, scale(curve[:now + 1]), line * s, colors["forecast"]) for dash in split_dashes(curve[now:], 64, 44)[1:]: stroke(draw, scale(dash), line * s, colors["forecast"]) x, y = curve[now] radius = 40 draw.ellipse(((x - radius) * s, (y - radius) * s, (x + radius) * s, (y + radius) * s), fill=colors["forecast"]) return image def main(): OUT.mkdir(exist_ok=True) for prefix, colors in THEMES.items(): image = render(colors) for suffix, size in (("", 256), ("@2x", 512)): path = OUT / f"{prefix}icon{suffix}.png" image.resize((size, size), Image.LANCZOS).save(path, optimize=True) print(f"wrote {path.relative_to(OUT.parents[2])}") if __name__ == "__main__": main()