Central configuration file /etc/energy-node/config.json
The central configuration file replaces the previously scattered environment variables: instead of seven different *.env files, there is now a single JSON document that holds all the values needed by the Python services, the Go dashboard, and the system components.
What the file is for
Previously the configuration was spread across seven files:
services/apsystems_ez1/apsystems.envservices/automation/automation.envservices/battery_soc/battery_soc.envservices/shelly/shelly_rpc.envservices/trucki/trucki.envservices/tuya_mqtt/tuya.envdashboard/energy_node_dashboard.env
This led to duplication (for example MQTT_HOST in all seven files), naming inconsistencies, and maintenance problems. The central file creates a single source of truth for 49 values. It is mandatory — there is no fallback to environment variables.
Location and override
The file is located at /etc/energy-node/config.json.
The directory and the file are owned by user root and group energynode:
- Directory:
root:energynodewith permissions0755 - File:
root:energynodewith permissions0664
For local tests or non-standard paths, the --config <path> parameter can be passed when starting a Python service or the dashboard, for example:
.venv/bin/python3 -m src.shelly.shelly_rpc_mqtt --config /home/test/my-config.json
The Go dashboard server accepts -config:
./cmd/dashboard/dashboard -config /path/to/config.json
All fields
Structure and mapping
The file follows this JSON structure:
{
"schema_version": 2,
"mqtt": { "host": "...", "port": 1883, "username": "...", "password_file": "..." },
"paths": { "devices_dir": "...", "data_dir": "...", "services_version_file": "..." },
"logging": { "level": "INFO" },
"services": { "apsystems": {...}, "battery_soc": {...}, "shelly": {...}, "trucki": {...}, "tuya": {...}, "automation": {...} },
"installed_services": { "apsystems": true, "automation": true, "battery_soc": true, "shelly": true, "tailscale": true, "trucki": true, "tuya": true },
"dashboard": { "bind_address": "...", "port": 8080, "client_id": "...", "device_identifier": "...", "log_level": "info", "sweep_interval_seconds": 300, "admin_username": "...", "admin_password_file": "...", "tls": {...}, "system_action_helper": "...", "mosquitto_bridge_target": "...", "node_device_id": "...", "node_device_name": "...", "node_poll_interval_s": 60, "node_diagnostic_poll_multiplier": 10 },
"tailscale": { "bin": "...", "status_timeout_s": 10 },
"tinytuya": { "probe_python": "...", "probe_script": "...", "probe_timeout_s": 30 }
}
Fields by purpose
| Field | Type | Read by | Meaning |
|---|---|---|---|
schema_version |
Integer | Python, Go | Version control of the file; must be 2 |
| MQTT broker (shared) | |||
mqtt.host |
String | Python, Go | IP or hostname of the broker |
mqtt.port |
Integer | Python, Go | Broker port (normally 1883) |
mqtt.username |
String | Python, Go | Username for broker authentication |
mqtt.password_file |
String | Python, Go | Path to the file containing the broker password |
| Paths (base directories) | |||
paths.devices_dir |
String | Python, Go | Base path for *_devices.json and other device configurations |
paths.data_dir |
String | Go | Path for settings.json, mqtt.json, bridge.json, layout.json (dashboard operating state) |
paths.services_version_file |
String | Go | Optional: path to the services/VERSION file deployed by scripts/deploy/deploy_src_to_remote.sh, shown on the settings page. Empty/missing = not configured. |
| Logging | |||
logging.level |
String | Python | Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL |
| Services (service-specific values) | |||
services.<name>.service_id |
String | Python | unique ID of the service (e.g. apsystems, shelly) |
services.<name>.poll_interval_s |
Integer | Python | poll interval of this service in seconds |
services.<name>.diagnostic_poll_multiplier |
Integer | Python | factor for diagnostic polls of this service |
services.<name>.http_timeout_s |
Integer | Python | HTTP timeout for HTTP-based services (Shelly, Trucki) |
| Installed services (optional) | |||
installed_services.<key> |
Boolean | Go | one boolean per service (apsystems, automation, battery_soc, shelly, tailscale, trucki, tuya); the dashboard hides that service’s tab/subpage when false. A missing block, or a missing key within it, both mean the service is on — the same default-on rule as everywhere else in this file. Written by the installer’s bootstrap step 65-dashboard-config.sh from the service selection; do not hand-edit it, the next install or re-deploy run overwrites it. |
| Dashboard (Go) | |||
dashboard.bind_address |
String | Go | bind address (normally 0.0.0.0 for both local and network access) |
dashboard.port |
Integer | Go | HTTP port of the dashboard (default: 8080) |
dashboard.client_id |
String | Go | unique MQTT client ID of the dashboard |
dashboard.device_identifier |
String | Go | device identifier for the dashboard’s MQTT communication |
dashboard.log_level |
String | Go | log level: debug, info, warn, error |
dashboard.sweep_interval_seconds |
Integer | Go | interval for periodic UI refreshes (default: 300) |
dashboard.admin_username |
String | Go | admin username for dashboard access |
dashboard.admin_password_file |
String | Go | path to the file containing the admin password |
dashboard.tls.cert_file |
String | Go | path to the TLS certificate file (empty = no TLS) |
dashboard.tls.key_file |
String | Go | path to the TLS key file (empty = no TLS) |
dashboard.system_action_helper |
String | Go | path to the helper program for system actions (e.g. reboot) |
dashboard.mosquitto_bridge_target |
String | Go | target path for bridge.conf on the target device |
dashboard.node_device_id |
String | Go | unique ID of the central node (must be energy_node); read by internal/nodeagent |
dashboard.node_device_name |
String | Go | human-readable name for display |
dashboard.node_poll_interval_s |
Integer | Go | poll interval of the node in seconds (default: 60) |
dashboard.node_diagnostic_poll_multiplier |
Integer | Go | factor for the diagnostic poll interval (default: 10) |
| Tailscale integration | |||
tailscale.bin |
String | Go | path to the tailscale binary |
tailscale.status_timeout_s |
Integer | Go | timeout for tailscale status queries |
| TinyTuya probe | |||
tinytuya.probe_python |
String | Python | path to the Python interpreter for the TinyTuya probe |
tinytuya.probe_script |
String | Python | path to the TinyTuya probe script |
tinytuya.probe_timeout_s |
Integer | Python | timeout for the probe in seconds |
Conventions
The file follows fixed conventions for paths and file names:
| Purpose | Convention | Example |
|---|---|---|
| Device configuration of a service | {paths.devices_dir}/{name}_devices.json |
{devices_dir}/shelly_devices.json |
| Schema for device configuration | {paths.devices_dir}/{name}_devices.schema.json |
{devices_dir}/shelly_devices.schema.json |
| Automation rules | {paths.devices_dir}/automation_rules.json |
{devices_dir}/automation_rules.json |
| Shelly presets | {paths.devices_dir}/shelly_presets.json |
{devices_dir}/shelly_presets.json |
| Dashboard operating state | {paths.data_dir}/{settings,mqtt,bridge,layout}.json |
{data_dir}/settings.json |
| MQTT password | {mqtt.password_file} |
/etc/energy-node/mqtt.pw |
| Admin password | {dashboard.admin_password_file} |
/etc/energy-node-dashboard/auth.pw |
The <name> key under services is consistent with the first part of the corresponding file names. A service must be defined under services in order to start — there is no implicit default name.
Service manifests delivery
Service manifests are delivered to the node via deploy: scripts/deploy/deploy_src_to_remote.sh
mirrors the complete set from services/*/manifest.json to
/etc/energy-node/manifests/<service_id>.json on every run (ensure_remote_manifests,
rsync --delete — orphaned manifests are removed). The set matches the
services block in config.json; energy_node_common validates this on the
node in both directions. The installer must replicate this same step when it
unpacks its bundle.
Credentials
Why passwords live in separate files
Passwords are not stored in config.json itself, only as file paths in the *_file fields:
mqtt.password_filefor the broker passworddashboard.admin_password_filefor the dashboard’s admin password
Reason for the ownership change: Previously systemd read the *.env files as root and passed the values to a process started as energynode. Without EnvironmentFile, the process must now open the password file itself — so the energynode group needs read permission.
Password files: permissions and content
MQTT password (/etc/energy-node/mqtt.pw):
- Owner:
root:energynode - Permissions:
0640 - Content: only the password itself, with no trailing whitespace
Admin password (/etc/energy-node-dashboard/auth.pw):
- Owner:
root:energynode - Permissions:
0640 - Content: only the password itself, with no trailing whitespace
Example of how to create one:
sudo install -o root -g energynode -m 0640 /dev/null /etc/energy-node/mqtt.pw
echo -n "my_mqtt_password" | sudo tee /etc/energy-node/mqtt.pw
Path allowlist when writing
When saving, the dashboard checks every *_file field against an allowlist. Only paths below the following directories are permitted:
/etc/energy-node//etc/energy-node-dashboard/
A missing or unreadable path is a startup error with a descriptive message.
Reload versus restart
The file contains two classes of values:
| Reloadable (no restart) | Restart required |
|---|---|
logging.level |
mqtt.* (host, port, authentication) |
services.*.poll_interval_s |
paths.* (files, directories) |
services.*.diagnostic_poll_multiplier |
dashboard.node_device_id, services.*.service_id |
services.*.http_timeout_s |
dashboard.port, .bind_address, .tls.* |
tinytuya.*, tailscale.* |
dashboard.admin_* |
The dividing line: A value belongs on the right-hand side if it determines the identity of a connection, a topic, or a listening port.
Reload sequence
When the dashboard changes a reloadable value:
- It writes
config.jsonatomically (to a temp file, thenrename) and stores a revision indata_dir/revisions/. - For each affected service, a message is published on
outstation/<service_id>/config/reload. - The service loads the new configuration; on an error it keeps the old values and reports
runtime_status: rejectedwith a reason. - If the change includes a field from the right-hand column, the dashboard shows “restart required” together with the list of units.
The restart remains an explicit action performed through the dashboard interface.
schema_version
The integer schema_version is currently set to 2. Every service (Python and Go) checks at startup that this version number matches the one it knows. A mismatch is a startup error:
error loading config.json: schema_version 3 found, but only 2 supported
A schema_version of 1 is migrated automatically by the dashboard on
startup. Version 2 dissolved the former top-level node block into flat
dashboard.node_* fields, so internal/appconfig transforms an old file
in memory as it loads it: moving the four surviving node.* fields,
dropping node.managed_bridges, normalising node_device_id to
energy_node, and — for a file predating the service-level device_id →
service_id rename — renaming that key in each services.* entry.
The dashboard then persists the migrated file through the privileged
system-action helper (apply-app-config), which backs the old file up to
/etc/energy-node/.config.json.bak before installing the new one — a
direct write fails because ensure_remote_config.sh creates
/etc/energy-node as mode 0755 and the service group cannot create files
there. If the helper is not reachable (not installed, no sudoers entry),
the dashboard still starts on the in-memory config, logs a warning, and
retries on the next start. The Python services do not migrate — they
still reject schema_version 1 and expect the dashboard to have upgraded
the file first; restart them once it has.
This concept ensures that a deployment in which the dashboard and the Python services come from different versions of the repository is noticed immediately — instead of surfacing as a subtle misconfiguration.
Error handling
| Error case | Behavior |
|---|---|
| File missing at the path | Startup error: names the expected path and the --config parameter |
| Invalid JSON (syntax) | Startup error: names line and column |
| Wrong field type (e.g. string instead of integer) | Startup error: names the JSON path and the expected type |
Wrong or mismatched schema_version |
Startup error: names the expected and found version |
| Referenced password file missing or unreadable | Startup error: names the file |
Required service missing under services |
Startup error: names the expected key |
| Runtime reload fails | The error is logged, the old values stay active, the dashboard shows runtime_status: rejected with a reason |
Dashboard cannot write config.json |
HTTP error with a descriptive reason, the file stays unchanged |
Principle: “Fail-closed” at startup — the service does not start without a valid configuration. At runtime, a faulty change is rejected and the previous state is kept.
Exceptions
The Mosquitto bridge stays in bridge.conf
The Mosquitto MQTT bridge is still configured through the file /etc/mosquitto/conf.d/bridge.conf in ini format, not through JSON. The reason: Mosquitto reads this file, not config.json.
config.json only contains the target path (dashboard.mosquitto_bridge_target) so that the dashboard knows where to write the configuration rendered from bridge.json. The bridge configuration itself is maintained in bridge.json or through the dashboard, not by any location in config.json.
Operating state stays in separate files
The files settings.json, mqtt.json, and bridge.json in data_dir are the operating state and do not belong to config.json. They contain settings that the user saves at runtime through the dashboard:
settings.json: general dashboard settingsmqtt.json: MQTT settings (broker alternatives). Two optional fields beyond the broker connection:metrics: mapmetric-name → bool. A missing key means the metric is published (default on); setting a key tofalsestops the dashboard’s node agent from publishing that individual system metric.simulation_active: bool, defaultfalse. Whentrue, the dashboard retained-broadcasts simulation mode to all bridges onoutstation/energy_node/settings/simulation_active/set.
bridge.json: Mosquitto bridge settings
This separation stays in place — config.json is only for settings that are set by the admin (locally or via deployment), not for runtime operating state.