Skip to content

Configuration

Martin Pospisil edited this page Sep 23, 2026 · 10 revisions

Configuration

Settings live in appsettings.json — src/Solax.Worker/appsettings.json in a clone, /app/appsettings.json inside the image — and any of them can be overridden by an environment variable without editing the file or rebuilding.

Running from source, editing the file is the simplest thing and the table below reads as Solax:Inverter:Host. Running a container, you cannot edit it, so the same settings arrive as the __ variables shown. Both forms mean the same setting; the environment wins where both are present.

How overrides map

The app uses the ASP.NET Core convention: a double underscore __ replaces the : between configuration sections.

appsettings.json           environment variable
──────────────────────────────────────────────────────
Solax:Inverter:Host    →   Solax__Inverter__Host
HomeAssistant:Password →   HomeAssistant__Password
Web:PasswordHash       →   Web__PasswordHash
Controller:TimeZone    →   Controller__TimeZone

Settings you will actually set

Variable Meaning
Pv__Inverter__Host Required Inverter IP address on the LAN
Pv__Chargers__0__Host Required EV charger IP address
TZ Linux Local timezone, IANA id — e.g. Europe/Prague
Controller__TimeZone Windows Windows zone id. Leave empty on Linux — see Platforms.
Web__Enabled Serve the controller's own web UI — see below. On by default; set false to bind no socket at all.
Web__Port Port the UI listens on. Default 8090; in a container it must match the published port.
Web__PasswordHash Advanced. Hash of the single shared UI password. Unset by default, which means no login — setting it is what turns the login on. See below.
Web__RequireAuthentication Advanced. Overrides the line above in either direction; leave unset. See below.
HomeAssistant__Enabled Publish to Home Assistant over MQTT discovery — see MQTT. Off by default.
HomeAssistant__BrokerHost MQTT broker hostname — the compose service name, not localhost
HomeAssistant__Username
HomeAssistant__Password
Broker credentials; must match the broker's password file. Only needed if HomeAssistant__Enabled is true.
Solcast__ApiKey
Solcast__ResourceId
Optional — needed only for the Forecasted mode. See Solcast.
Weather__ApiKey Optional — records the weather each charging session ran in. Nothing in charge control reads it. The site's coordinates come from Pv__Latitude / Pv__Longitude. See Weather.
Ev__Vehicles__0__BatteryCapacityKWh Optional — the car's usable pack. Without it, no target can be asked for as a percentage. See The car below.
Ev__Vehicles__0__Phases Optional, and the one worth getting right — see the warning under The car.
ChargeControl__DryRun Writes Simulate charger writes instead of performing them
BatteryHold__Enabled
BatteryHold__DryRun
Writes Off and dry-run by default. Verify registers first.

Warning

Keys that have moved make the controller refuse to start, naming the replacement, rather than being quietly ignored. If you are upgrading from an older release, the ones that moved are Solax__Inverter__* → Pv__Inverter__*, Solax__EvCharger__* → Pv__Chargers__0__*, Weather__Latitude / Weather__Longitude → Pv__Latitude / Pv__Longitude, and Vehicle__BatteryCapacityKWh / Vehicle__ChargeEfficiency / Vehicle__Topic → the Ev section below. Refusing beats ignoring: a value your file says is set and the controller has stopped reading is invisible.

If you deploy with deploy/deploy.sh, the compose file handles all of this and your .env needs no edit — the variable names there are unchanged.

The car

What the car is, as distinct from the feed that reports on it. All optional; describe none of it and every charging limit is the installation's, exactly as before this section existed.

Variable Meaning
Ev__Vehicles__0__Id
Ev__Vehicles__0__Name
Identity. The id is a slug; the name is what you see.
Ev__Vehicles__0__Make
Ev__Vehicles__0__Model
Reported, never acted on.
Ev__Vehicles__0__BatteryCapacityKWh The usable pack, not the brochure's gross figure — an ID.4 Pro is 77 usable of 82. Without it, no target can be asked for as a percentage.
Ev__Vehicles__0__ChargeEfficiency Charger meter → cells, default 0.9. Not the home battery's figure.
Ev__Vehicles__0__Phases How many phases the car can use.
Ev__Vehicles__0__MinChargingCurrentAmps The lowest current it will actually start on.
Ev__Vehicles__0__MaxChargingCurrentAmps Its on-board charger's ceiling.
Ev__Vehicles__0__Telemetry__Topic Where its readings arrive — see MQTT.

Warning

Phases is the one that goes wrong quietly. Every watts-to-amps conversion in the controller runs through a phase count, and without this it uses the wallbox's. If your car charges single-phase behind a three-phase wallbox and you do not say so, every power figure the controller reasons with is overstated threefold: a fast charge scheduled for a departure starts hours late, and the forecast plan budgets energy the car can never take. Nothing looks broken — the numbers are simply wrong.

The controller always works to the narrower of the two. The effective minimum is the higher of the car's and the charger's; the effective maximum is the lower; the effective phase count is the smaller. A car can only ever lower a limit — an installation limited to 16 A stays limited to 16 A behind a car that would take 32, because that is the site's supply and the wiring in the wall. All three columns are shown on the /pv-system page, so "why is my 32 A car charging at 16?" is answerable in a browser.

