Repository navigation
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.
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
| 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__UsernameHomeAssistant__Password
|
Broker credentials; must match the broker's password file. Only needed if HomeAssistant__Enabled is true. |
|
Solcast__ApiKeySolcast__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__EnabledBatteryHold__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.
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__IdEv__Vehicles__0__Name
|
Identity. The id is a slug; the name is what you see. |
Ev__Vehicles__0__MakeEv__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.
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 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.
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.
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
Runs entirely on your LAN · Full configuration reference in README.md · Pi preparation and backup in deploy/README.md
- Home
- Get started — build & run
- Configuration
- Solcast
- Weather
- Troubleshooting
Advanced
- Docker
- Platforms
- Deployment — Pi, two workflows
- MQTT
Images: ghcr.io/mpospisil/solax-controller