Skip to content

Commit 02b6b19

Browse files
author
Tom Lasswell
committed
feat(readme): custom star-growth SVG card, replace star-history.com embed
Adds 'stars' mode: fetches stargazer timestamps via GitHub API, renders a gold cumulative-growth card matching the other status SVGs. Generated daily by install-stats.yml; moved into the Live status section, full width.
1 parent 8039060 commit 02b6b19

4 files changed

Lines changed: 94 additions & 17 deletions

File tree

.github/workflows/install-stats.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ jobs:
4343
- name: Generate install-stats artifacts
4444
run: python scripts/status_badges.py installs --data-dir out --repo-dir .
4545

46+
- name: Generate star-growth artifact
47+
env:
48+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
run: python scripts/status_badges.py stars --data-dir out
50+
4651
- name: Publish to badges branch
4752
env:
4853
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

3131
<img alt="Installs by version" src="https://raw.githubusercontent.com/lasswellt/govee-homeassistant/badges/versions.svg?v=3" width="99%" />
3232

33+
<img alt="GitHub star growth" src="https://raw.githubusercontent.com/lasswellt/govee-homeassistant/badges/stars-trend.svg?v=1" width="99%" />
34+
3335
</div>
3436

3537
<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>
@@ -263,20 +265,6 @@ logger:
263265

264266
---
265267

266-
## ⭐ Star history
267-
268-
<div align="center">
269-
<a href="https://star-history.com/#lasswellt/govee-homeassistant&Date">
270-
<picture>
271-
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=lasswellt/govee-homeassistant&type=Date&theme=dark" />
272-
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=lasswellt/govee-homeassistant&type=Date" />
273-
<img alt="Star history chart for lasswellt/govee-homeassistant" src="https://api.star-history.com/svg?repos=lasswellt/govee-homeassistant&type=Date" width="70%" />
274-
</picture>
275-
</a>
276-
</div>
277-
278-
---
279-
280268
## Contributing
281269

282270
Issues and PRs welcome. Development quick start:

scripts/bootstrap_badges_branch.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ fi
2525
REMOTE_URL="$(git remote get-url "$REMOTE")"
2626

2727
python3 scripts/status_badges.py installs --data-dir "$STAGE" --repo-dir "$ROOT"
28-
python3 scripts/status_badges.py uptime --data-dir "$STAGE" --repo-dir "$ROOT"
28+
python3 scripts/status_badges.py uptime --data-dir "$STAGE" --repo-dir "$ROOT"
29+
GITHUB_TOKEN="${GITHUB_TOKEN:-$(gh auth token 2>/dev/null || true)}" \
30+
python3 scripts/status_badges.py stars --data-dir "$STAGE"
2931

3032
pushd "$STAGE" >/dev/null
3133
git init -q -b badges

scripts/status_badges.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
import re
2323
import subprocess
2424
import sys
25+
import os
2526
import urllib.request
26-
from datetime import datetime, timezone
27+
from datetime import datetime, timedelta, timezone
2728
from pathlib import Path
2829

2930
ANALYTICS_URL = "https://analytics.home-assistant.io/custom_integrations.json"
@@ -45,7 +46,9 @@
4546
GREEN = "#3fb950"
4647
AMBER = "#d29922"
4748
RED = "#f85149"
49+
STAR = "#e3b341" # GitHub star gold
4850

51+
REPO_SLUG = "lasswellt/govee-homeassistant"
4952
UA = "govee-homeassistant-status/1.0 (+https://github.com/lasswellt/govee-homeassistant)"
5053

5154

@@ -299,6 +302,83 @@ def run_installs(data_dir: Path, repo_dir: Path) -> None:
299302
f"versions_matched={len(fork_counts)}")
300303

301304

