/*
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 .
*/
#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(Wire);
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} =
, {m} = | , {e} = |
"{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} = , {m} = | , {e} = |
"{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