Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions I2CDEVICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,6 @@ Index | Define | Driver | Device | Address(es) | Bus2 | Descrip
92 | USE_PCF85063 | xdrv_56 | PCF85063 | 0x51 | | PCF85063 Real time clock
93 | USE_AS33772S | xdrv_119 | AS33772S | 0x52 | Yes | AS33772S USB PD Sink Controller
94 | USE_RV3028 | xdrv_56 | RV3028 | 0x52 | Yes | RV-3028-C7 RTC Controller
95 | USE_AGS02MA | xsns_118 | AGS02MA | 0x1A | | TVOC Gas sensor

NOTE: Bus2 supported on ESP32 only.
1 change: 1 addition & 0 deletions tasmota/my_user_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@
// #define MAX_DT_VARS 16 // Defaults to 7
// #define USE_GRAPH // Enable line charts with displays
// #define NUM_GRAPHS 4 // Max 16
#define USE_AGS02MA // [I2cDriver 93] Enable AGS02MA Air Quality Sensor (I2C address 0x1A)

#endif // USE_I2C

Copy link
Contributor

@sfromis sfromis Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that the new sensor driver should not by default be included in builds, hence the #define should be as a comment line, allowing the user to pick it in a custom build. Looks like you should have "I2CDriver95" instead of "I2CDriver 93" - same number as elsewhere, and no blank.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made the changes mentioned:
Updated the number to I2CDriver 95
Commented the #define line

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks better, but for consistency, I also mentioned the detail that it should be "I2cDriver95" with no blank. This is related to it being a command (where no space is allowed before the driver number), and also to make keyword searches easy. All the other drivers in the file use this syntax without inserting a space.

BTW. It would also be better to insert the line right after all the other [I2cDriverxx] lines in the file, instead of at the end of the whole USE_I2C block.
To align here, the line should start with "// " only two blanks instead of 4. This number of blanks is to enhance readability of the structure, where 4 blanks is used to nest one symbol with others in the same block, and 2 blanks with no nesting.

In both cases, the // can be overwritten with blanks when a feature is selected, like when default features already have 4 blanks before the #define, or 6 blanks if nested under a dependency symbol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the the code as mentioned

Expand Down
199 changes: 199 additions & 0 deletions tasmota/tasmota_xsns_sensor/xsns_118_ags02ma.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
xsns_118_ags02ma.ino - AGS02MA TVOC sensor support for Tasmota

Copyright (C) 2025 Akshaylal S

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_AGS02MA
/*********************************************************************************************\
* AGS02MA - TVOC (Total Volatile Organic Compounds) Sensor
*
* Source: RobTillaart/AGS02MA library
* Adaption for TASMOTA: Akshaylal S
*
* I2C Address: 0x1A
\*********************************************************************************************/

#define XSNS_118 118
#define XI2C_95 95 // See I2CDEVICES.md

#define AGS02MA_ADDRESS 0x1A

#include "AGS02MA.h"

enum AGS02MA_State {
STATE_AGS02MA_START,
STATE_AGS02MA_INIT,
STATE_AGS02MA_HEATING,
STATE_AGS02MA_NORMAL,
STATE_AGS02MA_FAIL,
};

AGS02MA ags02ma(AGS02MA_ADDRESS);
AGS02MA_State ags02ma_state = STATE_AGS02MA_START;

bool ags02ma_init = false;
bool ags02ma_read_pend = false;

uint32_t ags02ma_ppb_value = 0;
uint16_t heating_counter = 0; // Counter for heating period (120 seconds / 24 checks = 5 seconds per check)

/********************************************************************************************/

void Ags02maInit(void)
{
if (!I2cSetDevice(AGS02MA_ADDRESS)) { return; }

ags02ma.begin(I2cGetWire());

bool b = ags02ma.begin();
if (!b) {
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Sensor not found or initialization failed"));
ags02ma_state = STATE_AGS02MA_FAIL;
return;
}

uint8_t version = ags02ma.getSensorVersion();
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Sensor version 0x%02X"), version);

// Set PPB mode
b = ags02ma.setPPBMode();
if (!b) {
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Failed to set PPB mode"));
ags02ma_state = STATE_AGS02MA_FAIL;
return;
}

uint8_t mode = ags02ma.getMode();
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Mode set to %d"), mode);

I2cSetActiveFound(AGS02MA_ADDRESS, "AGS02MA", XI2C_95);

ags02ma_init = true;
ags02ma_state = STATE_AGS02MA_HEATING;
heating_counter = 0;

AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Starting warm-up period (120 seconds)"));
}

