# Home Assistant solar: current solar power, with a bar against your system's peak.
#
# Keys pushed by Home Assistant (POST /api/device/<id>/data):
#   solar_w           current solar power in watts, e.g. 1840
#   solar_kwh_today   (optional) energy produced today in kWh, e.g. 6.2
#
# Example payload:
#   {"solar_w": 1840, "solar_kwh_today": 6.2}
#
# The app reads them as config.get("ha_solar_w") and config.get("ha_solar_kwh_today").
# Values that are missing, empty or "unavailable" show as "--".

load("render.star", "render")

# Peak power of your solar system in watts: a full bar.
SOLAR_MAX_W = 4000

AMBER = "#f59e0b"
SUN = "#ffc23d"
WHITE = "#f8f8f0"
DIM = "#70707a"
DARK = "#26262c"

# Bold 7x12 digits: drawn thin (6x11) and thickened by bold().
D_SIDES = ["#....#"]
DIGITS = {
    "0": [".####."] + D_SIDES * 9 + [".####."],
    "1": ["...#..", "..##..", ".#.#.."] + ["...#.."] * 8,
    "2": [".####.", "#....#"] + [".....#"] * 3 + [".####."] + ["#....."] * 4 + ["######"],
    "3": [".####.", "#....#"] + [".....#"] * 3 + ["..###."] + [".....#"] * 3 + ["#....#", ".####."],
    "4": D_SIDES * 5 + ["######"] + [".....#"] * 5,
    "5": ["######"] + ["#....."] * 3 + ["#####."] + [".....#"] * 4 + ["#....#", ".####."],
    "6": [".####.", "#....#", "#.....", "#.....", "#####."] + D_SIDES * 5 + [".####."],
    "7": ["######", ".....#", ".....#", "....#.", "....#.", "...#..", "...#.."] + ["..#..."] * 4,
    "8": [".####."] + D_SIDES * 4 + [".####."] + D_SIDES * 4 + [".####."],
    "9": [".####."] + D_SIDES * 4 + [".#####"] + [".....#"] * 3 + ["#....#", ".####."],
    ".": ["."] * 10 + ["#"],
    "-": ["...."] * 5 + ["####"] + ["...."] * 5,
}
DIGIT_GAP = 2

def num(s, default):
    # Starlark has no try/except: only accept digits, one dot and a leading minus.
    s = s.strip()
    neg = s.startswith("-")
    body = s[1:] if neg else s
    if body == "" or body == "." or body.count(".") > 1:
        return default
    for ch in body.elems():
        if ch not in "0123456789.":
            return default
    if body.startswith("."):
        body = "0" + body
    if body.endswith("."):
        body = body + "0"
    return float(("-" if neg else "") + body)

