Add support for SGP41 (#3382)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Martin 2022-05-19 02:47:33 +02:00 committed by GitHub
parent 9c78049359
commit 0ed7db979b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 655 additions and 1210 deletions

View file

@ -178,6 +178,7 @@ esphome/components/sen5x/* @martgras
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @SenexCrenshaw @martgras
esphome/components/shelly_dimmer/* @edge90 @rnauber
esphome/components/sht4x/* @sjtrny
esphome/components/shutdown/* @esphome/core @jsuanet

View file

@ -1,628 +0,0 @@
#include "sensirion_voc_algorithm.h"
namespace esphome {
namespace sgp40 {
/* The VOC code were originally created by
* https://github.com/Sensirion/embedded-sgp
* The fixed point arithmetic parts of this code were originally created by
* https://github.com/PetteriAimonen/libfixmath
*/
/*!< the maximum value of fix16_t */
#define FIX16_MAXIMUM 0x7FFFFFFF
/*!< the minimum value of fix16_t */
static const uint32_t FIX16_MINIMUM = 0x80000000;
/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not
* specified */
static const uint32_t FIX16_OVERFLOW = 0x80000000;
/*!< fix16_t value of 1 */
const uint32_t FIX16_ONE = 0x00010000;
inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; }
inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); }
/*! Multiplies the two given fix16_t's and returns the result. */
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1);
/*! Divides the first given fix16_t by the second and returns the result. */
static fix16_t fix16_div(fix16_t a, fix16_t b);
/*! Returns the square root of the given fix16_t. */
static fix16_t fix16_sqrt(fix16_t in_value);
/*! Returns the exponent (e^) of the given fix16_t. */
static fix16_t fix16_exp(fix16_t in_value);
static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) {
// Each argument is divided to 16-bit parts.
// AB
// * CD
// -----------
// BD 16 * 16 -> 32 bit products
// CB
// AD
// AC
// |----| 64 bit product
int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16);
uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF);
int32_t ac = a * c;
int32_t ad_cb = a * d + c * b;
uint32_t bd = b * d;
int32_t product_hi = ac + (ad_cb >> 16); // NOLINT
// Handle carry from lower 32 bits to upper part of result.
uint32_t ad_cb_temp = ad_cb << 16; // NOLINT
uint32_t product_lo = bd + ad_cb_temp;
if (product_lo < bd)
product_hi++;
#ifndef FIXMATH_NO_OVERFLOW
// The upper 17 bits should all be the same (the sign).
if (product_hi >> 31 != product_hi >> 15)
return FIX16_OVERFLOW;
#endif
#ifdef FIXMATH_NO_ROUNDING
return (product_hi << 16) | (product_lo >> 16);
#else
// Subtracting 0x8000 (= 0.5) and then using signed right shift
// achieves proper rounding to result-1, except in the corner
// case of negative numbers and lowest word = 0x8000.
// To handle that, we also have to subtract 1 for negative numbers.
uint32_t product_lo_tmp = product_lo;
product_lo -= 0x8000;
product_lo -= (uint32_t) product_hi >> 31;
if (product_lo > product_lo_tmp)
product_hi--;
// Discard the lowest 16 bits. Note that this is not exactly the same
// as dividing by 0x10000. For example if product = -1, result will
// also be -1 and not 0. This is compensated by adding +1 to the result
// and compensating this in turn in the rounding above.
fix16_t result = (product_hi << 16) | (product_lo >> 16); // NOLINT
result += 1;
return result;
#endif
}
static fix16_t fix16_div(fix16_t a, fix16_t b) {
// This uses the basic binary restoring division algorithm.
// It appears to be faster to do the whole division manually than
// trying to compose a 64-bit divide out of 32-bit divisions on
// platforms without hardware divide.
if (b == 0)
return FIX16_MINIMUM;
uint32_t remainder = (a >= 0) ? a : (-a);
uint32_t divider = (b >= 0) ? b : (-b);
uint32_t quotient = 0;
uint32_t bit = 0x10000;
/* The algorithm requires D >= R */
while (divider < remainder) {
divider <<= 1;
bit <<= 1;
}
#ifndef FIXMATH_NO_OVERFLOW
if (!bit)
return FIX16_OVERFLOW;
#endif
if (divider & 0x80000000) {
// Perform one step manually to avoid overflows later.
// We know that divider's bottom bit is 0 here.
if (remainder >= divider) {
quotient |= bit;
remainder -= divider;
}
divider >>= 1;
bit >>= 1;
}
/* Main division loop */
while (bit && remainder) {
if (remainder >= divider) {
quotient |= bit;
remainder -= divider;
}
remainder <<= 1;
bit >>= 1;
}
#ifndef FIXMATH_NO_ROUNDING
if (remainder >= divider) {
quotient++;
}
#endif
fix16_t result = quotient;
/* Figure out the sign of result */
if ((a ^ b) & 0x80000000) {
#ifndef FIXMATH_NO_OVERFLOW
if (result == FIX16_MINIMUM) // NOLINT(clang-diagnostic-sign-compare)
return FIX16_OVERFLOW;
#endif
result = -result;
}
return result;
}
static fix16_t fix16_sqrt(fix16_t in_value) {
// It is assumed that x is not negative
uint32_t num = in_value;
uint32_t result = 0;
uint32_t bit;
uint8_t n;
bit = (uint32_t) 1 << 30;
while (bit > num)
bit >>= 2;
// The main part is executed twice, in order to avoid
// using 64 bit values in computations.
for (n = 0; n < 2; n++) {
// First we get the top 24 bits of the answer.
while (bit) {
if (num >= result + bit) {
num -= result + bit;
result = (result >> 1) + bit;
} else {
result = (result >> 1);
}
bit >>= 2;
}
if (n == 0) {
// Then process it again to get the lowest 8 bits.
if (num > 65535) {
// The remainder 'num' is too large to be shifted left
// by 16, so we have to add 1 to result manually and
// adjust 'num' accordingly.
// num = a - (result + 0.5)^2
// = num + result^2 - (result + 0.5)^2
// = num - result - 0.5
num -= result;
num = (num << 16) - 0x8000;
result = (result << 16) + 0x8000;
} else {
num <<= 16;
result <<= 16;
}
bit = 1 << 14;
}
}
#ifndef FIXMATH_NO_ROUNDING
// Finally, if next bit would have been 1, round the result upwards.
if (num > result) {
result++;
}
#endif
return (fix16_t) result;
}
static fix16_t fix16_exp(fix16_t in_value) {
// Function to approximate exp(); optimized more for code size than speed
// exp(x) for x = +/- {1, 1/8, 1/64, 1/512}
fix16_t x = in_value;
static const uint8_t NUM_EXP_VALUES = 4;
static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)};
static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)};
const fix16_t *exp_values;
fix16_t res, arg;
uint16_t i;
if (x >= F16(10.3972))
return FIX16_MAXIMUM;
if (x <= F16(-11.7835))
return 0;
if (x < 0) {
x = -x;
exp_values = EXP_NEG_VALUES;
} else {
exp_values = EXP_POS_VALUES;
}
res = FIX16_ONE;
arg = FIX16_ONE;
for (i = 0; i < NUM_EXP_VALUES; i++) {
while (x >= arg) {
res = fix16_mul(res, exp_values[i]);
x -= arg;
}
arg >>= 3;
}
return res;
}
static void voc_algorithm_init_instances(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial,
fix16_t tau_mean_variance_hours,
fix16_t gating_max_duration_minutes);
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std,
fix16_t uptime_gamma);
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params);
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params,
fix16_t voc_index_from_prior);
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw,
fix16_t voc_index_from_prior);
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params);
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l,
fix16_t x0, fix16_t k);
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample);
static void voc_algorithm_mox_model_init(VocAlgorithmParams *params);
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean);
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw);
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params);
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset);
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample);
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params);
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params);
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample);
void voc_algorithm_init(VocAlgorithmParams *params) {
params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT);
params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS);
params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES);
params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL);
params->mUptime = F16(0.);
params->mSraw = F16(0.);
params->mVoc_Index = 0;
voc_algorithm_init_instances(params);
}
static void voc_algorithm_init_instances(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_init(params);
voc_algorithm_mean_variance_estimator_set_parameters(
params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes);
voc_algorithm_mox_model_init(params);
voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
voc_algorithm_mean_variance_estimator_get_mean(params));
voc_algorithm_sigmoid_scaled_init(params);
voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset);
voc_algorithm_adaptive_lowpass_init(params);
voc_algorithm_adaptive_lowpass_set_parameters(params);
}
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1) {
*state0 = voc_algorithm_mean_variance_estimator_get_mean(params);
*state1 = voc_algorithm_mean_variance_estimator_get_std(params);
}
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1) {
voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA));
params->mSraw = state0;
}
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
int32_t learning_time_hours, int32_t gating_max_duration_minutes,
int32_t std_initial) {
params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset));
params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours));
params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes));
params->mSraw_Std_Initial = (fix16_from_int(std_initial));
voc_algorithm_init_instances(params);
}
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index) {
if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) {
params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
} else {
if (((sraw > 0) && (sraw < 65000))) {
if ((sraw < 20001)) {
sraw = 20001;
} else if ((sraw > 52767)) {
sraw = 52767;
}
params->mSraw = (fix16_from_int((sraw - 20000)));
}
params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw);
params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index);
params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index);
if ((params->mVoc_Index < F16(0.5))) {
params->mVoc_Index = F16(0.5);
}
if ((params->mSraw > F16(0.))) {
voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index);
voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params),
voc_algorithm_mean_variance_estimator_get_mean(params));
}
}
*voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5))));
}
static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.));
voc_algorithm_mean_variance_estimator_init_instances(params);
}
static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_sigmoid_init(params);
}
static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial,
fix16_t tau_mean_variance_hours,
fix16_t gating_max_duration_minutes) {
params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes = gating_max_duration_minutes;
params->m_Mean_Variance_Estimator_Initialized = false;
params->m_Mean_Variance_Estimator_Mean = F16(0.);
params->m_Mean_Variance_Estimator_Sraw_Offset = F16(0.);
params->m_Mean_Variance_Estimator_Std = std_initial;
params->m_Mean_Variance_Estimator_Gamma =
(fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))),
(tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.)))));
params->m_Mean_Variance_Estimator_Gamma_Initial_Mean =
F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
(VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Mean_Variance_Estimator_Gamma_Initial_Variance =
F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) /
(VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Mean_Variance_Estimator_Gamma_Mean = F16(0.);
params->m_Mean_Variance_Estimator_Gamma_Variance = F16(0.);
params->m_Mean_Variance_Estimator_Uptime_Gamma = F16(0.);
params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.);
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.);
}
static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std,
fix16_t uptime_gamma) {
params->m_Mean_Variance_Estimator_Mean = mean;
params->m_Mean_Variance_Estimator_Std = std;
params->m_Mean_Variance_Estimator_Uptime_Gamma = uptime_gamma;
params->m_Mean_Variance_Estimator_Initialized = true;
}
static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params) {
return params->m_Mean_Variance_Estimator_Std;
}
static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params) {
return (params->m_Mean_Variance_Estimator_Mean + params->m_Mean_Variance_Estimator_Sraw_Offset);
}
static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params,
fix16_t voc_index_from_prior) {
fix16_t uptime_limit;
fix16_t sigmoid_gamma_mean;
fix16_t gamma_mean;
fix16_t gating_threshold_mean;
fix16_t sigmoid_gating_mean;
fix16_t sigmoid_gamma_variance;
fix16_t gamma_variance;
fix16_t gating_threshold_variance;
fix16_t sigmoid_gating_variance;
uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL));
if ((params->m_Mean_Variance_Estimator_Uptime_Gamma < uptime_limit)) {
params->m_Mean_Variance_Estimator_Uptime_Gamma =
(params->m_Mean_Variance_Estimator_Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
}
if ((params->m_Mean_Variance_Estimator_Uptime_Gating < uptime_limit)) {
params->m_Mean_Variance_Estimator_Uptime_Gating =
(params->m_Mean_Variance_Estimator_Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL));
}
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN),
F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN));
sigmoid_gamma_mean =
voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma);
gamma_mean =
(params->m_Mean_Variance_Estimator_Gamma +
(fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Mean - params->m_Mean_Variance_Estimator_Gamma),
sigmoid_gamma_mean)));
gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) +
(fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
voc_algorithm_mean_variance_estimator_sigmoid_process(
params, params->m_Mean_Variance_Estimator_Uptime_Gating))));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean,
F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
params->m_Mean_Variance_Estimator_Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(
params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE));
sigmoid_gamma_variance =
voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma);
gamma_variance =
(params->m_Mean_Variance_Estimator_Gamma +
(fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Variance - params->m_Mean_Variance_Estimator_Gamma),
(sigmoid_gamma_variance - sigmoid_gamma_mean))));
gating_threshold_variance =
(F16(VOC_ALGORITHM_GATING_THRESHOLD) +
(fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)),
voc_algorithm_mean_variance_estimator_sigmoid_process(
params, params->m_Mean_Variance_Estimator_Uptime_Gating))));
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance,
F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION));
sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior);
params->m_Mean_Variance_Estimator_Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance));
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes =
(params->m_Mean_Variance_Estimator_Gating_Duration_Minutes +
(fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)),
((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) -
F16(VOC_ALGORITHM_GATING_MAX_RATIO)))));
if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes < F16(0.))) {
params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.);
}
if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes >
params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes)) {
params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.);
}
}
static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw,
fix16_t voc_index_from_prior) {
fix16_t delta_sgp;
fix16_t c;
fix16_t additional_scaling;
if ((!params->m_Mean_Variance_Estimator_Initialized)) {
params->m_Mean_Variance_Estimator_Initialized = true;
params->m_Mean_Variance_Estimator_Sraw_Offset = sraw;
params->m_Mean_Variance_Estimator_Mean = F16(0.);
} else {
if (((params->m_Mean_Variance_Estimator_Mean >= F16(100.)) ||
(params->m_Mean_Variance_Estimator_Mean <= F16(-100.)))) {
params->m_Mean_Variance_Estimator_Sraw_Offset =
(params->m_Mean_Variance_Estimator_Sraw_Offset + params->m_Mean_Variance_Estimator_Mean);
params->m_Mean_Variance_Estimator_Mean = F16(0.);
}
sraw = (sraw - params->m_Mean_Variance_Estimator_Sraw_Offset);
voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior);
delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator_Mean),
F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING)));
if ((delta_sgp < F16(0.))) {
c = (params->m_Mean_Variance_Estimator_Std - delta_sgp);
} else {
c = (params->m_Mean_Variance_Estimator_Std + delta_sgp);
}
additional_scaling = F16(1.);
if ((c > F16(1440.))) {
additional_scaling = F16(4.);
}
params->m_Mean_Variance_Estimator_Std = (fix16_mul(
fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) -
params->m_Mean_Variance_Estimator_Gamma_Variance)))),
fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator_Std,
(fix16_div(params->m_Mean_Variance_Estimator_Std,
(fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING),
additional_scaling)))))) +
(fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Variance, delta_sgp)),
additional_scaling)),
delta_sgp))))));
params->m_Mean_Variance_Estimator_Mean =
(params->m_Mean_Variance_Estimator_Mean + (fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Mean, delta_sgp)));
}
}
static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params) {
voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.));
}
static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l,
fix16_t x0, fix16_t k) {
params->m_Mean_Variance_Estimator_Sigmoid_L = l;
params->m_Mean_Variance_Estimator_Sigmoid_K = k;
params->m_Mean_Variance_Estimator_Sigmoid_X0 = x0;
}
static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t x;
x = (fix16_mul(params->m_Mean_Variance_Estimator_Sigmoid_K, (sample - params->m_Mean_Variance_Estimator_Sigmoid_X0)));
if ((x < F16(-50.))) {
return params->m_Mean_Variance_Estimator_Sigmoid_L;
} else if ((x > F16(50.))) {
return F16(0.);
} else {
return (fix16_div(params->m_Mean_Variance_Estimator_Sigmoid_L, (F16(1.) + fix16_exp(x))));
}
}
static void voc_algorithm_mox_model_init(VocAlgorithmParams *params) {
voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.));
}
static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean) {
params->m_Mox_Model_Sraw_Std = sraw_std;
params->m_Mox_Model_Sraw_Mean = sraw_mean;
}
static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw) {
return (fix16_mul((fix16_div((sraw - params->m_Mox_Model_Sraw_Mean),
(-(params->m_Mox_Model_Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))),
F16(VOC_ALGORITHM_VOC_INDEX_GAIN)));
}
static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params) {
voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.));
}
static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset) {
params->m_Sigmoid_Scaled_Offset = offset;
}
static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t x;
fix16_t shift;
x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0))));
if ((x < F16(-50.))) {
return F16(VOC_ALGORITHM_SIGMOID_L);
} else if ((x > F16(50.))) {
return F16(0.);
} else {
if ((sample >= F16(0.))) {
shift =
(fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled_Offset))), F16(4.)));
return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift);
} else {
return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled_Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))),
(fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x))))));
}
}
}
static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params) {
voc_algorithm_adaptive_lowpass_set_parameters(params);
}
static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params) {
params->m_Adaptive_Lowpass_A1 =
F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Adaptive_Lowpass_A2 =
F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL)));
params->m_Adaptive_Lowpass_Initialized = false;
}
static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample) {
fix16_t abs_delta;
fix16_t f1;
fix16_t tau_a;
fix16_t a3;
if ((!params->m_Adaptive_Lowpass_Initialized)) {
params->m_Adaptive_Lowpass_X1 = sample;
params->m_Adaptive_Lowpass_X2 = sample;
params->m_Adaptive_Lowpass_X3 = sample;
params->m_Adaptive_Lowpass_Initialized = true;
}
params->m_Adaptive_Lowpass_X1 =
((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A1), params->m_Adaptive_Lowpass_X1)) +
(fix16_mul(params->m_Adaptive_Lowpass_A1, sample)));
params->m_Adaptive_Lowpass_X2 =
((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A2), params->m_Adaptive_Lowpass_X2)) +
(fix16_mul(params->m_Adaptive_Lowpass_A2, sample)));
abs_delta = (params->m_Adaptive_Lowpass_X1 - params->m_Adaptive_Lowpass_X2);
if ((abs_delta < F16(0.))) {
abs_delta = (-abs_delta);
}
f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta)));
tau_a =
((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST));
a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a)));
params->m_Adaptive_Lowpass_X3 =
((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass_X3)) + (fix16_mul(a3, sample)));
return params->m_Adaptive_Lowpass_X3;
}
} // namespace sgp40
} // namespace esphome

