This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Solisdash — a small web app that surfaces solar/inverter data from the SolisCloud (Ginlong Solis) cloud platform.
Shape, decided 2026-05-13:
- View: hybrid — live tiles (current power, today's yield, battery SOC) at the top, historical charts (day/week/month/year) below.
- Audience: "me plus a few people" — shared instance, simple login, one shared SolisCloud key for v1 (per-user keys deferred). Not public/multi-tenant; no sign-up flow.
- Data flow: hybrid — a scheduled poller pulls history into MongoDB, and the live tiles call SolisCloud directly (through a short server-side cache) so the dashboard stays useful during SolisCloud outages.
.env at the repo root holds the SolisCloud credentials. Never read its values into transcripts or logs; reference variable names only.
| Variable | Purpose |
|---|---|
SOLIS_KEY_ID |
SolisCloud API Key ID (the apiId in the Authorization header). |
SOLIS_KEYSECRET |
SolisCloud API Key Secret (HMAC-SHA1 signing key). One word — no underscore between KEY and SECRET. |
SOLIS_API_URL |
Region-specific base URL, e.g. https://www.soliscloud.com:13333. |
SOLIS_MONGODB_URI |
MongoDB Atlas connection string (mongodb+srv://…). Database name is solis. Local dev must point at a non-prod cluster. |
SOLIS_MONGODB_DB |
Database name (default solis). Override for sandbox runs. |
SOLIS_STATION_ID |
Optional — pin the dashboard to one station. Empty = pick the first one returned by userStationList. |
SESSION_SECRET |
Session-cookie signing key. Required in production. |
RUN_SCHEDULER |
Default false. Set true to run the in-process poller alongside uvicorn (live tiles work without it). |
SCHEDULER_SAMPLE_MINUTES |
How often the scheduler pulls stationDetail into station_samples. Default 5. |
SCHEDULER_DAILY_HOUR_UTC / SCHEDULER_DAILY_MINUTE_UTC |
When the daily rollup runs. Default 00:30 UTC. |
SCHEDULER_RATE_PER_SEC |
Outbound rate-limit for the poller's token bucket. Default 1.5 (SolisCloud's per-endpoint cap is 2). |
.gitignore lists .env so it can't be committed. See .env.example for the full set.
Hosting: production runs on a Digital Ocean droplet (single uvicorn process under systemd, planned). Local dev runs the same invoke start task. The poller is in-process under uvicorn's lifespan (APScheduler) — no separate poller service.
SolisCloud Platform API Document V2.0.3.pdf — the upstream API spec. Read it before implementing any new endpoint binding.
Things the spec dictates (verify in the PDF before relying on them in code):
- Auth is per-request HMAC-SHA1. Required headers:
Date,Content-MD5,Content-Type,Authorization(the apiId travels inside Authorization — no separateKeyIdheader, despite the PDF mentioning one in passing). The signing string is a canonicalised concatenation of method + Content-MD5 + Content-Type + Date + resource path. Content-Type must be bareapplication/json, notapplication/json;charset=UTF-8— the §2.2 prose contradicts §2.4's worked example, and the live API returns 403wrong signif you send the charset suffix. Get the signing helper right once and unit-test it against the worked examples in the PDF — most integration bugs live here. - Base URL is region-specific. Pick it from the PDF, do not hard-code the EU host.
- Rate limits are strict and per-key. Client must back off on
1004/1007-class errors instead of tight retries. The poller cadence has to respect this — assume minute-granularity at best for "live" calls, lower for history. - Response envelope is
{code, msg, data, success}. Surface the upstreamcodein any raised exception so callers can branch on it.
- Python + uv for everything (
uv run python -m …,uv sync,uv pip install). - FastAPI for the web app.
- MongoDB Atlas for persistence. Connection string read from
SOLIS_MONGODB_URI(mongodb+srv://…). Database name issolis. Local dev must point at a non-prod Atlas cluster — never the production cluster. - HTMX + Jinja2 for the frontend. React is forbidden by global rules; HTMX fits FastAPI's server-rendered model and keeps the JS surface tiny. Chart.js (vendored or CDN) for historical charts.
- invoke for build/admin tasks — including
start/stop/restartof the FastAPI app,poll-once,backfill,add-user,add-solis-account. - argparse for any standalone CLI scripts.
- Type hints on every function and class.
All Python invocations go through uv run — bare pytest / invoke can be intercepted by pyenv.
uv sync— install/refresh dependencies (after editingpyproject.toml).uv run python -m invoke start— start app under uvicorn, detached, pidfile invar/uvicorn.pid, logs tovar/uvicorn.log. Add--reloadfor dev autoreload.uv run python -m invoke stop/restart/status— lifecycle and/healthprobe.uv run python -m invoke test— full pytest suite in parallel (-n auto).uv run python -m invoke lint— ruff + mypy.- Single test:
uv run python -m pytest tests/test_smoke.py::test_health -q(or pass-k <expr>).
Renamed the project directory? uv sync rebuilds packages but does not regenerate the entry-point wrapper scripts in .venv/bin/ — their shebangs are baked at install time. After any directory rename, run rm -rf .venv && uv sync --extra dev to get fresh shebangs; otherwise uv run invoke … and friends crash with bad interpreter: No such file or directory.
Pushing a vX.Y.Z tag to origin triggers .github/workflows/release.yml, which builds with uv build and publishes the sdist + wheel to PyPI via OIDC trusted publishing — no API tokens, no local uv publish. The PyPI side needs jdrumgoole/solisdash registered as a trusted publisher pointing at release.yml (one-time setup at https://pypi.org/manage/account/publishing/).
The scaffold step (steps 1–2 below) has already landed. Subsequent feature work follows the sequence in tasks/todo.md — do not skip ahead:
Plan intasks/todo.md.Scaffoldpyproject.toml,.python-version,tasks.py,src/solisdash/,tests/.- SolisCloud auth/signing helper first, with regression tests pinned to the worked examples in the V2.0.3 PDF — every later piece depends on it.
- Then station/inverter list endpoints, then live tiles, then poller + history.
Develop on a feature branch in a git worktree (git worktree add ../solisdash-<branch> -b <branch>), never directly on main. Bump the version in pyproject.toml and src/solisdash/__init__.py on every push (keep the two in sync).
These names recur in the API and should be used as-is in code/UI to keep grep-ability with the spec:
- Station — a physical site (one address, one owner). Has an
idand anuserId. - Inverter — a device under a station, identified by
sn(serial number). - Collector — the data-logger that ships inverter telemetry to SolisCloud.
- Battery / EPM / Meter — accessory devices reported alongside the inverter when present.
Common endpoints the dashboard will lean on (verify paths against the PDF):
/v1/api/userStationList— paginated station list for a user-key./v1/api/stationDetail— current state for one station (live tiles)./v1/api/inverterList,/v1/api/inverterDetail— per-inverter drill-down./v1/api/stationDay/stationMonth/stationYear/stationAll— historical energy series for charts./v1/api/alarmList— fault/alarm feed.
- Update sections of this file with
file:linereferences once the corresponding modules land — replace prose with pointers. - If anything in the live code contradicts a note here, trust the code and fix the note.