Skip to content

Distributing Profiles

Profiles can be downloaded and installed at runtime from git-hosted repositories — no redeploy, no .env edits, no code execution. Because a profile is pure data, the only thing that ever gets fetched and stored is a validated JSON blob.

A “repo” is a public git repository containing:

  1. A root index.json listing the profiles it offers:

    {
    "profiles": [
    {
    "id": "acme-hybrid",
    "name": "Acme Hybrid",
    "manufacturer": "Acme",
    "version": "1.0.0",
    "path": "profiles/acme.json",
    "description": "Acme 5–12 kW hybrid range"
    }
    ]
    }
  2. One committed ProfileData JSON file per entry, at the path given in the index — exactly what defineProfile emits, serialized.

One repo can offer many profiles — hundreds is fine. The index lists every profile and each has its own profiles/<id>.json, so a single repo can cover a manufacturer’s whole line-up. Authoring a family is the tidy way to do this: defineFamily emits the base plus one profile per model, and profile build writes all of them into the same repo.

You don’t have to write this layout by hand: bunx profile build ./src/profiles.ts --out . generates it from code-defined profiles — validate, emit, commit, push.

The official source ships as a default, protected source — the core itself carries no profile. https://github.com/SunReye/SunReye-Official-Profiles is merged in automatically; it can be disabled but not removed (shown as “Default” in the UI). Add more repositories yourself. Source URLs must be https:// (a trailing .git is optional).

From Settings → Profiles (admin only) you can:

  • Add / remove / enable repositories — the “Profile repositories” section. Edits auto-save.
  • Browse all enabled repos to see available profiles, grouped by manufacturer and then family, each row labelled with its source repo and annotated installed, update available (the repo offers a semver-newer release), or downloadable.
  • Download a profile — SunReye fetches its ProfileData, validates it, and stores it.
  • Set active — choose which installed profile the server runs.
  • Remove an installed profile (not the active one).

A background checker runs independently of the poll loop: shortly after boot, then every few hours, it syncs enabled sources and diffs installed versions against them (semver-aware — only a genuinely newer release counts). The result is cached behind a public GET /api/profiles/updates endpoint, so Settings → Profiles can show an updates banner with one-click updates without an admin manually browsing. The checker stops on graceful shutdown.

An update only reflects a newer version when the author bumped it — see change-aware versioning for how profile build does that automatically.

  • Repos are shallow-cloned into a temp cache (clone --depth 1, or fetch + reset on update). The clone cache is disposable; the database is the source of truth.
  • Downloaded profiles are re-validated with the strict schema and stored in the installed_profiles table (id, source, version, data, installedAt).
  • Security guards: https-only URLs, a 30 s git timeout with terminal prompts disabled, path-traversal protection, and a 1 MB per-file cap. Since profiles are data-only, the worst a malicious repo can do is fail validation.

Installing a profile just persists a validated row. The active profile shapes the REST routes, the manifest, and the MQTT topics — and those are built once at boot. So:

  • Downloading a profile makes it available immediately.
  • Switching the active profile takes effect on the next restart, and the Settings UI shows a persistent “Restart required” banner until then.

This is intentional — it keeps the boot model simple and avoids rebuilding every surface live.

On startup, profiles are registered in two phases before any routes or topics are built:

  1. Code profiles — first-party packages can self-register on import, but none ship in the core today (the box carries no profile).
  2. Installed profiles are read from the database, each re-validated (a stored row may predate a schema change) and registered; an invalid row is logged and skipped, so one bad download can’t take the server down.

Then the active profile id — from settings, or seeded from INVERTER_PROFILE — is resolved and the engine is built for it. When nothing is configured (a fresh install), the server boots in a degraded, onboarding-only mode: the admin picks a profile and tests the Modbus connection with it from the first-run flow, then restarts into the full API.

  • No hot-reload of the active profile (restart-to-apply, above).
  • No executable code in downloaded profiles — simulators and arbitrary compute closures remain the preserve of trusted first-party npm packages.
  • Not a general package manager — a repo is just a git repo with an index and JSON files.