diff --git a/docs/local-server.md b/docs/local-server.md index a65dd6e..3a54c98 100644 --- a/docs/local-server.md +++ b/docs/local-server.md @@ -162,7 +162,7 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the #### Corrections -The `corrections` object allows configuring PM2.5 correction algorithms and parameters. This affects both the display and local server response values. +The `corrections` object allows configuring PM2.5 correction algorithms and parameters locally. This affects both the display and local server response values. Example correction configuration: @@ -189,13 +189,28 @@ Example correction configuration: | 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**: +**NOTES**: -- Set `useEpa2021` to true if want to apply EPA 2021 correction factors on top of SLR correction value. +- Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false` - `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/) +- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below -**Example**: +**Examples**: + +- PMS5003_20231030 ```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}}}}' +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":true}}}}' +``` + +- PMS5003_20231218 + +```bash +curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0,03525,"useEpa2021":true}}}}' +``` + +- PMS5003_20240104 + +```bash +curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}' ``` \ No newline at end of file diff --git a/examples/OneOpenAir/OneOpenAir.ino b/examples/OneOpenAir/OneOpenAir.ino index 1700ebb..fce0feb 100644 --- a/examples/OneOpenAir/OneOpenAir.ino +++ b/examples/OneOpenAir/OneOpenAir.ino @@ -1216,6 +1216,6 @@ void setMeasurementMaxPeriod() { } int calculateMaxPeriod(int updateInterval) { - // 0.5 is 50% reduced interval for max period - return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval; + // 0.8 is 80% reduced interval for max period + return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.8)) / updateInterval; } diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp index 266886e..f090d3d 100644 --- a/src/AgConfigure.cpp +++ b/src/AgConfigure.cpp @@ -292,8 +292,8 @@ void Configuration::defaultConfig(void) { // PM2.5 correction pmCorrection.algorithm = None; pmCorrection.changed = false; - pmCorrection.intercept = -1; - pmCorrection.scalingFactor = -1; + pmCorrection.intercept = 0; + pmCorrection.scalingFactor = 1; pmCorrection.useEPA = false; saveConfig(); @@ -1369,7 +1369,12 @@ bool Configuration::isPMCorrectionChanged(void) { */ bool Configuration::isPMCorrectionEnabled(void) { PMCorrection pmCorrection = getPMCorrection(); - return pmCorrection.algorithm != PMCorrectionAlgorithm::None; + if (pmCorrection.algorithm == PMCorrectionAlgorithm::None || + pmCorrection.algorithm == PMCorrectionAlgorithm::Unknown) { + return false; + } + + return true; } Configuration::PMCorrection Configuration::getPMCorrection(void) { diff --git a/src/AgOledDisplay.cpp b/src/AgOledDisplay.cpp index 1dfbbba..d28ba40 100644 --- a/src/AgOledDisplay.cpp +++ b/src/AgOledDisplay.cpp @@ -12,7 +12,7 @@ */ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) { /** Temperature */ - float temp = value.getFloat(Measurements::Temperature); + float temp = value.getAverage(Measurements::Temperature); if (utils::isValidTemperature(temp)) { float t = 0.0f; if (config.isTemperatureUnitInF()) { @@ -44,7 +44,7 @@ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) { DISP()->drawUTF8(1, 10, buf); /** Show humidity */ - int rhum = (int)value.getFloat(Measurements::Humidity); + int rhum = round(value.getAverage(Measurements::Humidity)); if (utils::isValidHumidity(rhum)) { snprintf(buf, buf_size, "%d%%", rhum); } else { @@ -292,7 +292,7 @@ void OledDisplay::showDashboard(const char *status) { DISP()->drawUTF8(1, 27, "CO2"); DISP()->setFont(u8g2_font_t0_22b_tf); - int co2 = value.get(Measurements::CO2); + int co2 = round(value.getAverage(Measurements::CO2)); if (utils::isValidCO2(co2)) { sprintf(strBuf, "%d", co2); } else { @@ -313,11 +313,10 @@ void OledDisplay::showDashboard(const char *status) { DISP()->drawStr(55, 27, "PM2.5"); /** Draw PM2.5 value */ - - int pm25 = value.get(Measurements::PM25); + int pm25 = round(value.getAverage(Measurements::PM25)); if (utils::isValidPm(pm25)) { if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { - pm25 = (int)value.getCorrectedPM25(*ag, config); + pm25 = round(value.getCorrectedPM25(*ag, config, true)); } if (config.isPmStandardInUSAQI()) { sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25)); @@ -343,7 +342,7 @@ void OledDisplay::showDashboard(const char *status) { DISP()->drawStr(100, 27, "VOC:"); /** Draw tvocIndexvalue */ - int tvoc = value.get(Measurements::TVOC); + int tvoc = round(value.getAverage(Measurements::TVOC)); if (utils::isValidVOC(tvoc)) { sprintf(strBuf, "%d", tvoc); } else { @@ -352,7 +351,7 @@ void OledDisplay::showDashboard(const char *status) { DISP()->drawStr(100, 39, strBuf); /** Draw NOx label */ - int nox = value.get(Measurements::NOx); + int nox = round(value.getAverage(Measurements::NOx)); DISP()->drawStr(100, 53, "NOx:"); if (utils::isValidNOx(nox)) { sprintf(strBuf, "%d", nox); @@ -365,7 +364,7 @@ void OledDisplay::showDashboard(const char *status) { ag->display.clear(); /** Set CO2 */ - int co2 = value.get(Measurements::CO2); + int co2 = round(value.getAverage(Measurements::CO2)); if (utils::isValidCO2(co2)) { snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2); } else { @@ -376,9 +375,9 @@ void OledDisplay::showDashboard(const char *status) { ag->display.setText(strBuf); /** Set PM */ - int pm25 = value.get(Measurements::PM25); + int pm25 = round(value.getAverage(Measurements::PM25)); if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { - pm25 = (int)value.getCorrectedPM25(*ag, config); + pm25 = round(value.getCorrectedPM25(*ag, config, true)); } ag->display.setCursor(0, 12); @@ -390,7 +389,7 @@ void OledDisplay::showDashboard(const char *status) { ag->display.setText(strBuf); /** Set temperature and humidity */ - float temp = value.getFloat(Measurements::Temperature); + float temp = value.getAverage(Measurements::Temperature); if (utils::isValidTemperature(temp)) { if (config.isTemperatureUnitInF()) { snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp)); @@ -408,7 +407,7 @@ void OledDisplay::showDashboard(const char *status) { ag->display.setCursor(0, 24); ag->display.setText(strBuf); - int rhum = (int)value.getFloat(Measurements::Humidity); + int rhum = round(value.getAverage(Measurements::Humidity)); if (utils::isValidHumidity(rhum)) { snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum); } else { diff --git a/src/AgStateMachine.cpp b/src/AgStateMachine.cpp index 978eab4..63d8d63 100644 --- a/src/AgStateMachine.cpp +++ b/src/AgStateMachine.cpp @@ -86,7 +86,7 @@ bool StateMachine::sensorhandleLeds(void) { */ int StateMachine::co2handleLeds(void) { int totalUsed = ag->ledBar.getNumberOfLeds(); - int co2Value = value.get(Measurements::CO2); + int co2Value = round(value.getAverage(Measurements::CO2)); if (co2Value <= 700) { /** G; 1 */ ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1); @@ -171,9 +171,10 @@ int StateMachine::co2handleLeds(void) { */ int StateMachine::pm25handleLeds(void) { int totalUsed = ag->ledBar.getNumberOfLeds(); - int pm25Value = value.get(Measurements::PM25); + + int pm25Value = round(value.getAverage(Measurements::PM25)); if (config.hasSensorSHT && config.isPMCorrectionEnabled()) { - pm25Value = (int)value.getCorrectedPM25(*ag, config); + pm25Value = round(value.getCorrectedPM25(*ag, config, true)); } if (pm25Value <= 5) { diff --git a/src/AgValue.cpp b/src/AgValue.cpp index ca148ca..b0b281d 100644 --- a/src/AgValue.cpp +++ b/src/AgValue.cpp @@ -396,6 +396,52 @@ float Measurements::getFloat(MeasurementType type, int ch) { return temporary->listValues.back(); } +float Measurements::getAverage(MeasurementType type, int ch) { + // Sanity check to validate channel, assert if invalid + validateChannel(ch); + + // Follow array indexing just for get address of the value type + ch = ch - 1; + + // Define data point source. Data type doesn't matter because only to get the average value + FloatValue *temporary = nullptr; + Update update; + float measurementAverage; + switch (type) { + case CO2: + measurementAverage = _co2.update.avg; + break; + case TVOC: + measurementAverage = _tvoc.update.avg; + break; + case NOx: + measurementAverage = _nox.update.avg; + break; + case PM25: + measurementAverage = _pm_25[ch].update.avg; + break; + case Temperature: + measurementAverage = _temperature[ch].update.avg; + break; + case Humidity: + measurementAverage = _humidity[ch].update.avg; + break; + default: + // Invalidate, measurements type not handled + measurementAverage = -1000; + break; + }; + + // Sanity check if measurement type is not defined + if (measurementAverage == -1000) { + Serial.printf("ERROR! %s is not defined on get average value function\n", measurementTypeStr(type)); + delay(1000); + assert(0); + } + + return measurementAverage; +} + String Measurements::pms5003FirmwareVersion(int fwCode) { return pms5003FirmwareVersionBase("PMS5003x", fwCode); } @@ -485,11 +531,12 @@ void Measurements::validateChannel(int ch) { float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg, int ch) { float pm25; + float corrected; float humidity; float pm003Count; - int channel = ch - 1; // Array index if (useAvg) { // Directly call from the index + int channel = ch - 1; // Array index pm25 = _pm_25[channel].update.avg; humidity = _humidity[channel].update.avg; pm003Count = _pm_03_pc[channel].update.avg; @@ -500,19 +547,27 @@ float Measurements::getCorrectedPM25(AirGradient &ag, Configuration &config, boo } Configuration::PMCorrection pmCorrection = config.getPMCorrection(); - if (pmCorrection.algorithm == PMCorrectionAlgorithm::EPA_2021) { - // EPA correction directly applied - pm25 = ag.pms5003.compensate(pm25, humidity); - } else { - // SLR correction, this is assumes before calling this function, correction algorithm is not None - pm25 = ag.pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor, pmCorrection.intercept); + switch (pmCorrection.algorithm) { + case PMCorrectionAlgorithm::Unknown: + case PMCorrectionAlgorithm::None: + // If correction is Unknown, then default is None + corrected = pm25; + break; + case PMCorrectionAlgorithm::EPA_2021: + corrected = ag.pms5003.compensate(pm25, humidity); + break; + default: { + // All SLR correction using the same flow, hence default condition + corrected = ag.pms5003.slrCorrection(pm25, pm003Count, pmCorrection.scalingFactor, + pmCorrection.intercept); if (pmCorrection.useEPA) { // Add EPA compensation on top of SLR - pm25 = ag.pms5003.compensate(pm25, humidity); + corrected = ag.pms5003.compensate(pm25, humidity); } } + } - return pm25; + return corrected; } String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag, diff --git a/src/AgValue.h b/src/AgValue.h index 3b129b3..f6ae46d 100644 --- a/src/AgValue.h +++ b/src/AgValue.h @@ -114,9 +114,19 @@ public: */ float getFloat(MeasurementType type, int ch = 1); + /** + * @brief Get the target measurement average value + * + * @param type measurement type that will be retrieve + * @param ch target type value channel + * @return moving average value of target measurements type + */ + float getAverage(MeasurementType type, int ch = 1); /** * @brief Get the Corrected PM25 object based on the correction algorithm from configuration + * + * If correction is not enabled, then will return the raw value (either average or last value) * * @param ag AirGradient instance * @param config Configuration instance