mirror of https://github.com/arendst/Tasmota.git
309 lines
10 KiB
C++
309 lines
10 KiB
C++
/*
|
|
xsns_103_sen5x.ino - SEN5X gas and air quality sensor support for Tasmota
|
|
|
|
Copyright (C) 2022 Tyeth Gundry
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifdef USE_I2C
|
|
#ifdef USE_SEN5X
|
|
/*********************************************************************************************\
|
|
* SEN5X - Gas (VOC - Volatile Organic Compounds / NOx - Nitrous Oxides) and Particulates (PM)
|
|
*
|
|
* Source: Sensirion SEN5X Driver + Example, and Tasmota Driver 98 by Jean-Pierre Deschamps
|
|
* Adaption for TASMOTA: Tyeth Gundry
|
|
*
|
|
* I2C Address: 0x69
|
|
\*********************************************************************************************/
|
|
|
|
#define XSNS_103 103
|
|
#define XI2C_76 76 // See I2CDEVICES.md
|
|
|
|
#define SEN5X_ADDRESS 0x69
|
|
|
|
#include <SensirionI2CSen5x.h>
|
|
#include <Wire.h>
|
|
SensirionI2CSen5x *sen5x = nullptr;
|
|
|
|
struct SEN5XDATA_s {
|
|
bool sen5x_ready;
|
|
float massConcentrationPm1p0;
|
|
float massConcentrationPm2p5;
|
|
float massConcentrationPm4p0;
|
|
float massConcentrationPm10p0;
|
|
float ambientHumidity;
|
|
float ambientTemperature;
|
|
float vocIndex;
|
|
float noxIndex;
|
|
} *SEN5XDATA = nullptr;
|
|
/********************************************************************************************/
|
|
|
|
void sen5x_Init(void)
|
|
{
|
|
if(!TasmotaGlobal.i2c_enabled){
|
|
DEBUG_SENSOR_LOG(PSTR("I2C Not enabled, so not loading SEN5X driver."));
|
|
return;
|
|
}
|
|
int usingI2cBus = 0;
|
|
#ifdef ESP32
|
|
if (!I2cSetDevice(SEN5X_ADDRESS, 0))
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("Sensirion SEN5X not found, i2c bus 0"));
|
|
if (TasmotaGlobal.i2c_enabled_2 ){
|
|
|
|
if(!I2cSetDevice(SEN5X_ADDRESS, 1)){
|
|
DEBUG_SENSOR_LOG(PSTR("Sensirion SEN5X not found, i2c bus 1"));
|
|
return;
|
|
}
|
|
usingI2cBus = 1;
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
#else
|
|
if (!I2cSetDevice(SEN5X_ADDRESS))
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("Sensirion SEN5X not found, i2c bus 0"));
|
|
return;
|
|
}
|
|
#endif
|
|
if (SEN5XDATA == nullptr)
|
|
SEN5XDATA = (SEN5XDATA_s *)calloc(1, sizeof(struct SEN5XDATA_s));
|
|
SEN5XDATA->sen5x_ready = false;
|
|
if(sen5x == nullptr) sen5x = new SensirionI2CSen5x();
|
|
if(usingI2cBus==1){
|
|
#ifdef ESP32
|
|
sen5x->begin(Wire1);
|
|
#else
|
|
sen5x->begin(Wire);
|
|
#endif
|
|
}
|
|
else {
|
|
sen5x->begin(Wire);
|
|
}
|
|
int error_stop = sen5x->deviceReset();
|
|
if (error_stop != 0)
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("Sensirion SEN5X failed to reset device (I2C Bus %d)"), usingI2cBus);
|
|
return;
|
|
}
|
|
// Wait 1 second for sensors to start recording + 100ms for reset command
|
|
delay(1100);
|
|
int error_start = sen5x->startMeasurement();
|
|
if (error_start != 0)
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("Sensirion SEN5X failed to start measurement (I2C Bus %d)"), usingI2cBus);
|
|
return;
|
|
}
|
|
SEN5XDATA->sen5x_ready = true;
|
|
I2cSetActiveFound(SEN5X_ADDRESS, "SEN5X", usingI2cBus);
|
|
DEBUG_SENSOR_LOG(PSTR("Sensirion SEN5X found, i2c bus %d"), usingI2cBus);
|
|
}
|
|
|
|
// #define POW_FUNC pow
|
|
#define POW_FUNC FastPrecisePow
|
|
|
|
float sen5x_AbsoluteHumidity(float temperature, float humidity)
|
|
{
|
|
// taken from https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
|
|
// precision is about 0.1°C in range -30 to 35°C
|
|
// August-Roche-Magnus 6.1094 exp(17.625 x T)/(T + 243.04)
|
|
// Buck (1981) 6.1121 exp(17.502 x T)/(T + 240.97)
|
|
// reference https://www.eas.ualberta.ca/jdwilson/EAS372_13/Vomel_CIRES_satvpformulae.html
|
|
float temp = NAN;
|
|
const float mw = 18.01534f; // molar mass of water g/mol
|
|
const float r = 8.31447215f; // Universal gas constant J/mol/K
|
|
|
|
if (isnan(temperature) || isnan(humidity))
|
|
{
|
|
return NAN;
|
|
}
|
|
|
|
temp = POW_FUNC(2.718281828f, (17.67f * temperature) / (temperature + 243.5f));
|
|
|
|
// return (6.112 * temp * humidity * 2.1674) / (273.15 + temperature); //simplified version
|
|
return (6.112f * temp * humidity * mw) / ((273.15f + temperature) * r); // long version
|
|
}
|
|
|
|
#define SAVE_PERIOD 30
|
|
|
|
void SEN5XUpdate(void) // Perform every second to ensure proper operation of the baseline compensation algorithm
|
|
{
|
|
uint16_t error;
|
|
char errorMessage[256];
|
|
DEBUG_SENSOR_LOG(PSTR("Running readMeasuredValues for SEN5X..."));
|
|
|
|
error = sen5x->readMeasuredValues(
|
|
SEN5XDATA->massConcentrationPm1p0, SEN5XDATA->massConcentrationPm2p5, SEN5XDATA->massConcentrationPm4p0,
|
|
SEN5XDATA->massConcentrationPm10p0, SEN5XDATA->ambientHumidity, SEN5XDATA->ambientTemperature, SEN5XDATA->vocIndex,
|
|
SEN5XDATA->noxIndex);
|
|
|
|
if (error)
|
|
{
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR("Failed to retrieve SEN5X readings."));
|
|
#ifdef DEBUG_TASMOTA_SENSOR
|
|
DEBUG_SENSOR_LOG(PSTR("Error trying to execute readMeasuredValues(): \n"));
|
|
errorToString(error, errorMessage, 256);
|
|
DEBUG_SENSOR_LOG(errorMessage);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
#ifdef DEBUG_TASMOTA_SENSOR
|
|
DEBUG_SENSOR_LOG(PSTR("SEN5x readings:-"));
|
|
DEBUG_SENSOR_LOG(PSTR("MassConcentrationPm1p0: %1_f\n"), &SEN5XDATA->massConcentrationPm1p0);
|
|
DEBUG_SENSOR_LOG(PSTR("MassConcentrationPm2p5: %1_f\n"), &SEN5XDATA->massConcentrationPm2p5);
|
|
DEBUG_SENSOR_LOG(PSTR("MassConcentrationPm4p0: %1_f\n"), &SEN5XDATA->massConcentrationPm4p0);
|
|
DEBUG_SENSOR_LOG(PSTR("MassConcentrationPm10p0: %1_f\n"), &SEN5XDATA->massConcentrationPm10p0);
|
|
if (isnan(SEN5XDATA->ambientHumidity))
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("AmbientHumidity: n/a\n"));
|
|
}
|
|
else
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("AmbientHumidity: %*_f\n"), 2, &SEN5XDATA->ambientHumidity);
|
|
}
|
|
|
|
if (isnan(SEN5XDATA->ambientTemperature))
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("AmbientTemperature: n/a\n"));
|
|
}
|
|
else
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("AmbientTemperature: %*_f\n"), 2, &SEN5XDATA->ambientTemperature);
|
|
}
|
|
|
|
if (isnan(SEN5XDATA->vocIndex))
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("VocIndex: n/a\n"));
|
|
}
|
|
else
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("VocIndex: %*_f\n"), 0, &SEN5XDATA->vocIndex);
|
|
}
|
|
|
|
if (isnan(SEN5XDATA->noxIndex))
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("NoxIndex: n/a\n"));
|
|
}
|
|
else
|
|
{
|
|
DEBUG_SENSOR_LOG(PSTR("NoxIndex: %*_f\n"), 0, &SEN5XDATA->noxIndex);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#ifdef USE_WEBSERVER
|
|
const char HTTP_SNS_SEN5X_UNITS[] PROGMEM = "{s}SEN5X %s{m}%*_f %s{e}";
|
|
const char HTTP_SNS_SEN5X_UNITLESS[] PROGMEM = "{s}SEN5X %s{m}%*_f{e}";
|
|
// {s} = <tr><th>, {m} = </th><td>, {e} = </td></tr>
|
|
const char HTTP_SNS_AHUMSEN5X[] PROGMEM = "{s}SEN5X Abs Humidity{m}%s g/m³{e}";
|
|
#endif
|
|
|
|
#define D_JSON_AHUM "aHumidity"
|
|
|
|
void SEN5XShow(bool json)
|
|
{
|
|
float convertedTemp = 0;
|
|
float convertedHum = 0;
|
|
if (SEN5XDATA->sen5x_ready)
|
|
{
|
|
char sen5x_abs_hum[13];
|
|
bool ahum_available = !isnan(SEN5XDATA->ambientTemperature) &&
|
|
!isnan(SEN5XDATA->ambientHumidity) &&
|
|
SEN5XDATA->ambientHumidity > 0;
|
|
if (ahum_available)
|
|
{
|
|
convertedTemp = ConvertTempToFahrenheit(SEN5XDATA->ambientTemperature);
|
|
convertedHum = ConvertHumidity(SEN5XDATA->ambientHumidity);
|
|
dtostrfd(sen5x_AbsoluteHumidity(convertedTemp,convertedHum), 4, sen5x_abs_hum);
|
|
}
|
|
if (json)
|
|
{
|
|
ResponseAppend_P(PSTR(",\"SEN5X\":{"));
|
|
ResponseAppend_P(PSTR("\"" D_ENVIRONMENTAL_CONCENTRATION "1\":%1_f,"), &SEN5XDATA->massConcentrationPm1p0);
|
|
ResponseAppend_P(PSTR("\"" D_ENVIRONMENTAL_CONCENTRATION "2.5\":%1_f,"), &SEN5XDATA->massConcentrationPm2p5);
|
|
ResponseAppend_P(PSTR("\"" D_ENVIRONMENTAL_CONCENTRATION "4\":%1_f,"), &SEN5XDATA->massConcentrationPm4p0);
|
|
ResponseAppend_P(PSTR("\"" D_ENVIRONMENTAL_CONCENTRATION "10\":%1_f,"), &SEN5XDATA->massConcentrationPm10p0);
|
|
if (!isnan(SEN5XDATA->noxIndex))
|
|
ResponseAppend_P(PSTR("\"NOx\":%0_f,"), &SEN5XDATA->noxIndex);
|
|
if (!isnan(SEN5XDATA->vocIndex))
|
|
ResponseAppend_P(PSTR("\"VOC\":%0_f,"), &SEN5XDATA->vocIndex);
|
|
if (ahum_available)
|
|
ResponseAppendTHD(convertedTemp, convertedHum);
|
|
ResponseAppend_P(PSTR(",\"" D_JSON_AHUM "\":%s"), sen5x_abs_hum);
|
|
ResponseJsonEnd();
|
|
}
|
|
|
|
#ifdef USE_WEBSERVER
|
|
|
|
WSContentSend_PD(HTTP_SNS_SEN5X_UNITS, PSTR(D_ENVIRONMENTAL_CONCENTRATION "1"), 1, &SEN5XDATA->massConcentrationPm1p0, D_UNIT_MICROGRAM_PER_CUBIC_METER);
|
|
WSContentSend_PD(HTTP_SNS_SEN5X_UNITS, PSTR(D_ENVIRONMENTAL_CONCENTRATION "2.5"), 1, &SEN5XDATA->massConcentrationPm2p5, D_UNIT_MICROGRAM_PER_CUBIC_METER);
|
|
WSContentSend_PD(HTTP_SNS_SEN5X_UNITS, PSTR(D_ENVIRONMENTAL_CONCENTRATION "4"), 1, &SEN5XDATA->massConcentrationPm4p0, D_UNIT_MICROGRAM_PER_CUBIC_METER);
|
|
WSContentSend_PD(HTTP_SNS_SEN5X_UNITS, PSTR(D_ENVIRONMENTAL_CONCENTRATION "10"), 1, &SEN5XDATA->massConcentrationPm10p0, D_UNIT_MICROGRAM_PER_CUBIC_METER);
|
|
if (!isnan(SEN5XDATA->noxIndex))
|
|
WSContentSend_PD(HTTP_SNS_SEN5X_UNITLESS, "NOx", 0, &SEN5XDATA->noxIndex);
|
|
if (!isnan(SEN5XDATA->vocIndex))
|
|
WSContentSend_PD(HTTP_SNS_SEN5X_UNITLESS, "VOC", 0, &SEN5XDATA->vocIndex);
|
|
if (ahum_available)
|
|
WSContentSend_THD(PSTR("SEN5X"), convertedTemp, convertedHum);
|
|
WSContentSend_PD(HTTP_SNS_AHUMSEN5X, sen5x_abs_hum);
|
|
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
* Interface
|
|
\*********************************************************************************************/
|
|
|
|
bool Xsns103(uint32_t function)
|
|
{
|
|
if (!I2cEnabled(XI2C_76))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool result = false;
|
|
|
|
if (FUNC_INIT == function)
|
|
{
|
|
sen5x_Init();
|
|
}
|
|
else if (SEN5XDATA != nullptr)
|
|
{
|
|
switch (function)
|
|
{
|
|
case FUNC_EVERY_SECOND:
|
|
SEN5XUpdate();
|
|
break;
|
|
case FUNC_JSON_APPEND:
|
|
SEN5XShow(1);
|
|
break;
|
|
#ifdef USE_WEBSERVER
|
|
case FUNC_WEB_SENSOR:
|
|
SEN5XShow(0);
|
|
break;
|
|
#endif // USE_WEBSERVER
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#endif // USE_SEN5X
|
|
#endif // USE_I2C
|