| 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 constexpr at the top of src/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
ConfigManager::logOtaTransition();
Factory Reset
ConfigManager::reset() clears the entire "config" NVS namespace and restores factory defaults:
ConfigManager::reset();
ConfigManager::save();
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
constexpr key name at the top of ConfigManager.cpp
- Add a
prefs.getTYPE(key, default) call in load()
- Add a
prefs.putTYPE(key, value) call in save()
- Add a default initialization in the struct definition (for
reset())
Example — adding a new heaterTimeout setting:
struct ControllerSettings {
int heaterTimeout = 30;
};
static constexpr const char *kSetHeaterTimeout = "set_heat_to";
settings_.heaterTimeout = prefs.getInt(kSetHeaterTimeout, 30);
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 |