mirror of https://github.com/arendst/Tasmota.git
326 lines
8.8 KiB
C++
326 lines
8.8 KiB
C++
/*
|
|
xsns_109_sgp4x.ino - SGP4X VOC and NOx sensor support for Tasmota
|
|
|
|
Copyright (C) 2023 Andrew Klaus
|
|
|
|
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_SGP4X
|
|
/*********************************************************************************************\
|
|
* SGP4x - Gas (TVOC - Total Volatile Organic Compounds) and NOx
|
|
*
|
|
* Source: Sensirion Driver, with mods by Andrew Klaus
|
|
* Adaption for TASMOTA: Andrew Klaus
|
|
*
|
|
* I2C Address: 0x59
|
|
\*********************************************************************************************/
|
|
|
|
#define XSNS_109 109
|
|
#define XI2C_82 82 // See I2CDEVICES.md
|
|
|
|
#define SGP4X_ADDRESS 0x59
|
|
|
|
#include "SensirionI2CSgp4x.h"
|
|
|
|
extern "C" {
|
|
#include "sensirion_gas_index_algorithm.h"
|
|
};
|
|
|
|
enum SGP4X_Type {
|
|
TYPE_SGP41,
|
|
TYPE_SGP40,
|
|
};
|
|
|
|
enum SGP4X_State {
|
|
STATE_SGP4X_START,
|
|
STATE_SGP4X_SELFTEST_SENT,
|
|
STATE_SGP4X_SELFTEST_DONE,
|
|
STATE_SGP4X_COND_SENT,
|
|
STATE_SGP4X_COND_DONE,
|
|
STATE_SGP4X_NORMAL,
|
|
STATE_SGP4X_FAIL,
|
|
};
|
|
SensirionI2CSgp4x sgp4x;
|
|
SGP4X_State sgp4x_state = STATE_SGP4X_START;
|
|
SGP4X_Type sgp4x_type = TYPE_SGP41;
|
|
|
|
bool sgp4x_init = false;
|
|
bool sgp4x_read_pend = false;
|
|
|
|
uint16_t srawVoc;
|
|
uint16_t srawNox;
|
|
int32_t voc_index_sgp4x;
|
|
int32_t nox_index_sgp4x;
|
|
|
|
uint16_t selftest_wait = 2; // 2x 250ms cycles (500ms) delay for self-testing
|
|
uint16_t conditioning_s = 10; // 10 second delay for startup
|
|
|
|
GasIndexAlgorithmParams voc_algorithm_params;
|
|
GasIndexAlgorithmParams nox_algorithm_params;
|
|
|
|
/********************************************************************************************/
|
|
|
|
void sgp4x_Init(void)
|
|
{
|
|
if (!I2cSetDevice(SGP4X_ADDRESS)) { return; }
|
|
|
|
uint8_t serialNumberSize = 3;
|
|
uint16_t serialNumber[serialNumberSize];
|
|
uint16_t error;
|
|
|
|
sgp4x.begin(I2cGetWire());
|
|
error = sgp4x.getSerialNumber(serialNumber, serialNumberSize);
|
|
|
|
if (error) {
|
|
AddLog(LOG_LEVEL_INFO, PSTR("SGP4X error: error during getserialnumber"));
|
|
Sgp4xHandleError(error);
|
|
return;
|
|
} else {
|
|
AddLog(LOG_LEVEL_INFO, PSTR("SGP4X serial nr 0x%X 0x%X 0x%X") ,serialNumber[0], serialNumber[1], serialNumber[2]);
|
|
}
|
|
|
|
// The command is still active for both SGP40 and SGP41 using 0x202f to distinguish them.
|
|
uint16_t features;
|
|
error = sgp4x.getFeaturesValue(features);
|
|
if (error) {
|
|
AddLog(LOG_LEVEL_INFO, PSTR("SGP4X error: error during getFeaturesValue"));
|
|
Sgp4xHandleError(error);
|
|
return;
|
|
}
|
|
AddLog(LOG_LEVEL_INFO, PSTR("SGP4X features: 0x%X") ,features);
|
|
|
|
// Undocumented, but confirmed by Sensirion
|
|
// SGP40 is 0x3240, SGP41 is 0x0240
|
|
if (features == 0x3240) {
|
|
sgp4x_type = TYPE_SGP40;
|
|
// SGP40 doesn't do conditioning, so skip
|
|
sgp4x_state = STATE_SGP4X_NORMAL;
|
|
}
|
|
|
|
I2cSetActiveFound(SGP4X_ADDRESS, "SGP4X");
|
|
|
|
sgp4x_init = true;
|
|
error = sgp4x.sendSelfTestCmd();
|
|
|
|
if (error) {
|
|
AddLog(LOG_LEVEL_INFO, PSTR("SGP4X error: error during selftest"));
|
|
Sgp4xHandleError(error);
|
|
sgp4x_state = STATE_SGP4X_FAIL;
|
|
return;
|
|
}
|
|
|
|
sgp4x_state = STATE_SGP4X_SELFTEST_SENT;
|
|
|
|
GasIndexAlgorithm_init(&nox_algorithm_params, GasIndexAlgorithm_ALGORITHM_TYPE_NOX);
|
|
GasIndexAlgorithm_init(&voc_algorithm_params, GasIndexAlgorithm_ALGORITHM_TYPE_VOC);
|
|
}
|
|
|
|
void Sgp4xHandleError(uint16_t error) {
|
|
char errorMessage[256];
|
|
errorToString(error, errorMessage, 256);
|
|
AddLog(LOG_LEVEL_ERROR, PSTR("SGP4X error: %s"), errorMessage);
|
|
}
|
|
|
|
void Sgp4xSendReadCmd(void)
|
|
{
|
|
// Check if we're already waiting for a read
|
|
// Or if we need to wait a cycle before initiating a reading
|
|
if (sgp4x_read_pend){return;}
|
|
|
|
uint16_t error;
|
|
uint16_t rhticks = (uint16_t)((TasmotaGlobal.humidity * 65535) / 100 + 0.5);
|
|
uint16_t tempticks = (uint16_t)(((TasmotaGlobal.temperature_celsius + 45) * 65535) / 175);
|
|
|
|
// Handle self testing
|
|
// Wait 2 cycles (at least 500ms to read selftest value)
|
|
if (sgp4x_state == STATE_SGP4X_SELFTEST_SENT and selftest_wait > 0) {
|
|
selftest_wait--;
|
|
return;
|
|
} else if (sgp4x_state == STATE_SGP4X_SELFTEST_SENT) {
|
|
if (Sgp4xReadSelfTest()){
|
|
sgp4x_state = STATE_SGP4X_FAIL;
|
|
return;
|
|
} else {
|
|
sgp4x_state = STATE_SGP4X_SELFTEST_DONE;
|
|
}
|
|
}
|
|
|
|
// Initiate conditioning
|
|
if (sgp4x_state == STATE_SGP4X_SELFTEST_DONE) {
|
|
error = sgp4x.sendConditioningCmd(0x8000, 0x6666);
|
|
sgp4x_state = STATE_SGP4X_COND_SENT;
|
|
|
|
if (error) {
|
|
Sgp4xHandleError(error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (sgp4x_state == STATE_SGP4X_COND_DONE) {
|
|
sgp4x_state = STATE_SGP4X_NORMAL;
|
|
}
|
|
|
|
// Normal operation
|
|
if (sgp4x_state == STATE_SGP4X_NORMAL) {
|
|
if (sgp4x_type == TYPE_SGP41) {
|
|
error = sgp4x.sendRawSignalsCmd(rhticks, tempticks);
|
|
} else {
|
|
error = sgp4x.sendRawSignalCmd(rhticks, tempticks);
|
|
}
|
|
if (error) {
|
|
Sgp4xHandleError(error);
|
|
} else {
|
|
sgp4x_read_pend = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
bool Sgp4xReadSelfTest() {
|
|
uint16_t testResult;
|
|
uint16_t error;
|
|
|
|
error = sgp4x.readSelfTestValue(testResult);
|
|
if (error) {
|
|
Sgp4xHandleError(error);
|
|
return true;
|
|
} else if (testResult != 0xD400) {
|
|
AddLog(LOG_LEVEL_ERROR, PSTR("SGP4X: executeSelfTest failed with error: %s"), testResult);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Sgp4xUpdate(void)
|
|
{
|
|
uint16_t error;
|
|
|
|
if (sgp4x_state == STATE_SGP4X_FAIL) {
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR("SGP4X in FAIL state"));
|
|
return;
|
|
}
|
|
|
|
// Conditioning - SGP41 NOx needs 10s to warmup
|
|
if (sgp4x_state == STATE_SGP4X_COND_SENT) {
|
|
if (conditioning_s > 0) {
|
|
conditioning_s--;
|
|
return;
|
|
} else {
|
|
sgp4x_state = STATE_SGP4X_COND_DONE;
|
|
}
|
|
}
|
|
|
|
if (sgp4x_state == STATE_SGP4X_NORMAL && sgp4x_read_pend) {
|
|
if (sgp4x_type == TYPE_SGP41) {
|
|
error = sgp4x.readRawSignalsValue(srawVoc, srawNox);
|
|
} else {
|
|
error = sgp4x.readRawSignalValue(srawVoc);
|
|
}
|
|
sgp4x_read_pend = false;
|
|
|
|
if (!error) {
|
|
// SGP40 and SGP41 support VOC
|
|
GasIndexAlgorithm_process(&voc_algorithm_params, srawVoc, &voc_index_sgp4x);
|
|
|
|
// SGP41 supports NOx
|
|
if (sgp4x_type == TYPE_SGP41) {
|
|
GasIndexAlgorithm_process(&nox_algorithm_params, srawNox, &nox_index_sgp4x);
|
|
}
|
|
} else {
|
|
Sgp4xHandleError(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef USE_WEBSERVER
|
|
const char HTTP_SNS_SGP4X_SGP41[] PROGMEM =
|
|
"{s}SGP41 TVOC " D_JSON_RAW "{m}%d " "{e}" // {s} = <tr><th>, {m} = </th><td>, {e} = </td></tr>
|
|
"{s}SGP41 NOX " D_JSON_RAW "{m}%d " "{e}"
|
|
"{s}SGP41 " D_TVOC "{m}%d " "{e}"
|
|
"{s}SGP41 " D_NOX "{m}%d " "{e}";
|
|
|
|
const char HTTP_SNS_SGP4X_SGP40[] PROGMEM =
|
|
"{s}SGP40 TVOC " D_JSON_RAW "{m}%d " "{e}" // {s} = <tr><th>, {m} = </th><td>, {e} = </td></tr>
|
|
"{s}SGP40 " D_TVOC "{m}%d " "{e}";
|
|
#endif
|
|
|
|
void Sgp4xShow(bool json)
|
|
{
|
|
if (sgp4x_state == STATE_SGP4X_NORMAL) {
|
|
if (json) {
|
|
if (sgp4x_type == TYPE_SGP41) {
|
|
ResponseAppend_P(PSTR(",\"SGP41\":{\"VOC_" D_JSON_RAW "\":%d,\"NOX_" D_JSON_RAW "\":%d,\"" D_TVOC "\":%d,\"" D_NOX "\":%d"), srawVoc, srawNox, voc_index_sgp4x, nox_index_sgp4x);
|
|
} else {
|
|
ResponseAppend_P(PSTR(",\"SGP40\":{\"VOC_" D_JSON_RAW "\":%d,\"" D_TVOC "\":%d"), srawVoc, voc_index_sgp4x);
|
|
}
|
|
ResponseJsonEnd();
|
|
#ifdef USE_DOMOTICZ
|
|
if (0 == TasmotaGlobal.tele_period) {
|
|
DomoticzSensor(DZ_AIRQUALITY, voc_index_sgp4x);
|
|
}
|
|
#endif // USE_DOMOTICZ
|
|
#ifdef USE_WEBSERVER
|
|
} else {
|
|
if (sgp4x_type == TYPE_SGP41) {
|
|
WSContentSend_PD(HTTP_SNS_SGP4X_SGP41, srawVoc, srawNox, voc_index_sgp4x, nox_index_sgp4x);
|
|
} else {
|
|
WSContentSend_PD(HTTP_SNS_SGP4X_SGP40, srawVoc, voc_index_sgp4x);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
* Interface
|
|
\*********************************************************************************************/
|
|
|
|
bool Xsns109(uint32_t function)
|
|
{
|
|
if (!I2cEnabled(XI2C_82)) { return false; }
|
|
|
|
bool result = false;
|
|
|
|
if (FUNC_INIT == function) {
|
|
sgp4x_Init();
|
|
}
|
|
else if (sgp4x_init) {
|
|
switch (function) {
|
|
case FUNC_EVERY_250_MSECOND:
|
|
Sgp4xSendReadCmd();
|
|
break;
|
|
case FUNC_EVERY_SECOND:
|
|
Sgp4xUpdate();
|
|
break;
|
|
case FUNC_JSON_APPEND:
|
|
Sgp4xShow(1);
|
|
break;
|
|
#ifdef USE_WEBSERVER
|
|
case FUNC_WEB_SENSOR:
|
|
Sgp4xShow(0);
|
|
break;
|
|
#endif // USE_WEBSERVER
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#endif // USE_sgp4x
|
|
#endif // USE_I2C
|