Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 65 additions & 9 deletions docs/local-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,17 @@ You get the following response:
| `noxIndex` | Number | Senisirion NOx Index |
| `noxRaw` | Number | NOx raw value |
| `boot` | Number | Counts every measurement cycle. Low boot counts indicate restarts. |
| `bootCount` | Number | Same as boot property. Required for Home Assistant compatability. Will be depreciated. |
| `bootCount` | Number | Same as boot property. Required for Home Assistant compatability. (deprecated soon!) |
| `ledMode` | String | Current configuration of the LED mode |
| `firmware` | String | Current firmware version |
| `model` | String | Current model name |
| `monitorDisplayCompensatedValues` | Boolean | Switching Display of AirGradient ONE to Compensated / Non Compensated Values |

Compensated values apply correction algorithms to make the sensor values more accurate. Temperature and relative humidity correction is only applied on the outdoor monitor Open Air but the properties _compensated will still be send also for the indoor monitor AirGradient ONE.

#### Get Configuration Parameters (GET)
With the path "/config" you can get the current configuration.

"/config" path returns the current configuration of the monitor.

```json
{
"country": "TH",
Expand All @@ -99,28 +100,40 @@ With the path "/config" you can get the current configuration.
"displayBrightness": 100,
"offlineMode": false,
"model": "I-9PSL",
"monitorDisplayCompensatedValues": true
"monitorDisplayCompensatedValues": true,
"corrections": {
"pm02": {
"correctionAlgorithm": "epa_2021",
"slr": {}
}
}
}
}
```

#### Set Configuration Parameters (PUT)

Configuration parameters can be changed with a put request to the monitor, e.g.
Configuration parameters can be changed with a PUT request to the monitor, e.g.

Example to force CO2 calibration

```curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config ```
```bash
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
```

Example to set monitor to Celsius

```curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config ```
```bash
curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config
```

If you use command prompt on Windows, you need to escape the quotes:

``` -d "{\"param\":\"value\"}" ```

#### Avoiding Conflicts with Configuration on AirGradient Server
If the monitor is set up on the AirGradient dashboard, it will also receive configurations from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.

If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.

#### Configuration Parameters (GET/PUT)

Expand All @@ -142,4 +155,47 @@ If the monitor is set up on the AirGradient dashboard, it will also receive conf
| `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 12}` |
| `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` |
| `offlineMode` | Set monitor to run without WiFi. | Boolean | `false`: Disabled (default) <br> `true`: Enabled | `{"offlineMode": true}` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (From [3.1.9]()) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on [3.1.9]()) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `corrections` | Sets correction options to display and measurement values on local server response. | Object | _see corretions section_ | _see corretions section_ |



#### Corrections

The `corrections` object allows configuring PM2.5 correction algorithms and parameters. This affects both the display and local server response values.

Example correction configuration:

```json
{
"corrections": {
"pm02": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0,
"useEpa2021": false
}
}
}
}
```

| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
| EPA 2021 | `"epa_2021"` | Use EPA 2021 correction factors on top of raw value | No |
| PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes |
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |

**Notes**:

- Set `useEpa2021` to true if want to apply EPA 2021 correction factors on top of SLR correction value.
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)

**Example**:

```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":false}}}}'
```
171 changes: 165 additions & 6 deletions src/AgConfigure.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#include "AgConfigure.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#if ESP32
#include "FS.h"
#include "SPIFFS.h"
Expand All @@ -22,6 +21,18 @@ const char *LED_BAR_MODE_NAMES[] = {
[LedBarModeCO2] = "co2",
};

const char *PM_CORRECTION_ALGORITHM_NAMES[] = {
[Unknown] = "-", // This is only to pass "non-trivial designated initializers" error
[None] = "none",
[EPA_2021] = "epa_2021",
[SLR_PMS5003_20220802] = "slr_PMS5003_20220802",
[SLR_PMS5003_20220803] = "slr_PMS5003_20220803",
[SLR_PMS5003_20220824] = "slr_PMS5003_20220824",
[SLR_PMS5003_20231030] = "slr_PMS5003_20231030",
[SLR_PMS5003_20231218] = "slr_PMS5003_20231218",
[SLR_PMS5003_20240104] = "slr_PMS5003_20240104",
};

#define JSON_PROP_NAME(name) jprop_##name
#define JSON_PROP_DEF(name) const char *JSON_PROP_NAME(name) = #name

Expand All @@ -42,6 +53,7 @@ JSON_PROP_DEF(co2CalibrationRequested);
JSON_PROP_DEF(ledBarTestRequested);
JSON_PROP_DEF(offlineMode);
JSON_PROP_DEF(monitorDisplayCompensatedValues);
JSON_PROP_DEF(corrections);

#define jprop_model_default ""
#define jprop_country_default "TH"
Expand Down Expand Up @@ -87,6 +99,112 @@ String Configuration::getLedBarModeName(LedBarMode mode) {
return String("unknown");
}

PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
// Loop through all algorithm names in the PM_CORRECTION_ALGORITHM_NAMES array
// If the input string matches an algorithm name, return the corresponding enum value
// Else return Unknown