View file

@ -1,147 +0,0 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace sgp40 {
/* The VOC code were originally created by
* https://github.com/Sensirion/embedded-sgp
* The fixed point arithmetic parts of this code were originally created by
* https://github.com/PetteriAimonen/libfixmath
*/
using fix16_t = int32_t;
#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5)))
static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.);
static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.);
static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.);
static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.);
static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.);
static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.);
static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.);
static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75));
static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01);
static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.);
static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45));
static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01);
static const float VOC_ALGORITHM_GATING_THRESHOLD(340.);
static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.);
static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09);
static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.));
static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3);
static const float VOC_ALGORITHM_SIGMOID_L(500.);
static const float VOC_ALGORITHM_SIGMOID_K(-0.0065);
static const float VOC_ALGORITHM_SIGMOID_X0(213.);
static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.);
static const float VOC_ALGORITHM_LP_TAU_FAST(20.0);
static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0);
static const float VOC_ALGORITHM_LP_ALPHA(-0.2);
static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.));
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.);
static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.);
/**
* Struct to hold all the states of the VOC algorithm.
*/
struct VocAlgorithmParams {
fix16_t mVoc_Index_Offset;
fix16_t mTau_Mean_Variance_Hours;
fix16_t mGating_Max_Duration_Minutes;
fix16_t mSraw_Std_Initial;
fix16_t mUptime;
fix16_t mSraw;
fix16_t mVoc_Index;
fix16_t m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes;
bool m_Mean_Variance_Estimator_Initialized;
fix16_t m_Mean_Variance_Estimator_Mean;
fix16_t m_Mean_Variance_Estimator_Sraw_Offset;
fix16_t m_Mean_Variance_Estimator_Std;
fix16_t m_Mean_Variance_Estimator_Gamma;
fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Mean;
fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Variance;
fix16_t m_Mean_Variance_Estimator_Gamma_Mean;
fix16_t m_Mean_Variance_Estimator_Gamma_Variance;
fix16_t m_Mean_Variance_Estimator_Uptime_Gamma;
fix16_t m_Mean_Variance_Estimator_Uptime_Gating;
fix16_t m_Mean_Variance_Estimator_Gating_Duration_Minutes;
fix16_t m_Mean_Variance_Estimator_Sigmoid_L;
fix16_t m_Mean_Variance_Estimator_Sigmoid_K;
fix16_t m_Mean_Variance_Estimator_Sigmoid_X0;
fix16_t m_Mox_Model_Sraw_Std;
fix16_t m_Mox_Model_Sraw_Mean;
fix16_t m_Sigmoid_Scaled_Offset;
fix16_t m_Adaptive_Lowpass_A1;
fix16_t m_Adaptive_Lowpass_A2;
bool m_Adaptive_Lowpass_Initialized;
fix16_t m_Adaptive_Lowpass_X1;
fix16_t m_Adaptive_Lowpass_X2;
fix16_t m_Adaptive_Lowpass_X3;
};
/**
* Initialize the VOC algorithm parameters. Call this once at the beginning or
* whenever the sensor stopped measurements.
* @param params Pointer to the VocAlgorithmParams struct
*/
void voc_algorithm_init(VocAlgorithmParams *params);
/**
* Get current algorithm states. Retrieved values can be used in
* voc_algorithm_set_states() to resume operation after a short interruption,
* skipping initial learning phase. This feature can only be used after at least
* 3 hours of continuous operation.
* @param params Pointer to the VocAlgorithmParams struct
* @param state0 State0 to be stored
* @param state1 State1 to be stored
*/
void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1);
/**
* Set previously retrieved algorithm states to resume operation after a short
* interruption, skipping initial learning phase. This feature should not be
* used after inerruptions of more than 10 minutes. Call this once after
* voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if
* desired. Otherwise, the algorithm will start with initial learning phase.
* @param params Pointer to the VocAlgorithmParams struct
* @param state0 State0 to be restored
* @param state1 State1 to be restored
*/
void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1);
/**
* Set parameters to customize the VOC algorithm. Call this once after
* voc_algorithm_init(), if desired. Otherwise, the default values will be used.
*
* @param params Pointer to the VocAlgorithmParams struct
* @param voc_index_offset VOC index representing typical (average)
* conditions. Range 1..250, default 100
* @param learning_time_hours Time constant of long-term estimator.
* Past events will be forgotten after about
* twice the learning time.
* Range 1..72 [hours], default 12 [hours]
* @param gating_max_duration_minutes Maximum duration of gating (freeze of
* estimator during high VOC index signal).
* 0 (no gating) or range 1..720 [minutes],
* default 180 [minutes]
* @param std_initial Initial estimate for standard deviation.
* Lower value boosts events during initial
* learning period, but may result in larger
* device-to-device variations.
* Range 10..500, default 50
*/
void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset,
int32_t learning_time_hours, int32_t gating_max_duration_minutes,
int32_t std_initial);
/**
* Calculate the VOC index value from the raw sensor value.
*
* @param params Pointer to the VocAlgorithmParams struct
* @param sraw Raw value from the SGP40 sensor
* @param voc_index Calculated VOC index value from the raw sensor value. Zero
* during initial blackout period and 1..500 afterwards
*/
void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index);
} // namespace sgp40
} // namespace esphome

