diff --git a/CHANGELOG.md b/CHANGELOG.md index f75f2a0f0..0337f1a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - DALI inverted signal configuration using GPIO DALI RX_i/TX_i - Support for Shelly DALI Dimmer Gen3 (See tips and template in file xdrv_75_dali.ino) - HASPmota `haspmota.get_pages()` to get the sorted list of pages (#22358) +- Support for US AQI and EPA AQI in PMS5003x sensors (#22294) ### Breaking Changed diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e67ab7d3b..5562fe537 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -122,6 +122,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm - DALI command `DaliTarget` to set light control broadcast, group number or gear number - DALI inverted signal configuration using GPIO DALI RX_i/TX_i - Support for Shelly DALI Dimmer Gen3 (See tips and template in file xdrv_75_dali.ino) +- Support for US AQI and EPA AQI in PMS5003x sensors [#22294](https://github.com/arendst/Tasmota/issues/22294) - Mitsubishi Electric HVAC Operation time for MiElHVAC [#22334](https://github.com/arendst/Tasmota/issues/22334) - Mitsubishi Electric HVAC Outdoor Temperature for MiElHVAC [#22345](https://github.com/arendst/Tasmota/issues/22345) - Mitsubishi Electric HVAC Compressor Frequency for MiElHVAC [#22347](https://github.com/arendst/Tasmota/issues/22347) diff --git a/tasmota/include/i18n.h b/tasmota/include/i18n.h index 5555ec9d9..b243f01e4 100644 --- a/tasmota/include/i18n.h +++ b/tasmota/include/i18n.h @@ -1006,7 +1006,9 @@ const char HTTP_SNS_STANDARD_CONCENTRATION[] PROGMEM = "{s}%s " D_STANDAR const char HTTP_SNS_ENVIRONMENTAL_CONCENTRATION[] PROGMEM = "{s}%s " D_ENVIRONMENTAL_CONCENTRATION " %s " D_UNIT_MICROMETER "{m}%d " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"; const char HTTP_SNS_F_ENVIRONMENTAL_CONCENTRATION[] PROGMEM = "{s}%s " D_ENVIRONMENTAL_CONCENTRATION " %s " D_UNIT_MICROMETER "{m}%1_f " D_UNIT_MICROGRAM_PER_CUBIC_METER "{e}"; const char HTTP_SNS_PARTICALS_BEYOND[] PROGMEM = "{s}%s " D_PARTICALS_BEYOND " %s " D_UNIT_MICROMETER "{m}%d " D_UNIT_PARTS_PER_DECILITER "{e}"; -const char HTTP_SNS_AVG_RAD_DOSE[] PROGMEM = "{s}%s " D_AVG_RAD_DOSE " %s " D_UNIT_MINUTE "{m}%d.%02d " D_UNIT_US_H "{e}"; +const char HTTP_SNS_AVG_RAD_DOSE[] PROGMEM = "{s}%s " D_AVG_RAD_DOSE " %s " D_UNIT_MINUTE "{m}%d.%02d " D_UNIT_US_H "{e}"; +const char HTTP_SNS_US_AQI[] PROGMEM = "{s}%s US AQI" "{m}%d" "{e}"; +const char HTTP_SNS_US_EPA_AQI[] PROGMEM = "{s}%s US EPA AQI" "{m}%d" "{e}"; const char HTTP_SNS_VOLTAGE[] PROGMEM = "{s}" D_VOLTAGE "{m}%s " D_UNIT_VOLT "{e}"; const char HTTP_SNS_CURRENT[] PROGMEM = "{s}" D_CURRENT "{m}%s " D_UNIT_AMPERE "{e}"; diff --git a/tasmota/tasmota_xsns_sensor/xsns_18_pms5003.ino b/tasmota/tasmota_xsns_sensor/xsns_18_pms5003.ino index a5f9e0ced..1da6a0058 100644 --- a/tasmota/tasmota_xsns_sensor/xsns_18_pms5003.ino +++ b/tasmota/tasmota_xsns_sensor/xsns_18_pms5003.ino @@ -353,6 +353,59 @@ void PmsInit(void) { } } +// This gives more accurate data for forest fire smoke. PurpleAir gives you this conversion option labeled "US EPA" +// https://cfpub.epa.gov/si/si_public_record_report.cfm?dirEntryId=353088&Lab=CEMM +/* +Copy-paste from the PDF Slide 26 +y={0 ≤ x <30: 0.524*x - 0.0862*RH + 5.75} +y={30≤ x <50: (0.786*(x/20 - 3/2) + 0.524*(1 - (x/20 - 3/2)))*x -0.0862*RH + 5.75} +y={50 ≤ x <210: 0.786*x - 0.0862*RH + 5.75} +y={210 ≤ x <260: (0.69*(x/50 – 21/5) + 0.786*(1 - (x/50 – 21/5)))*x - 0.0862*RH*(1 - (x/50 – 21/5)) + 2.966*(x/50 – 21/5) + 5.75*(1 - (x/50 – 21/5)) + 8.84*(10^{-4})*x^{2}*(x/50 – 21/5)} +y={260 ≤ x: 2.966 + 0.69*x + 8.84*10^{-4}*x^2} + +y= corrected PM2.5 µg/m3 +x= PM2.5 cf_atm (lower) +RH= Relative humidity as measured by the PurpleAir +*/ +int usaEpaStandardPm2d5Adjustment(int pm25_standard, int relative_humidity) +{ + // Rename to use the same variables from the paper + float x = pm25_standard; + float RH = relative_humidity; + if (x<30) { + return 0.524f * x - 0.0862f * RH + 5.75f; + } else if(x<50) { + return (0.786f * (x/20.0f - 3.0f/2.0f) + 0.524f * (1.0f - (x/20.0f - 3.0f/2.0f))) * x - 0.0862f * RH + 5.75f; + } else if(x<210) { + return 0.786f * x - 0.0862f * RH + 5.75f; + } else if(x<260) { + return (0.69f * (x/50.0f - 21.0f/5.0f) + 0.786f * (1.0f - (x/50.0f - 21.0f/5.0f))) * x - 0.0862f * RH * (1.0f - (x/50.0f - 21.0f/5.0f)) + 2.966f * (x/50.0f - 21.0f/5.0f) + 5.75f * (1.0f - (x/50.0f - 21.0f/5.0f)) + 8.84f * FastPrecisePowf(10.0f, -4.0f) * FastPrecisePowf(x,2.0f) * (x/50.0f - 21.0f/5.0f); + } else { + return 2.966f + 0.69f * x + 8.84f * FastPrecisePowf(10.0f, -4.0f) * FastPrecisePowf(x, 2.0f); + } +} + +// Compute US AQI using the 2024+ table +// https://forum.airnowtech.org/t/the-aqi-equation-2024-valid-beginning-may-6th-2024/453 +int compute_us_aqi(int pm25_standard) +{ + if (pm25_standard <= 9) { + return map_double(pm25_standard, 0, 9, 0, 50); + } else if (pm25_standard <= 35) { + return map_double(pm25_standard, 9.1f, 35.4f, 51, 100); + } else if (pm25_standard <= 55) { + return map_double(pm25_standard, 35.5f, 55.4f, 101, 150); + } else if (pm25_standard <= 125) { + return map_double(pm25_standard, 55.5f, 125.4f, 151, 200); + } else if (pm25_standard <= 225) { + return map_double(pm25_standard, 125.5f, 225.4f, 201, 300); + } else if (pm25_standard <= 325) { + return map_double(pm25_standard, 225.5f, 325.4f, 301, 500); + } else { + return 500; + } +} + void PmsShow(bool json) { if (Pms.valid) { char types[10]; @@ -370,20 +423,37 @@ void PmsShow(bool json) { #ifdef PMS_MODEL_PMS5003T float temperature = ConvertTemp(pms_data.temperature10x/10.0); float humidity = ConvertHumidity(pms_data.humidity10x/10.0); + int epa_us_aqi; + // When in Fahrenheit include US AQI + if (Settings->flag.temperature_conversion) { // Fahrenheit - US, Liberia, Cayman Islands + epa_us_aqi = compute_us_aqi(usaEpaStandardPm2d5Adjustment(pms_data.pm25_standard, humidity)); + } #endif // PMS_MODEL_PMS5003T + int us_aqi; + // Use US AQI for Fahrenheit, EAQI (European Air Quality Index) for Celsius + if (Settings->flag.temperature_conversion) { // Fahrenheit - US, Liberia, Cayman Islands + us_aqi = compute_us_aqi(pms_data.pm25_standard); + } if (json) { ResponseAppend_P(PSTR(",\"%s\":{\"CF1\":%d,\"CF2.5\":%d,\"CF10\":%d,\"PM1\":%d,\"PM2.5\":%d,\"PM10\":%d"), types, pms_data.pm10_standard, pms_data.pm25_standard, pms_data.pm100_standard, pms_data.pm10_env, pms_data.pm25_env, pms_data.pm100_env); + if (Settings->flag.temperature_conversion) { // Fahrenheit - US, Liberia, Cayman Islands + ResponseAppend_P(PSTR(",\"US_AQI\":%d"), us_aqi); + } #if !(defined(PMS_MODEL_PMS3003) || defined(PMS_MODEL_ZH03X)) - ResponseAppend_P(PSTR(",\"PB0.3\":%d,\"PB0.5\":%d,\"PB1\":%d,\"PB2.5\":%d,"), + ResponseAppend_P(PSTR(",\"PB0.3\":%d,\"PB0.5\":%d,\"PB1\":%d,\"PB2.5\":%d"), pms_data.particles_03um, pms_data.particles_05um, pms_data.particles_10um, pms_data.particles_25um); #ifdef PMS_MODEL_PMS5003T + ResponseAppend_P(PSTR(",")); ResponseAppendTHD(temperature, humidity); + if (Settings->flag.temperature_conversion) { // Fahrenheit - US, Liberia, Cayman Islands + ResponseAppend_P(PSTR(",\"US_EPA_AQI\":%d"), epa_us_aqi); + } #else - ResponseAppend_P(PSTR("\"PB5\":%d,\"PB10\":%d"), + ResponseAppend_P(PSTR(",\"PB5\":%d,\"PB10\":%d"), pms_data.particles_50um, pms_data.particles_100um); #endif // PMS_MODEL_PMS5003T #endif // No PMS_MODEL_PMS3003 @@ -409,8 +479,14 @@ void PmsShow(bool json) { WSContentSend_PD(HTTP_SNS_PARTICALS_BEYOND, types, "0.5", pms_data.particles_05um); WSContentSend_PD(HTTP_SNS_PARTICALS_BEYOND, types, "1", pms_data.particles_10um); WSContentSend_PD(HTTP_SNS_PARTICALS_BEYOND, types, "2.5", pms_data.particles_25um); + if (Settings->flag.temperature_conversion) { // Fahrenheit - US, Liberia, Cayman Islands + WSContentSend_PD(HTTP_SNS_US_AQI, types, us_aqi); + } #ifdef PMS_MODEL_PMS5003T WSContentSend_THD(types, temperature, humidity); + if (Settings->flag.temperature_conversion) { // Fahrenheit - US, Liberia, Cayman Islands + WSContentSend_PD(HTTP_SNS_US_EPA_AQI, types, epa_us_aqi); + } #else WSContentSend_PD(HTTP_SNS_PARTICALS_BEYOND, types, "5", pms_data.particles_50um); WSContentSend_PD(HTTP_SNS_PARTICALS_BEYOND, types, "10", pms_data.particles_100um); @@ -455,4 +531,4 @@ bool Xsns18(uint32_t function) return result; } -#endif // USE_PMS5003 +#endif // USE_PMS5003 \ No newline at end of file