void Ags02maUpdate(void)
{
if (ags02ma_state == STATE_AGS02MA_FAIL) {
AddLog(LOG_LEVEL_DEBUG, PSTR("AGS02MA: In FAIL state"));
return;
}

// Handle heating period
if (ags02ma_state == STATE_AGS02MA_HEATING) {
// Check every 5 seconds (called from FUNC_EVERY_SECOND, so count to 5)
if (heating_counter % 5 == 0) {
if (ags02ma.isHeated()) {
AddLog(LOG_LEVEL_INFO, PSTR("AGS02MA: Warm-up complete, sensor ready"));
ags02ma_state = STATE_AGS02MA_NORMAL;
heating_counter = 0;
return;
}
}
heating_counter++;

// Log progress every 24 seconds (approximately)
if (heating_counter % 24 == 0) {
AddLog(LOG_LEVEL_DEBUG, PSTR("AGS02MA: Warming up... %d seconds elapsed"), heating_counter);
}
return;
}

// Normal operation - read sensor value
if (ags02ma_state == STATE_AGS02MA_NORMAL) {
ags02ma_ppb_value = ags02ma.readPPB();

uint8_t status = ags02ma.lastStatus();
uint8_t error = ags02ma.lastError();

if (error != 0) {
AddLog(LOG_LEVEL_DEBUG, PSTR("AGS02MA: Read error - Status: 0x%02X, Error: 0x%02X"), status, error);
} else {
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("AGS02MA: PPB value: %d"), ags02ma_ppb_value);
}
}
}

#ifdef USE_WEBSERVER
const char HTTP_SNS_AGS02MA[] PROGMEM =
"{s}AGS02MA " D_TVOC "{m}%d " D_UNIT_PARTS_PER_BILLION "{e}"; // {s} = <tr><th>, {m} = </th><td>, {e} = </td></tr>
#endif

void Ags02maShow(bool json)
{
if (ags02ma_state == STATE_AGS02MA_NORMAL) {
if (json) {
ResponseAppend_P(PSTR(",\"AGS02MA\":{\"" D_JSON_TVOC "\":%d,\"" D_JSON_TVOC_PPB "\":%d}"),
ags02ma_ppb_value, ags02ma_ppb_value);
#ifdef USE_DOMOTICZ
if (0 == TasmotaGlobal.tele_period) {
DomoticzSensor(DZ_AIRQUALITY, ags02ma_ppb_value);
}
#endif // USE_DOMOTICZ
#ifdef USE_WEBSERVER
} else {
WSContentSend_PD(HTTP_SNS_AGS02MA, ags02ma_ppb_value);
#endif
}
} else if (ags02ma_state == STATE_AGS02MA_HEATING) {
if (json) {
ResponseAppend_P(PSTR(",\"AGS02MA\":{\"Status\":\"Heating\"}"));
#ifdef USE_WEBSERVER
} else {
WSContentSend_PD(PSTR("{s}AGS02MA Status{m}Warming up...{e}"));
#endif
}
}
}

/*********************************************************************************************\
* Interface
\*********************************************************************************************/

bool Xsns118(uint32_t function)
{
if (!I2cEnabled(XI2C_95)) { return false; }

bool result = false;

if (FUNC_INIT == function) {
Ags02maInit();
}
else if (ags02ma_init) {
switch (function) {
case FUNC_EVERY_SECOND:
Ags02maUpdate();
break;
case FUNC_JSON_APPEND:
Ags02maShow(1);
break;
#ifdef USE_WEBSERVER
case FUNC_WEB_SENSOR:
Ags02maShow(0);
break;
#endif // USE_WEBSERVER
}
}
return result;
}

#endif // USE_AGS02MA
#endif // USE_I2C