View file

@ -1,70 +1,8 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, sensirion_common
from esphome.const import (
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
ICON_RADIATOR,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
)
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
CODEOWNERS = ["@SenexCrenshaw"]
sgp40_ns = cg.esphome_ns.namespace("sgp40")
SGP40Component = sgp40_ns.class_(
"SGP40Component",
sensor.Sensor,
cg.PollingComponent,
sensirion_common.SensirionI2CDevice,
CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid(
"SGP40 is deprecated.\nPlease use the SGP4x platform instead.\nSGP4x supports both SPG40 and SGP41.\n"
" See https://esphome.io/components/sensor/sgp4x.html"
)
CONF_COMPENSATION = "compensation"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_VOC_BASELINE = "voc_baseline"
CONFIG_SCHEMA = (
sensor.sensor_schema(
SGP40Component,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_COMPENSATION): cv.Schema(
{
cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
},
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x59))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
cg.add(var.set_humidity_sensor(sens))
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
if CONF_VOC_BASELINE in config:
cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))

View file

@ -1,274 +0,0 @@
#include "sgp40.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace sgp40 {
static const char *const TAG = "sgp40";
void SGP40Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up SGP40...");
// Serial Number identification
if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
uint16_t raw_serial_number[3];
if (!this->read_data(raw_serial_number, 3)) {
this->mark_failed();
return;
}
this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
(uint64_t(raw_serial_number[2]));
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
// Featureset identification for future use
if (!this->write_command(SGP40_CMD_GET_FEATURESET)) {
ESP_LOGD(TAG, "raw_featureset write_command_ failed");
this->mark_failed();
return;
}
uint16_t raw_featureset;
if (!this->read_data(raw_featureset)) {
ESP_LOGD(TAG, "raw_featureset read_data_ failed");
this->mark_failed();
return;
}
this->featureset_ = raw_featureset;
if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) {
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
SGP40_FEATURESET);
this->mark_failed();
return;
}
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
voc_algorithm_init(&this->voc_algorithm_params_);
if (this->store_baseline_) {
// Hash with compilation time
// This ensures the baseline storage is cleared after OTA
uint32_t hash = fnv1_hash(App.get_compilation_time());
this->pref_ = global_preferences->make_preference<SGP40Baselines>(hash, true);
if (this->pref_.load(&this->baselines_storage_)) {
this->state0_ = this->baselines_storage_.state0;
this->state1_ = this->baselines_storage_.state1;
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0,
this->baselines_storage_.state1);
}
}
this->self_test_();
/* The official spec for this sensor at https://docs.rs-online.com/1956/A700000007055193.pdf
indicates this sensor should be driven at 1Hz. Comments from the developers at:
https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit
resilient to slight timing variations so the software timer should be accurate enough for
this.
This block starts sampling from the sensor at 1Hz, and is done seperately from the call
to the update method. This seperation is to support getting accurate measurements but
limit the amount of communication done over wifi for power consumption or to keep the
number of records reported from being overwhelming.
*/
ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
this->set_interval(1000, [this]() { this->update_voc_index(); });
}
void SGP40Component::self_test_() {
ESP_LOGD(TAG, "Self-test started");
if (!this->write_command(SGP40_CMD_SELF_TEST)) {
this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGD(TAG, "Self-test communication failed");
this->mark_failed();
}
this->set_timeout(250, [this]() {
uint16_t reply;
if (!this->read_data(reply)) {
ESP_LOGD(TAG, "Self-test read_data_ failed");
this->mark_failed();
return;
}
if (reply == 0xD400) {
this->self_test_complete_ = true;
ESP_LOGD(TAG, "Self-test completed");
return;
}
ESP_LOGD(TAG, "Self-test failed");
this->mark_failed();
});
}
/**
* @brief Combined the measured gasses, temperature, and humidity
* to calculate the VOC Index
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return int32_t The VOC Index
*/
int32_t SGP40Component::measure_voc_index_() {
int32_t voc_index;
uint16_t sraw = measure_raw_();
if (sraw == UINT16_MAX)
return UINT16_MAX;
this->status_clear_warning();
voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index);
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_);
if ((uint32_t) abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF ||
(uint32_t) abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->baselines_storage_.state0 = this->state0_;
this->baselines_storage_.state1 = this->state1_;
if (this->pref_.save(&this->baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0,
baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
return voc_index;
}
/**
* @brief Return the raw gas measurement
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return uint16_t The current raw gas measurement
*/
uint16_t SGP40Component::measure_raw_() {
float humidity = NAN;
if (!this->self_test_complete_) {
ESP_LOGD(TAG, "Self-test not yet complete");
return UINT16_MAX;
}
if (this->humidity_sensor_ != nullptr) {
humidity = this->humidity_sensor_->state;
}
if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
humidity = 50;
}
float temperature = NAN;
if (this->temperature_sensor_ != nullptr) {
temperature = float(this->temperature_sensor_->state);
}
if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
temperature = 25;
}
uint16_t data[2];
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
// first paramater is the relative humidity ticks
data[0] = rhticks;
// second paramater is the temperature ticks
data[1] = tempticks;
if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) {
this->status_set_warning();
ESP_LOGD(TAG, "write error (%d)", this->last_error_);
return false;
}
delay(30);
uint16_t raw_data;
if (!this->read_data(raw_data)) {
this->status_set_warning();
ESP_LOGD(TAG, "read_data_ error");
return UINT16_MAX;
}
return raw_data;
}
void SGP40Component::update_voc_index() {
this->seconds_since_last_store_ += 1;
this->voc_index_ = this->measure_voc_index_();
if (this->samples_read_ < this->samples_to_stabalize_) {
this->samples_read_++;
ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
this->samples_to_stabalize_, this->voc_index_);
return;
}
}
void SGP40Component::update() {
if (this->samples_read_ < this->samples_to_stabalize_) {
return;
}
if (this->voc_index_ != UINT16_MAX) {
this->status_clear_warning();
this->publish_state(this->voc_index_);
} else {
this->status_set_warning();
}
}
void SGP40Component::dump_config() {
ESP_LOGCONFIG(TAG, "SGP40:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
default:
ESP_LOGW(TAG, "Unknown setup error!");
break;
}
} else {
ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT);
}
LOG_UPDATE_INTERVAL(this);
if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " Compensation:");
LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
} else {
ESP_LOGCONFIG(TAG, " Compensation: No source configured");
}
}
} // namespace sgp40
} // namespace esphome