305+
# --------------------------------------------------------------------------- #
306+
# stars mode
307+
# --------------------------------------------------------------------------- #
308+
def fetch_stargazers(slug: str) -> list[datetime]:
309+
"""All star timestamps, oldest first. Uses GITHUB_TOKEN if present."""
310+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
311+
out: list[datetime] = []
312+
page = 1
313+
while page <= 50: # 5000-star ceiling
314+
url = f"https://api.github.com/repos/{slug}/stargazers?per_page=100&page={page}"
315+
req = urllib.request.Request(url, headers={
316+
"User-Agent": UA, "Accept": "application/vnd.github.star+json"})
317+
if token:
318+
req.add_header("Authorization", f"Bearer {token}")
319+
with urllib.request.urlopen(req, timeout=30) as resp:
320+
data = json.loads(resp.read().decode("utf-8"))
321+
if not data:
322+
break
323+
out += [datetime.strptime(d["starred_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
324+
for d in data if d.get("starred_at")]
325+
if len(data) < 100:
326+
break
327+
page += 1
328+
return sorted(out)
329+
330+
331+
def render_stars_svg(times: list[datetime], delta30: int) -> str:
332+
w, h = 760, 180
333+
s = card_open(w, h)
334+
pad = 20
335+
total = len(times)
336+
s.append(txt(pad, 34, "GITHUB STARS", 11, MUTED, weight=600, spacing="1.5"))
337+
s.append(txt(pad, 92, human(total), 46, STAR, weight=700))
338+
if delta30 > 0:
339+
s.append(txt(pad + 4, 120, f"★ +{delta30} / 30d", 12, GREEN, weight=600))
340+
341+
gx0, gy0, gw, gh = 220, 30, w - pad - 220, 118
342+
if total >= 2:
343+
first = times[0]
344+
now = _now()
345+
span = (now - first).total_seconds() or 1
346+
pts = []
347+
for i, t in enumerate(times):
348+
px = gx0 + gw * (t - first).total_seconds() / span
349+
py = gy0 + gh - (gh - 8) * (i + 1) / total
350+
pts.append((px, py))
351+
pts.append((gx0 + gw, gy0 + gh - (gh - 8))) # extend to "now" at full count
352+
line = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
353+
area = f"M{gx0},{gy0 + gh} " + " ".join(f"L{x:.1f},{y:.1f}" for x, y in pts) + f" L{pts[-1][0]:.1f},{gy0 + gh} Z"
354+
s.append(
355+
f'<defs><linearGradient id="sg" x1="0" x2="0" y1="0" y2="1">'
356+
f'<stop offset="0" stop-color="{STAR}" stop-opacity="0.4"/>'
357+
f'<stop offset="1" stop-color="{STAR}" stop-opacity="0"/></linearGradient></defs>'
358+
)
359+
s.append(f'<path d="{area}" fill="url(#sg)"/>')
360+
s.append(f'<polyline points="{line}" fill="none" stroke="{STAR}" stroke-width="2" stroke-linejoin="round"/>')
361+
s.append(f'<circle cx="{pts[-1][0]:.1f}" cy="{pts[-1][1]:.1f}" r="3.5" fill="{STAR}"/>')
362+
foot_l = f"since {first:%b %Y} · {total} stargazers"
363+
else:
364+
s.append(txt(gx0 + gw / 2, gy0 + gh / 2, "collecting…", 12, MUTED, anchor="middle"))
365+
foot_l = "collecting…"
366+
367+
s.append(txt(pad, h - 13, foot_l, 10.5, MUTED))
368+
s.append(txt(w - pad, h - 13, stamp(), 10.5, MUTED, anchor="end"))
369+
s.append("</svg>")
370+
return "\n".join(s)
371+
372+
373+
def run_stars(data_dir: Path) -> None:
374+
times = fetch_stargazers(REPO_SLUG)
375+
cutoff = _now() - timedelta(days=30)
376+
delta30 = sum(1 for t in times if t >= cutoff)
377+
write_json(data_dir / "stars.json", shields_endpoint("stars", human(len(times)), "e3b341"))
378+
(data_dir / "stars-trend.svg").write_text(render_stars_svg(times, delta30))
379+
print(f"[stars] total={len(times)} +{delta30}/30d")
380+
381+
302382
# --------------------------------------------------------------------------- #
303383
# uptime mode
304384
# --------------------------------------------------------------------------- #
@@ -448,13 +528,15 @@ def run_uptime(data_dir: Path) -> None:
448528
# --------------------------------------------------------------------------- #
449529
def main() -> int:
450530
ap = argparse.ArgumentParser()
451-
ap.add_argument("mode", choices=["installs", "uptime"])
531+
ap.add_argument("mode", choices=["installs", "uptime", "stars"])
452532
ap.add_argument("--data-dir", default="docs/badges", type=Path)
453533
ap.add_argument("--repo-dir", default=".", type=Path)
454534
args = ap.parse_args()
455535
args.data_dir.mkdir(parents=True, exist_ok=True)
456536
if args.mode == "installs":
457537
run_installs(args.data_dir, args.repo_dir)
538+
elif args.mode == "stars":
539+
run_stars(args.data_dir)
458540
else:
459541
run_uptime(args.data_dir)
460542
return 0

0 commit comments

Comments
 (0)