Skip to content

Commit d661f8e

Browse files
author
Tom Lasswell
committed
feat(readme): version-breakdown graphic (installs by release)
Adds versions.svg to the installs generator: horizontal bar chart of active installs per released version (top 8 + older rollup), latest-in-use highlighted. Same daily data/cadence as install-stats; embedded in README Live status.
1 parent c5648ae commit d661f8e

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
 
2929
<img alt="Govee API uptime" src="https://raw.githubusercontent.com/lasswellt/govee-homeassistant/badges/api-uptime.svg" width="49%" />
3030

31+
<img alt="Installs by version" src="https://raw.githubusercontent.com/lasswellt/govee-homeassistant/badges/versions.svg" width="60%" />
32+
3133
</div>
3234

3335
<sub>**Active installs** counts only versions **released by this repository** — other `govee` forks and legacy installs sharing the same domain are excluded — and reflects Home Assistant instances opted into Usage‑level analytics, so true usage is higher. **Govee API status** pings `openapi.api.govee.com` and `app2.govee.com` hourly: round of red bars on the right = an outage today, not a problem with your setup. Both graphs update automatically via GitHub Actions ([uptime](.github/workflows/uptime.yml) · [install‑stats](.github/workflows/install-stats.yml)).</sub>

scripts/status_badges.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,70 @@ def render_installs_svg(history: list, fork_total: int, official_total: int) ->
212212
return "\n".join(s)
213213

214214

215+
def _vkey(v: str) -> tuple:
216+
try:
217+
return tuple(int(x) for x in v.split("."))
218+
except ValueError:
219+
return ()
220+
221+
222+
def render_versions_svg(fork_counts: dict[str, int], fork_total: int) -> str:
223+
items = sorted(fork_counts.items(), key=lambda kv: -kv[1])
224+
top = items[:8]
225+
rest = items[8:]
226+
rows = list(top)
227+
rest_sum = sum(c for _, c in rest)
228+
if rest_sum:
229+
rows.append((f"+{len(rest)} older", rest_sum))
230+
231+
latest = max((v for v, _ in items), key=_vkey, default="")
232+
on_latest = fork_counts.get(latest, 0)
233+
234+
w = 480
235+
row_h = 23
236+
top_pad = 66
237+
h = top_pad + len(rows) * row_h + 24
238+
s = card_open(w, h)
239+
pad = 20
240+
s.append(txt(pad, 32, "VERSION BREAKDOWN", 11, MUTED, weight=600, spacing="1.5"))
241+
s.append(txt(pad, 50, "active installs by release", 11.5, MUTED))
242+
s.append(txt(w - pad, 42, human(fork_total), 24, ACCENT, weight=700, anchor="end"))
243+
244+
label_w = 78
245+
bar_x = pad + label_w
246+
bar_max = w - pad - bar_x - 84 # reserve right column for count + %
247+
maxc = max((c for _, c in rows), default=1)
248+
for i, (ver, c) in enumerate(rows):
249+
y = top_pad + i * row_h
250+
cy = y + row_h / 2 + 4
251+
is_latest = ver == latest
252+
col = GREEN if is_latest else ACCENT
253+
s.append(txt(bar_x - 8, cy, ver, 11.5, TEXT if is_latest else MUTED,
254+
weight=700 if is_latest else 400, anchor="end"))
255+
bw = max(bar_max * c / maxc, 2)
256+
s.append(f'<rect x="{bar_x}" y="{y + 4}" width="{bw:.1f}" height="{row_h - 11}" rx="3" '
257+
f'fill="{col}" fill-opacity="{0.95 if is_latest else 0.65}"/>')
258+
pct = c / fork_total * 100 if fork_total else 0
259+
s.append(txt(w - pad, cy, f"{human(c)} · {pct:.0f}%", 10.5, MUTED, anchor="end"))
260+
261+
foot = f"{latest} leads · {len(fork_counts)} releases in use · {_now():%b %-d}" if fork_total else "collecting…"
262+
s.append(txt(pad, h - 11, foot, 10.5, MUTED))
263+
s.append("</svg>")
264+
return "\n".join(s)
265+
266+
215267
def run_installs(data_dir: Path, repo_dir: Path) -> None:
216268
fv = fork_versions(repo_dir)
217269
blob = fetch_json(ANALYTICS_URL)
218270
entry = blob.get(DOMAIN, {})
219271
versions = entry.get("versions", {})
220272
official_total = int(entry.get("total", 0))
221-
fork_total = sum(
222-
int(c) for ver, c in versions.items() if (nv := normalize_version(ver)) and nv in fv
223-
)
273+
fork_counts: dict[str, int] = {}
274+
for ver, c in versions.items():
275+
nv = normalize_version(ver)
276+
if nv and nv in fv:
277+
fork_counts[nv] = fork_counts.get(nv, 0) + int(c)
278+
fork_total = sum(fork_counts.values())
224279

225280
today = f"{_now():%Y-%m-%d}"
226281
hist_path = data_dir / "installs-history.json"
@@ -234,8 +289,9 @@ def run_installs(data_dir: Path, repo_dir: Path) -> None:
234289

235290
write_json(data_dir / "installs.json", shields_endpoint("active installs", human(fork_total), "41BDF5"))
236291
(data_dir / "installs-trend.svg").write_text(render_installs_svg(history, fork_total, official_total))
237-
print(f"[installs] fork={fork_total} (of {official_total} domain total) versions_matched="
238-
f"{sum(1 for v in versions if (n := normalize_version(v)) and n in fv)}")
292+
(data_dir / "versions.svg").write_text(render_versions_svg(fork_counts, fork_total))
293+
print(f"[installs] fork={fork_total} (of {official_total} domain total) "
294+
f"versions_matched={len(fork_counts)}")
239295

240296

241297
# --------------------------------------------------------------------------- #

0 commit comments

Comments
 (0)