View file

@ -1,93 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
#include "sensirion_voc_algorithm.h"
#include <cmath>
namespace esphome {
namespace sgp40 {
struct SGP40Baselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
// commands and constants
static const uint8_t SGP40_FEATURESET = 0x0020; ///< The required set for this library
static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31; ///< Seed for SGP40's CRC polynomial
static const uint8_t SGP40_CRC8_INIT = 0xFF; ///< Init value for CRC
static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word
// Commands
static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682;
static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f;
static const uint16_t SGP40_CMD_SELF_TEST = 0x280e;
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
// Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
// Store anyway if the baseline difference exceeds the max storage diff value
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
class SGP40Component;
/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors.
class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
public:
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void setup() override;
void update() override;
void update_voc_index();
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
protected:
/// Input sensor for humidity and temperature compensation.
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
int16_t sensirion_init_sensors_();
int16_t sgp40_probe_();
uint64_t serial_number_;
uint16_t featureset_;
int32_t measure_voc_index_();
uint8_t generate_crc_(const uint8_t *data, uint8_t datalen);
uint16_t measure_raw_();
ESPPreferenceObject pref_;
uint32_t seconds_since_last_store_;
SGP40Baselines baselines_storage_;
VocAlgorithmParams voc_algorithm_params_;
bool self_test_complete_;
bool store_baseline_;
int32_t state0_;
int32_t state1_;
int32_t voc_index_ = 0;
uint8_t samples_read_ = 0;
uint8_t samples_to_stabalize_ = static_cast<int8_t>(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2;
/**
* @brief Request the sensor to perform a self-test, returning the result
*
* @return true: success false:failure
*/
void self_test_();
enum ErrorCode {
COMMUNICATION_FAILED,
MEASUREMENT_INIT_FAILED,
INVALID_ID,
UNSUPPORTED_ID,
UNKNOWN
} error_code_{UNKNOWN};
};
} // namespace sgp40
} // namespace esphome