A car whose minimum is above the installation's maximum could never charge, and every symptom of that is silence — so it is refused at startup naming both keys.

Only one vehicle is supported. The list is a list so that a second one does not need a configuration change the day it can be driven.

Reading the car: an ID.4 or a Škoda

Which live source reads the car is chosen by its own section in .env, never by Make. One installation has one car, so the two live sources below are alternatives — enabling both stops startup with a sentence naming both. The EU Data Act portal (VW_ENABLED, VW_BRAND) may run beside either.

A Volkswagen, read from volkswagen.de while it charges (a one-time code under Car → Account signs in):

Ev__Vehicles__0__Id=id4
Ev__Vehicles__0__Make=Volkswagen
Ev__Vehicles__0__BatteryCapacityKWh=77
VW_WEBSITE_ENABLED=true
VW_WEBSITE_USERNAME=you@example.com
VW_WEBSITE_PASSWORD=...
VW_WEBSITE_VIN=WVGZZZ...

A Škoda, read from the MyŠkoda Public API every 15 minutes, every 5 while charging:

Ev__Vehicles__0__Id=enyaq
Ev__Vehicles__0__Make=Škoda
Ev__Vehicles__0__BatteryCapacityKWh=77
SKODA_ENABLED=true
SKODA_VIN=TMB...

There is no key in .env for the Škoda. Create an API key for the car in the MySkoda app and paste it under Account on the web UI's Car page: it is checked with Škoda once, kept in data/skoda-api-key.json (owner-only — treat it as a password, since the same key can start and stop charging), and used without a restart. Keys expire; the dashboard warns a week ahead, and renewing is pasting a new key on the same page. The Škoda feed is built from Škoda's published spec and has not yet been verified on a car.

Home Assistant and the web UI are independent — run either, both, or neither. See Deployment for which deploy script (deploy.sh vs. deploy-controller-only.sh) decides which containers actually start; the Web__*/HomeAssistant__* variables above only control what the controller itself does once it's running.

The self-hosted web UI

The controller serves a dashboard, every control Home Assistant has, charging-session history and the forecast plan straight from itself — no broker and no Home Assistant required.

It is on by default, everywhere, and needs no configuration. A clone, the published image and the compose stack all serve it on port 8090 the moment they start; there is no environment-dependent default and nothing to enable. Web__Enabled=false turns it off, and off means nothing is listening: the process binds no socket at all, rather than serving an empty site.

This is the opposite of the Home Assistant integration, and for a concrete reason — the UI needs no broker, no credentials and no onboarding to be useful, so there is nothing an operator could usefully decide by being asked first.

The password is optional

There is no login out of the box. Anyone who can reach port 8090 gets the dashboard and every control on it. That suits a household LAN and is what makes "no configuration" true; it does not suit a network with guests on it, or a port that is reachable from outside.

Turning the login on is one setting — configure Web__PasswordHash and every page starts redirecting anonymous visitors to a login form. One shared password gates the whole UI; there are no per-user accounts. There is no second switch to remember, so a configured password cannot end up unenforced.

Generate the hash with the app itself — no configuration or listening socket involved, it just prints the hash and exits:

# from a clone
dotnet run --project src/Solax.Worker -- hash-password '<your password>'

# or with the image
docker run --rm ghcr.io/mpospisil/solax-controller:latest hash-password '<your password>'

Put the result in Web__PasswordHash (.env's WEB_PASSWORD_HASH in the compose deployment) — never in appsettings.json, same as any other secret here.

Web__RequireAuthentication overrides that inference and is rarely worth setting:

RequireAuthentication PasswordHash Result
unset (default) not set UI served openly; a warning is logged at every startup
unset (default) set login required — the normal way to protect the UI
true set login required; identical to the row above
true not set refuses to start — nobody could ever sign in
false either UI served openly even with a password configured; warning logged

The single refusal is the combination that cannot be honoured: a login demanded with nothing to check it against would leave the UI permanently unreachable, so the host stops and says so instead of looking like a broken page.

In the compose deployment these arrive from .env under their own names — WEB_ENABLED, WEB_PORT, WEB_PASSWORD_HASH, WEB_REQUIRE_AUTHENTICATION — and none of them has to be present. The port is published by docker-compose.yml itself; the COMPOSE_FILE line earlier releases required is obsolete, and should be deleted from any .env that still carries it. See Deployment.

The two directories it writes to

Both are relative to the app root — the worker's output directory when you run from source, /app inside the image, where they should be bind-mounted so they outlive the container.

Path Contents If you skip the mount
data/ → /app/data Charging-session SQLite database, and a vehicle sign-in's saved session or API key Session history is lost on the next docker rm. This is the one thing that cannot be regenerated.
logs/ → /app/logs Rolling log files Logs vanish with the container

Warning

Silent failure if you get the ownership wrong. If the host directory is not writable by uid 1654, the log sink fails silently — the container runs, docker logs looks healthy, and the log files simply never appear. The session store logs one error and then records nothing for the entire run. Always chown both directories.


Previous: Get started · Next: Docker

SolaX Local Controller

Advanced


Images: ghcr.io/mpospisil/solax-controller

Clone this wiki locally