def fixed(x, dec):
    # Like "%.2f" (Starlark has no precision in %): round half away from zero.
    scale = 1
    for _ in range(dec):
        scale *= 10
    n = int(abs(x) * scale + 0.5)
    s = str(n // scale)
    if dec > 0:
        s += "." + ("0" * dec + str(n % scale))[-dec:]
    return "-" + s if x < 0 and n > 0 else s

def pixels(rows, x, y, color):
    # One box per horizontal run of lit pixels.
    out = []
    for r, line in enumerate(rows):
        start = -1
        for c in range(len(line) + 1):
            on = c < len(line) and line[c] == "#"
            if on and start < 0:
                start = c
            elif not on and start >= 0:
                out.append(render.Padding(pad = (x + start, y + r, 0, 0), child = render.Box(width = c - start, height = 1, color = color)))
                start = -1
    return out

def bold(rows):
    # Thicken every stroke to 2 px: a pixel is lit when it or its left/upper neighbour is.
    h = len(rows)
    w = len(rows[0])
    out = []
    for r in range(h + 1):
        line = ""
        for c in range(w + 1):
            on = False
            for dr, dc in [(0, 0), (1, 0), (0, 1), (1, 1)]:
                rr = r - dr
                cc = c - dc
                if rr >= 0 and rr < h and cc >= 0 and cc < w and rows[rr][cc] == "#":
                    on = True
            line += "#" if on else "."
        out.append(line)
    return out

def big_width(text):
    w = 0
    for ch in text.elems():
        if ch in DIGITS:
            w += len(DIGITS[ch][0]) + 1 + DIGIT_GAP
    return w - DIGIT_GAP if w > 0 else 0

def big(text, color):
    out = [render.Box(width = 1, height = 12)]  # full height, so "-" sits mid-line
    x = 0
    for ch in text.elems():
        if ch in DIGITS:
            glyph = bold(DIGITS[ch])
            out.extend(pixels(glyph, x, 0, color))
            x += len(glyph[0]) + DIGIT_GAP
    return render.Stack(children = out)

# "#" = sun disc, "+" = rays.
SUN_ICON = [
    "......+......",
    "..+...+...+..",
    "...+.....+...",
    ".....###.....",
    "....#####....",
    "...#######...",
    "++.#######.++",
    "...#######...",
    "....#####....",
    ".....###.....",
    "...+.....+...",
    "..+...+...+..",
    "......+......",
]

def at(x, y, child):
    return render.Padding(pad = (x, y, 0, 0), child = child)

def power_text(w):
    # (big digits, unit); None when there is no usable value.
    if w == None or w >= 999999.5:
        return None
    if w < 0:
        w = 0  # some inverters report -1 W at night
    if w < 9999.5:
        return str(int(w + 0.5)), "W"
    kw = w / 1000
    return fixed(kw, 1 if kw < 99.95 else 0), "kW"

def main(config):
    solar = num(config.get("ha_solar_w", "1840"), None)
    today = num(config.get("ha_solar_kwh_today", "6.2"), None)

    power = power_text(solar)
    active = power != None and solar >= 0.5

    if active:
        rays = [r.replace("#", ".").replace("+", "#") for r in SUN_ICON]
        icon = pixels(SUN_ICON, 1, 3, SUN) + pixels(rays, 1, 3, AMBER)
    else:
        icon = pixels([r.replace("+", "#") for r in SUN_ICON], 1, 3, DARK if power == None else DIM)

    children = [render.Box(width = 64, height = 32)]
    children.extend(icon)
    children.append(at(18, 1, render.Text("SOLAR", font = "tom-thumb", color = DIM)))

    if power == None:
        children.append(at(18, 8, big("--", DIM)))
    else:
        digits, unit = power
        color = WHITE if active else DIM
        children.append(at(18, 8, big(digits, color)))
        children.append(at(18 + big_width(digits) + 2, 12, render.Text(unit, font = "5x8", color = AMBER if active else DIM)))

    # Today's energy under the value; share of the peak top right.
    if today == None or today < 0 or today >= 99999.5:
        today_txt = "--"
    else:
        today_txt = fixed(today, 1 if today < 99.95 else 0)
    children.append(at(1, 21, render.Row(children = [
        render.Text("TODAY ", font = "tom-thumb", color = DIM),
        render.Text(today_txt, font = "tom-thumb", color = WHITE),
        render.Text(" KWH", font = "tom-thumb", color = DIM),
    ])))

    frac = 0.0
    if power != None:
        frac = min(max(solar, 0) / SOLAR_MAX_W, 1.0)
        pct = str(int(frac * 100 + 0.5)) + "%"
        children.append(at(64 - len(pct) * 4, 1, render.Text(pct, font = "tom-thumb", color = AMBER if active else DIM)))

    # Bar with quarter ticks.
    children.append(at(1, 28, render.Box(width = 62, height = 3, color = DARK)))
    fill = int(62 * frac + 0.5)
    if fill > 0:
        children.append(at(1, 28, render.Box(width = fill, height = 3, color = AMBER)))
    for q in [1, 2, 3]:
        x = 1 + int(62 * q / 4)
        children.append(at(x, 28, render.Box(width = 1, height = 3, color = "#000000" if x <= fill else "#3c3c44")))

    return render.Root(child = render.Box(width = 64, height = 32, color = "#000000", child = render.Stack(children = children)))