View file

View file

@ -0,0 +1,144 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, sensirion_common
from esphome.const import (
CONF_ID,
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
ICON_RADIATOR,
DEVICE_CLASS_NITROUS_OXIDE,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
STATE_CLASS_MEASUREMENT,
)
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensirion_common"]
CODEOWNERS = ["@SenexCrenshaw", "@martgras"]
sgp4x_ns = cg.esphome_ns.namespace("sgp4x")
SGP4xComponent = sgp4x_ns.class_(
"SGP4xComponent",
sensor.Sensor,
cg.PollingComponent,
sensirion_common.SensirionI2CDevice,
)
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_COMPENSATION = "compensation"
CONF_GAIN_FACTOR = "gain_factor"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
def validate_sensors(config):
if CONF_VOC not in config and CONF_NOX not in config:
raise cv.Invalid(
f"At least one sensor is required. Define {CONF_VOC} and/or {CONF_NOX}"
)
return config
GAS_SENSOR = cv.Schema(
{
cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
{
cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_,
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_,
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_,
cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_,
cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_,
}
)
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SGP4xComponent),
cv.Optional(CONF_VOC): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_NOX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_NITROUS_OXIDE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(GAS_SENSOR),
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_COMPENSATION): cv.Schema(
{
cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor),
},
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x59)),
validate_sensors,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
cg.add(var.set_humidity_sensor(sens))
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
if CONF_VOC_BASELINE in config:
cg.add(var.set_voc_baseline(CONF_VOC_BASELINE))
if CONF_VOC in config:
sens = await sensor.new_sensor(config[CONF_VOC])
cg.add(var.set_voc_sensor(sens))
if CONF_ALGORITHM_TUNING in config[CONF_VOC]:
cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING]
cg.add(
var.set_voc_algorithm_tuning(
cfg[CONF_INDEX_OFFSET],
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
cfg[CONF_GATING_MAX_DURATION_MINUTES],
cfg[CONF_STD_INITIAL],
cfg[CONF_GAIN_FACTOR],
)
)
if CONF_NOX in config:
sens = await sensor.new_sensor(config[CONF_NOX])
cg.add(var.set_nox_sensor(sens))
if CONF_ALGORITHM_TUNING in config[CONF_NOX]:
cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING]
cg.add(
var.set_nox_algorithm_tuning(
cfg[CONF_INDEX_OFFSET],
cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
cfg[CONF_LEARNING_TIME_GAIN_HOURS],
cfg[CONF_GATING_MAX_DURATION_MINUTES],
cfg[CONF_GAIN_FACTOR],
)
)
cg.add_library(
None, None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git"
)

