You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: add config backup and restore with schema versioning
Add export/import of the full device configuration as a versioned JSON
file via the web UI. Backup downloads from GET /api/backup (secrets
optional via checkbox); restore uploads to POST /api/restore, applies
recognized settings leniently, and reboots.
- config_backup module serializes/deserializes config via ArduinoJson,
gated by CONFIG_SCHEMA_VERSION with a migrate() hook for future
non-additive schema changes
- Lenient restore: recognized fields applied (clamped via setters),
unknown ignored, absent left at current values; cross-version restore
proceeds with a mismatch warning
- Secrets (WiFi/MQTT passwords, HA token) omitted unless requested and
never erased by a no-secrets restore
- Backup and Restore card on the System settings page
- Docs: configuration guide and API reference
Export the full device configuration to a file, then restore that file onto a device that has been wiped or reset. The backup captures runtime settings stored in NVS: device name, WiFi, MQTT, Home Assistant, image sources and transforms, display settings, and system thresholds. Compile-time settings are not part of a backup.
657
+
658
+
### Where the Controls Live
659
+
660
+
The controls are on the System settings page, in the "Backup and Restore" card.
661
+
662
+
1. Navigate to `http://allskyesp32.lan:8080/system`
663
+
2. Find the "Backup and Restore" card
664
+
665
+
The card contains a "Download backup" button, an "Include passwords and tokens" checkbox, a file picker, and a "Restore and reboot" button.
666
+
667
+
### Download a Backup
668
+
669
+
1. Decide whether to include secrets. The "Include passwords and tokens" checkbox controls whether the WiFi password, MQTT password, and Home Assistant access token are written to the file.
670
+
2. Select "Download backup". The browser saves a `.json` file named after the device.
671
+
672
+
Warning: when "Include passwords and tokens" is checked, the secrets are stored in plaintext in the downloaded file. There is no encryption. Store the file in a protected location. When the checkbox is unchecked, the secret keys are omitted from the file, and other identifiers such as the WiFi SSID and MQTT username are still written.
673
+
674
+
### Restore and Reboot
675
+
676
+
1. Select the file picker and choose a `.json` backup file.
677
+
2. Select "Restore and reboot" and confirm the prompt.
678
+
3. The device applies the recognized settings, saves them to NVS, and reboots. After the reboot, the device runs with the restored settings.
679
+
680
+
A restore that fails to parse the file does not reboot the device.
681
+
682
+
### Version Behavior
683
+
684
+
The backup file carries a schema version. Restore is lenient and best-effort:
685
+
686
+
- Recognized fields are applied.
687
+
- Unknown fields are ignored.
688
+
- Fields absent from the file keep their current value on the device.
689
+
- A backup made on a different schema version still restores. The restore proceeds and reports a version mismatch warning.
690
+
- A no-secrets backup does not erase existing credentials. Secret fields are applied only when present and non-empty, so restoring a file saved without secrets leaves the current WiFi password, MQTT password, and Home Assistant token in place.
691
+
692
+
### Wipe Then Restore Sequence
693
+
694
+
After a factory reset the web UI is not reachable until WiFi is configured, because a factory reset clears WiFi credentials. Use this order:
695
+
696
+
1. Factory reset the device.
697
+
2. Complete WiFi setup through the captive portal so the device joins your network and the web UI becomes reachable.
698
+
3. Open the System settings page and restore the backup file.
699
+
4. The device reboots with the restored configuration.
700
+
701
+
If the backup includes secrets, the restored WiFi credentials take effect after the reboot. If the backup excludes secrets, the WiFi credentials entered during setup are retained.
702
+
703
+
---
704
+
653
705
## Serial Commands Reference
654
706
655
707
Connect via Serial Monitor at 9600 baud to access these commands:
Copy file name to clipboardExpand all lines: docs/developer/api_reference.md
+53Lines changed: 53 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -835,6 +835,59 @@ bool isRunning();
835
835
bool isOTAInProgress() const;
836
836
```
837
837
838
+
### REST API Endpoints
839
+
840
+
#### GET /api/backup
841
+
842
+
Returns the device configuration as a JSON file attachment.
843
+
844
+
Query parameters:
845
+
846
+
| Parameter | Values | Description |
847
+
|-----------|--------|-------------|
848
+
|`secrets`|`0` or `1`|`1` includes passwords and tokens (WiFi password, MQTT password, Home Assistant access token). `0` omits them. |
849
+
850
+
Response: the configuration document with `Content-Type: application/json` and a `Content-Disposition: attachment` header. The filename is derived from the device name.
Applies a backup file, saves the configuration, and reboots on success.
865
+
866
+
Request body: the raw backup file text (the JSON document returned by `GET /api/backup`). The body is read from the `plain` argument.
867
+
868
+
Behavior: the handler parses the body, applies every recognized field through the matching `ConfigStorage` setters, saves the configuration, then reboots the device. Unknown fields are ignored. Absent fields keep their current value. Secret fields are applied only when present and non-empty, so a no-secrets backup does not erase existing credentials.
869
+
870
+
Response JSON fields:
871
+
872
+
| Field | Type | Description |
873
+
|-------|------|-------------|
874
+
|`status`| string | Result of the restore. |
875
+
|`message`| string | Human-readable detail. |
876
+
|`applied`| number | Count of recognized fields applied. |
877
+
|`skipped`| number | Count of unknown fields ignored. |
878
+
|`fileVersion`| number | Schema version read from the file. Absent value is treated as 0. |
879
+
|`versionMismatch`| boolean |`true` when `fileVersion` differs from the firmware schema version. The restore still proceeds. |
880
+
881
+
Errors: an empty or unparseable body returns HTTP 400 and does not reboot. A successful restore reboots after sending the response.
882
+
883
+
Example:
884
+
885
+
```bash
886
+
# Restore a previously downloaded backup
887
+
curl -X POST "http://allskyesp32.lan:8080/api/restore" \
html += "<div class='card' style='margin-top:1.5rem'><h2>💾 Backup and Restore</h2>";
588
+
html += "<p style='color:#94a3b8;margin-bottom:1rem'>Download the full device configuration to a JSON file, or restore a previously saved file. Restoring replaces the current configuration and reboots the device.</p>";
589
+
html += "<div style='background:rgba(245,158,11,0.1);border:1px solid #f59e0b;border-radius:8px;padding:1rem;margin-bottom:1rem'>";
590
+
html += "<p style='color:#f59e0b;margin:0;font-size:0.9rem'><i class='fas fa-exclamation-triangle' style='margin-right:8px'></i>If <strong>Include passwords and tokens</strong> is checked, the backup file stores the WiFi password, MQTT password, and Home Assistant token in plaintext.</p>";
591
+
html += "</div>";
592
+
html += "<div class='form-group'><label><input type='checkbox' id='backupSecrets'> Include passwords and tokens</label></div>";
593
+
html += "<div style='display:flex;gap:0.75rem;flex-wrap:wrap;margin-bottom:1.5rem'>";
594
+
html += "<button type='button' class='btn btn-primary' onclick='downloadBackup()'>⬇️ Download backup</button></div>";
595
+
html += "<div class='form-group'><label for='restoreFile'>Restore from file</label>";
596
+
html += "<input type='file' id='restoreFile' accept='.json,application/json' class='form-control'></div>";
597
+
html += "<div style='display:flex;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.75rem'>";
598
+
html += "<button type='button' class='btn btn-primary' onclick='restoreBackup()'>♻️ Restore and reboot</button></div>";
599
+
html += "<div id='backupStatus' style='color:#94a3b8'></div>";
"if(res.body&&res.body.versionMismatch){msg+=' (backup schema version differs from this firmware; recognized fields were applied on a best-effort basis)';}"
617
+
"st.textContent=msg;"
618
+
"if(res.ok){st.textContent=msg+' Device is rebooting...';}"
0 commit comments