name: config-nvs description: "NVS/Preferences configuration storage for the pool-controller — ConfigManager API, key naming, NVS vs LittleFS trade-offs, OTA-safe persistence, and migration. 🇩🇪 Deutsche Trigger: Konfiguration, NVS, Preferences, ConfigManager, Konfiguration speichern, Einstellungen persistieren, OTA-sicher, Factory Reset, Werksreset." keywords: - configuration - konfiguration - nvs - preferences - configmanager - konfiguration speichern - einstellungen - settings - persistenz - persistence - ota-sicher - ota safe - factory reset - werksreset - wifi credentials - mqtt config - ntp config
NVS Configuration Storage — Pool Controller
The pool-controller stores all configuration data in ESP32
NVS (Non-Volatile Storage) via the Arduino Preferences library.
This replaces the earlier LittleFS JSON-based /config.json approach.
Why NVS over LittleFS/JSON?
| Feature | NVS / Preferences | LittleFS / JSON |
|---|---|---|
| OTA survival | ✅ Native — separate flash partition | ⚠️ Needs manual backup/restore |
| Wear leveling | ✅ Built into ESP-IDF NVS driver | ⚠️ File-based, no wear leveling |
| Atomic writes | ✅ Power-fail safe | ❌ Can corrupt on power loss |
| Boot speed | ✅ Instant — no mount/parse | ❌ Needs FS mount + JSON deserialize |
| API | putString(key, val) / getString(key, default) |
Open file → parse JSON → access field |
| Flash overhead | ~0 extra (uses existing partition) | Requires LittleFS partition |
Architecture
ConfigManager (static class)
│
├── begin() → opens NVS namespace "config", calls load()
├── load() → reads all keys from NVS into memory structs
├── save() → writes all memory structs to NVS atomically
└── reset() → clears entire NVS namespace, resets to defaults
│
├── getWiFi() → WiFiConfig& { ssid, password }
├── getMqtt() → MqttConfig& { host, port, username, password, useTls }
├── getNtp() → NtpConfig& { server, timezone }
├── getSettings() → ControllerSettings& { loopInterval, tempMaxPool, … }
│
├── setAdminPassword() / verifyAdminPassword()
└── isConfigured() / setConfigured()
⚠️ Save pattern: After modifying any field via the getters above, call
ConfigManager::save()explicitly:ConfigManager::getSettings().opMode = "timer"; ConfigManager::save();
NVS Key Map
All config values live in a single NVS namespace "config":
| Key | Type | Struct Field | Default |
|---|---|---|---|
wifi_ssid |
String | WiFiConfig::ssid |
"" |
wifi_pass |
String | WiFiConfig::password |
"" |
mqtt_host |
String | MqttConfig::host |
"" |
mqtt_port |
uint32_t | MqttConfig::port |
1883 |
mqtt_user |
String | MqttConfig::username |
"" |
mqtt_pass |
String | MqttConfig::password |
"" |
mqtt_tls |
bool | MqttConfig::useTls |
false |
ntp_server |
String | NtpConfig::server |
"pool.ntp.org" |
ntp_tz |
int32_t | NtpConfig::timezone |
0 |
set_interval |
int32_t | ControllerSettings::loopInterval |
10 |
set_maxpool |
double | ControllerSettings::tempMaxPool |
28.5 |
set_minsolar |
double | ControllerSettings::tempMinSolar |
55.0 |
set_hyst |
double | ControllerSettings::tempHysteresis |
1.0 |
set_opmode |
String | ControllerSettings::opMode |
"auto" |
set_tzidx |
int16_t | ControllerSettings::timezoneIndex |
0 |
set_green |
uint8_t | ControllerSettings::timeLossGreenHours |
1 |
set_red |
uint8_t | ControllerSettings::timeLossRedHours |
24 |
adm_pass |
String | adminPasswordHash_ |
SHA256 of "admin" |
cfg_configured |
bool | configured_ |
false |
🔍 Code Search: the exact key names are defined as
constexprat the top ofsrc/ConfigManager.cpp.
OTA Safety
NVS data survives all firmware update methods automatically:
- OTA (Over-the-Air): ✅ NVS partition is never touched by OTA
- USB serial flash: ✅ Safe as long as Erase All Flash Before Sketch Upload is Disabled (default in PlatformIO)
- PlatformIO
pio run --target erase: ❌ Will wipe NVS — use with caution
The old OTA backup/restore logic (backupConfig() / restoreConfig())
and the /config.json.ota file have been removed.
They are no longer needed.
Boot Version Tracking
ConfigManager::logOtaTransition() uses a separate NVS namespace
"ota-version" with a single key "fw_version" to detect version
transitions across OTA updates. It logs:
- First boot ever — no previous version found
- OTA update detected — version changed (logs old → new)
- Normal boot — same version as before
// Called once in PoolControllerContext::setup() after ConfigManager::begin()
ConfigManager::logOtaTransition();
Factory Reset
ConfigManager::reset() clears the entire "config" NVS namespace and restores factory defaults:
ConfigManager::reset();
ConfigManager::save(); // persist the cleared state
The WebPortal::apiFactoryReset() calls this and reboots the ESP32.
Adding a New Config Field
- Add the field to the appropriate struct in
ConfigManager.hpp - Add a
constexprkey name at the top ofConfigManager.cpp - Add a
prefs.getTYPE(key, default)call inload() - Add a
prefs.putTYPE(key, value)call insave() - Add a default initialization in the struct definition (for
reset())
Example — adding a new heaterTimeout setting:
// In ConfigManager.hpp:
struct ControllerSettings {
// ... existing fields ...
int heaterTimeout = 30; // minutes
};
// In ConfigManager.cpp — key:
static constexpr const char *kSetHeaterTimeout = "set_heat_to";
// In ConfigManager::load():
settings_.heaterTimeout = prefs.getInt(kSetHeaterTimeout, 30);
// In ConfigManager::save():
prefs.putInt(kSetHeaterTimeout, settings_.heaterTimeout);
Related Files
| File | Role |
|---|---|
src/ConfigManager.hpp |
Struct definitions + public API |
src/ConfigManager.cpp |
NVS read/write + key map |
src/PoolController.cpp |
Calls ConfigManager::begin() + logOtaTransition() |
src/WebPortal.cpp |
REST API reads/saves via ConfigManager |
src/MqttPublisher.cpp |
MQTT handlers apply config changes via ConfigManager |
src/NetworkManager.cpp |
Reads WiFi + MQTT config on reconnect |
src/WpsProvisioner.cpp |
Writes WiFi credentials via ConfigManager |