View file

@ -0,0 +1,343 @@
#include "sgp4x.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
namespace sgp4x {
static const char *const TAG = "sgp4x";
void SGP4xComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up SGP4x...");
// Serial Number identification
uint16_t raw_serial_number[3];
if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) {
ESP_LOGE(TAG, "Failed to read serial number");
this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
this->mark_failed();
return;
}
this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) |
(uint64_t(raw_serial_number[2]));
ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_);
// Featureset identification for future use
uint16_t raw_featureset;
if (!this->get_register(SGP4X_CMD_GET_FEATURESET, raw_featureset, 1)) {
ESP_LOGD(TAG, "raw_featureset write_command_ failed");
this->mark_failed();
return;
}
this->featureset_ = raw_featureset;
if ((this->featureset_ & 0x1FF) == SGP40_FEATURESET) {
sgp_type_ = SGP40;
self_test_time_ = SPG40_SELFTEST_TIME;
measure_time_ = SGP40_MEASURE_TIME;
if (this->nox_sensor_) {
ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected");
// disable the sensor
this->nox_sensor_->set_disabled_by_default(true);
// make sure it's not visiable in HA
this->nox_sensor_->set_internal(true);
this->nox_sensor_->state = NAN;
// remove pointer to sensor
this->nox_sensor_ = nullptr;
}
} else {
if ((this->featureset_ & 0x1FF) == SGP41_FEATURESET) {
sgp_type_ = SGP41;
self_test_time_ = SPG41_SELFTEST_TIME;
measure_time_ = SGP41_MEASURE_TIME;
} else {
ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF),
SGP40_FEATURESET);
this->mark_failed();
return;
}
}
ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF));
if (this->store_baseline_) {
// Hash with compilation time
// This ensures the baseline storage is cleared after OTA
uint32_t hash = fnv1_hash(App.get_compilation_time());
this->pref_ = global_preferences->make_preference<SGP4xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
this->voc_state0_ = this->voc_baselines_storage_.state0;
this->voc_state1_ = this->voc_baselines_storage_.state1;
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0,
voc_baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X",
this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
}
}
if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) {
voc_algorithm_.set_tuning_parameters(
voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours,
voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes,
voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor);
}
if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) {
nox_algorithm_.set_tuning_parameters(
nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours,
nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes,
nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor);
}
this->self_test_();
/* The official spec for this sensor at
https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this
sensor should be driven at 1Hz. Comments from the developers at:
https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight
timing variations so the software timer should be accurate enough for this.
This block starts sampling from the sensor at 1Hz, and is done seperately from the call
to the update method. This seperation is to support getting accurate measurements but
limit the amount of communication done over wifi for power consumption or to keep the
number of records reported from being overwhelming.
*/
ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler");
this->set_interval(1000, [this]() { this->update_gas_indices(); });
}
void SGP4xComponent::self_test_() {
ESP_LOGD(TAG, "Self-test started");
if (!this->write_command(SGP4X_CMD_SELF_TEST)) {
this->error_code_ = COMMUNICATION_FAILED;
ESP_LOGD(TAG, "Self-test communication failed");
this->mark_failed();
}
this->set_timeout(self_test_time_, [this]() {
uint16_t reply;
if (!this->read_data(reply)) {
this->error_code_ = SELF_TEST_FAILED;
ESP_LOGD(TAG, "Self-test read_data_ failed");
this->mark_failed();
return;
}
if (reply == 0xD400) {
this->self_test_complete_ = true;
ESP_LOGD(TAG, "Self-test completed");
return;
} else {
this->error_code_ = SELF_TEST_FAILED;
ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
return;
}
ESP_LOGD(TAG, "Self-test failed 0x%X", reply);
this->mark_failed();
});
}
/**
* @brief Combined the measured gasses, temperature, and humidity
* to calculate the VOC Index
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return int32_t The VOC Index
*/
bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) {
uint16_t voc_sraw;
uint16_t nox_sraw;
if (!measure_raw_(voc_sraw, nox_sraw))
return false;
this->status_clear_warning();
voc = voc_algorithm_.process(voc_sraw);
if (nox_sensor_) {
nox = nox_algorithm_.process(nox_sraw);
}
ESP_LOGV(TAG, "VOC = %d, NOx = %d", voc, nox);
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_);
if ((uint32_t) abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF ||
(uint32_t) abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->voc_baselines_storage_.state0 = this->voc_state0_;
this->voc_baselines_storage_.state1 = this->voc_state1_;
if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0,
voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
return true;
}
/**
* @brief Return the raw gas measurement
*
* @param temperature The measured temperature in degrees C
* @param humidity The measured relative humidity in % rH
* @return uint16_t The current raw gas measurement
*/
bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) {
float humidity = NAN;
static uint32_t nox_conditioning_start = millis();
if (!this->self_test_complete_) {
ESP_LOGD(TAG, "Self-test not yet complete");
return false;
}
if (this->humidity_sensor_ != nullptr) {
humidity = this->humidity_sensor_->state;
}
if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) {
humidity = 50;
}
float temperature = NAN;
if (this->temperature_sensor_ != nullptr) {
temperature = float(this->temperature_sensor_->state);
}
if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) {
temperature = 25;
}
uint16_t command;
uint16_t data[2];
size_t response_words;
// Use SGP40 measure command if we don't care about NOx
if (nox_sensor_ == nullptr) {
command = SGP40_CMD_MEASURE_RAW;
response_words = 1;
} else {
// SGP41 sensor must use NOx conditioning command for the first 10 seconds
if (millis() - nox_conditioning_start < 10000) {
command = SGP41_CMD_NOX_CONDITIONING;
response_words = 1;
} else {
command = SGP41_CMD_MEASURE_RAW;
response_words = 2;
}
}
uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100));
uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175);
// first paramater are the relative humidity ticks
data[0] = rhticks;
// secomd paramater are the temperature ticks
data[1] = tempticks;
if (!this->write_command(command, data, 2)) {
this->status_set_warning();
ESP_LOGD(TAG, "write error (%d)", this->last_error_);
return false;
}
delay(measure_time_);
uint16_t raw_data[2];
raw_data[1] = 0;
if (!this->read_data(raw_data, response_words)) {
this->status_set_warning();
ESP_LOGD(TAG, "read error (%d)", this->last_error_);
return false;
}
voc_raw = raw_data[0];
nox_raw = raw_data[1]; // either 0 or the measured NOx ticks
return true;
}
void SGP4xComponent::update_gas_indices() {
if (!this->self_test_complete_)
return;
this->seconds_since_last_store_ += 1;
if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) {
// Set values to UINT16_MAX to indicate failure
this->voc_index_ = this->nox_index_ = UINT16_MAX;
ESP_LOGE(TAG, "measure gas indices failed");
return;
}
if (this->samples_read_ < this->samples_to_stabilize_) {
this->samples_read_++;
ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_,
this->samples_to_stabilize_, this->voc_index_);
return;
}
}
void SGP4xComponent::update() {
if (this->samples_read_ < this->samples_to_stabilize_) {
return;
}
if (this->voc_sensor_) {
if (this->voc_index_ != UINT16_MAX) {
this->status_clear_warning();
this->voc_sensor_->publish_state(this->voc_index_);
} else {
this->status_set_warning();
}
}
if (this->nox_sensor_) {
if (this->nox_index_ != UINT16_MAX) {
this->status_clear_warning();
this->nox_sensor_->publish_state(this->nox_index_);
} else {
this->status_set_warning();
}
}
}
void SGP4xComponent::dump_config() {
ESP_LOGCONFIG(TAG, "SGP4x:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
break;
case SERIAL_NUMBER_IDENTIFICATION_FAILED:
ESP_LOGW(TAG, "Get Serial number failed.");
break;
case SELF_TEST_FAILED:
ESP_LOGW(TAG, "Self test failed.");
break;
default:
ESP_LOGW(TAG, "Unknown setup error!");
break;
}
} else {
ESP_LOGCONFIG(TAG, " Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40");
ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_);
ESP_LOGCONFIG(TAG, " Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT);
}
LOG_UPDATE_INTERVAL(this);
if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " Compensation:");
LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_);
} else {
ESP_LOGCONFIG(TAG, " Compensation: No source configured");
}
LOG_SENSOR(" ", "VOC", this->voc_sensor_);
LOG_SENSOR(" ", "NOx", this->nox_sensor_);
}
} // namespace sgp4x
} // namespace esphome