const size_t enumSize = SLR_PMS5003_20240104 + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = PMCorrectionAlgorithm::Unknown;

// Loop through enum values
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
result = static_cast<PMCorrectionAlgorithm>(enumVal);
}
}

return result;
}

bool Configuration::updatePmCorrection(JSONVar &json) {
if (!json.hasOwnProperty("corrections")) {
// TODO: need to response message?
Serial.println("corrections not found");
return false;
}

JSONVar corrections = json["corrections"];
if (!corrections.hasOwnProperty("pm02")) {
Serial.println("pm02 not found");
return false;
}

JSONVar pm02 = corrections["pm02"];
if (!pm02.hasOwnProperty("correctionAlgorithm")) {
Serial.println("correctionAlgorithm not found");
return false;
}

// TODO: Need to have data type check, with error message response if invalid

// Check algorithm
String algorithm = pm02["correctionAlgorithm"];
PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm);
if (algo == Unknown) {
logInfo("Unknown algorithm");
return false;
}
logInfo("Correction algorithm: " + algorithm);

// If algo is None or EPA_2021, no need to check slr
// But first check if pmCorrection different from algo
if (algo == None || algo == EPA_2021) {
if (pmCorrection.algorithm != algo) {
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections]["pm02"]["correctionAlgorithm"] = algorithm;
jconfig[jprop_corrections]["pm02"]["slr"] = JSON.parse("{}"); // Clear slr
// Update pmCorrection with new values
pmCorrection.algorithm = algo;
pmCorrection.changed = true;
logInfo("PM2.5 correction updated");
return true;
}

return false;
}

// Check if pm02 has slr object
if (!pm02.hasOwnProperty("slr")) {
Serial.println("slr not found");
return false;
}

JSONVar slr = pm02["slr"];

// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor") ||
!slr.hasOwnProperty("useEpa2021")) {
Serial.println("Missing required slr properties");
return false;
}

// arduino_json doesn't support float type, need to cast to double first
float intercept = (float)((double)slr["intercept"]);
float scalingFactor = (float)((double)slr["scalingFactor"]);

// Compare with current pmCorrection
if (pmCorrection.algorithm == algo && pmCorrection.intercept == intercept &&
pmCorrection.scalingFactor == scalingFactor &&
pmCorrection.useEPA == (bool)slr["useEpa2021"]) {
return false; // No changes needed
}

// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections] = corrections;

// Update pmCorrection with new values
pmCorrection.algorithm = algo;
pmCorrection.intercept = intercept;
pmCorrection.scalingFactor = scalingFactor;
pmCorrection.useEPA = (bool)slr["useEpa2021"];
pmCorrection.changed = true;

// Correction values were updated
logInfo("PM2.5 correction updated");
return true;
}

/**
* @brief Save configure to device storage (EEPROM)
*
Expand Down Expand Up @@ -171,6 +289,13 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default;

// PM2.5 correction
pmCorrection.algorithm = None;
pmCorrection.changed = false;
pmCorrection.intercept = -1;
pmCorrection.scalingFactor = -1;
pmCorrection.useEPA = false;

saveConfig();
}

Expand Down Expand Up @@ -660,20 +785,25 @@ bool Configuration::parse(String data, bool isLocal) {
if (curVer != newVer) {
logInfo("Detected new firmware version: " + newVer);
otaNewFirmwareVersion = newVer;
udpated = true;
updated = true;
} else {
otaNewFirmwareVersion = String("");
}
}
}

// Corrections
if (updatePmCorrection(root)) {
changed = true;
}

if (changed) {
udpated = true;
updated = true;
saveConfig();
printConfig();
} else {
if (ledBarTestRequested || co2CalibrationRequested) {
udpated = true;
updated = true;
}
}
return true;
Expand Down Expand Up @@ -860,8 +990,8 @@ String Configuration::getModel(void) {
}

bool Configuration::isUpdated(void) {
bool updated = this->udpated;
this->udpated = false;
bool updated = this->updated;
this->updated = false;
return updated;
}

Expand Down Expand Up @@ -1118,6 +1248,15 @@ void Configuration::toConfig(const char *buf) {
jprop_monitorDisplayCompensatedValues_default;
}


// Set default first before parsing local config
pmCorrection.algorithm = PMCorrectionAlgorithm::None;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 0;
pmCorrection.useEPA = false;
// Load correction from saved config
updatePmCorrection(jconfig);

if (changed) {
saveConfig();
}
Expand Down Expand Up @@ -1216,3 +1355,23 @@ String Configuration::newFirmwareVersion(void) {
otaNewFirmwareVersion = String("");
return newFw;
}

bool Configuration::isPMCorrectionChanged(void) {
bool changed = pmCorrection.changed;
pmCorrection.changed = false;
return changed;
}

/**
* @brief Check if PM correction is enabled
*
* @return true if PM correction algorithm is not None, otherwise false
*/
bool Configuration::isPMCorrectionEnabled(void) {
PMCorrection pmCorrection = getPMCorrection();
return pmCorrection.algorithm != PMCorrectionAlgorithm::None;
}

Configuration::PMCorrection Configuration::getPMCorrection(void) {
return pmCorrection;
}
Loading