View file

@ -0,0 +1,142 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
#include <VOCGasIndexAlgorithm.h>
#include <NOxGasIndexAlgorithm.h>
#include <cmath>
namespace esphome {
namespace sgp4x {
struct SGP4xBaselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
enum SgpType { SGP40, SGP41 };
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
uint16_t learning_time_gain_hours;
uint16_t gating_max_duration_minutes;
uint16_t std_initial;
uint16_t gain_factor;
};
// commands and constants
static const uint8_t SGP40_FEATURESET = 0x0020; // can measure VOC
static const uint8_t SGP41_FEATURESET = 0x0040; // can measure VOC and NOX
// Commands
static const uint16_t SGP4X_CMD_GET_SERIAL_ID = 0x3682;
static const uint16_t SGP4X_CMD_GET_FEATURESET = 0x202f;
static const uint16_t SGP4X_CMD_SELF_TEST = 0x280e;
static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F;
static const uint16_t SGP41_CMD_MEASURE_RAW = 0x2619;
static const uint16_t SGP41_CMD_NOX_CONDITIONING = 0x2612;
static const uint8_t SGP41_SUBCMD_NOX_CONDITIONING = 0x12;
// Shortest time interval of 3H for storing baseline values.
// Prevents wear of the flash because of too many write operations
const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
static const uint16_t SPG40_SELFTEST_TIME = 250; // 250 ms for self test
static const uint16_t SPG41_SELFTEST_TIME = 320; // 320 ms for self test
static const uint16_t SGP40_MEASURE_TIME = 30;
static const uint16_t SGP41_MEASURE_TIME = 55;
// Store anyway if the baseline difference exceeds the max storage diff value
const uint32_t MAXIMUM_STORAGE_DIFF = 50;
class SGP4xComponent;
/// This class implements support for the Sensirion sgp4x i2c GAS (VOC) sensors.
class SGP4xComponent : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice {
enum ErrorCode {
COMMUNICATION_FAILED,
MEASUREMENT_INIT_FAILED,
INVALID_ID,
UNSUPPORTED_ID,
SERIAL_NUMBER_IDENTIFICATION_FAILED,
SELF_TEST_FAILED,
UNKNOWN
} error_code_{UNKNOWN};
public:
// SGP4xComponent() {};
void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
void setup() override;
void update() override;
void update_gas_indices();
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
voc_tuning_params_.value().index_offset = index_offset;
voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
voc_tuning_params_.value().std_initial = std_initial;
voc_tuning_params_.value().gain_factor = gain_factor;
}
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t gain_factor) {
nox_tuning_params_.value().index_offset = index_offset;
nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
nox_tuning_params_.value().std_initial = 50;
nox_tuning_params_.value().gain_factor = gain_factor;
}
protected:
void self_test_();
/// Input sensor for humidity and temperature compensation.
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
int16_t sensirion_init_sensors_();
bool measure_gas_indices_(int32_t &voc, int32_t &nox);
bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw);
SgpType sgp_type_{SGP40};
uint64_t serial_number_;
uint16_t featureset_;
bool self_test_complete_;
uint16_t self_test_time_;
sensor::Sensor *voc_sensor_{nullptr};
VOCGasIndexAlgorithm voc_algorithm_;
optional<GasTuning> voc_tuning_params_;
int32_t voc_state0_;
int32_t voc_state1_;
int32_t voc_index_ = 0;
sensor::Sensor *nox_sensor_{nullptr};
int32_t nox_index_ = 0;
NOxGasIndexAlgorithm nox_algorithm_;
optional<GasTuning> nox_tuning_params_;
uint16_t measure_time_;
uint8_t samples_read_ = 0;
uint8_t samples_to_stabilize_ = static_cast<int8_t>(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2;
bool store_baseline_;
ESPPreferenceObject pref_;
uint32_t seconds_since_last_store_;
SGP4xBaselines voc_baselines_storage_;
};
} // namespace sgp4x
} // namespace esphome

View file

@ -39,6 +39,8 @@ lib_deps =
bblanchon/ArduinoJson@6.18.5 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git ; Sensirion Gas Index Algorithm Arduino Library
build_flags =
-DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
src_filter =

View file

@ -281,10 +281,27 @@ sensor:
window_correction_factor: 1.0
address: 0x53
update_interval: 60s
- platform: sgp40
name: 'Workshop VOC'
- platform: sgp4x
voc:
name: "VOC Index"
id: sgp40_voc_index
algorithm_tuning:
index_offset: 100
learning_time_offset_hours: 12
learning_time_gain_hours: 12
gating_max_duration_minutes: 180
std_initial: 50
gain_factor: 230
nox:
name: "NOx"
algorithm_tuning:
index_offset: 100
learning_time_offset_hours: 12
learning_time_gain_hours: 12
gating_max_duration_minutes: 180
std_initial: 50
gain_factor: 230
update_interval: 5s
store_baseline: 'true'
- platform: mcp3008
update_interval: 5s
mcp3008_id: 'mcp3008_hub'