mirror of https://github.com/arendst/Tasmota.git
340 lines
10 KiB
Arduino
340 lines
10 KiB
Arduino
|
/*
|
||
|
xsns_31_ccs811.ino - CCS811 gas and air quality sensor support for Tasmota
|
||
|
|
||
|
Copyright (C) 2021 Gerhard Mutz and Theo Arends
|
||
|
|
||
|
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_CCS811_V2
|
||
|
/*********************************************************************************************\
|
||
|
* CCS811 - Gas (TVOC - Total Volatile Organic Compounds) and Air Quality (CO2)
|
||
|
*
|
||
|
* Source: Adafruit
|
||
|
*
|
||
|
* This driver supports one to two devices at a time at
|
||
|
* addressses 0x5A or/and 0x5B
|
||
|
* - for I2C address 0x5A, connect ADDR to GND
|
||
|
* - for I2C address 0x5B, connect ADDR to VCC
|
||
|
* NOTE:
|
||
|
* - Wake must be connected to GND (no sleep mode supported!)
|
||
|
* - depending on the breakout board, SDA & SCL may require
|
||
|
* pull-ups to VCC, e.g. 4k7R
|
||
|
*
|
||
|
\*********************************************************************************************/
|
||
|
|
||
|
#define XSNS_31 31
|
||
|
#define XI2C_24 24 // See I2CDEVICES.md
|
||
|
|
||
|
#define EVERYNSECONDS 5
|
||
|
#define RESETCOUNT 6
|
||
|
|
||
|
#include "Adafruit_CCS811.h"
|
||
|
|
||
|
uint8_t CCS811_addresses[] = { CCS811_ADDRESS, (CCS811_ADDRESS + 1) };
|
||
|
#define MAXDEVICECOUNT (sizeof( CCS811_addresses) / sizeof(uint8_t))
|
||
|
|
||
|
typedef struct {
|
||
|
uint8_t address;
|
||
|
uint8_t device_found;
|
||
|
uint8_t device_index;
|
||
|
uint8_t device_ready;
|
||
|
Adafruit_CCS811 ccsinstance;
|
||
|
uint16_t eCO2;
|
||
|
uint16_t TVOC;
|
||
|
uint8_t refresh_count;
|
||
|
uint8_t reset_count;
|
||
|
} CCS811DATA;
|
||
|
|
||
|
uint8_t CCS811_devices_found = 0;
|
||
|
CCS811DATA ccsd[ MAXDEVICECOUNT];
|
||
|
CCS811DATA * pccsd;
|
||
|
uint32_t i;
|
||
|
|
||
|
#define D_PRFX_CCS811 "CCS811"
|
||
|
#define D_CMND_HWVERSION "HW"
|
||
|
#define D_CMND_FWAPPVERSION "FWApp"
|
||
|
#define D_CMND_BASELINE "Baseline"
|
||
|
|
||
|
const char kCCS811Commands[] PROGMEM = D_PRFX_CCS811 "|" // Prefix
|
||
|
D_CMND_HWVERSION "|"
|
||
|
D_CMND_FWAPPVERSION "|"
|
||
|
D_CMND_BASELINE;
|
||
|
|
||
|
void (* const CCS811Command[])(void) PROGMEM = {
|
||
|
&CmndCCS811HwVersion,
|
||
|
&CmndCCS811FwAppVersion,
|
||
|
&CmndCCS811Baseline
|
||
|
};
|
||
|
|
||
|
/********************************************************************************************/
|
||
|
|
||
|
void CCS811Detect(void)
|
||
|
{
|
||
|
if (!CCS811_devices_found) {
|
||
|
memset( ccsd, 0, sizeof( ccsd));
|
||
|
}
|
||
|
int active_index = 1;
|
||
|
for (i = 0, pccsd = ccsd; i < MAXDEVICECOUNT; i++, pccsd++) {
|
||
|
pccsd->address = CCS811_addresses[ i];
|
||
|
if (I2cActive( pccsd->address)) { continue; }
|
||
|
if (!pccsd->ccsinstance.begin(pccsd->address)) {
|
||
|
pccsd->device_found = 1;
|
||
|
CCS811_devices_found += 1;
|
||
|
I2cSetActiveFound( pccsd->address, "CCS811");
|
||
|
pccsd->device_index = active_index;
|
||
|
active_index++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void CCS811Update(void) // Perform every n second
|
||
|
{
|
||
|
for (i = 0, pccsd = ccsd; i < MAXDEVICECOUNT; i++, pccsd++) {
|
||
|
|
||
|
if (!pccsd->device_found)
|
||
|
continue;
|
||
|
|
||
|
pccsd->refresh_count++;
|
||
|
if (pccsd->refresh_count >= EVERYNSECONDS) {
|
||
|
pccsd->refresh_count = 0;
|
||
|
pccsd->device_ready = 0;
|
||
|
if (pccsd->ccsinstance.available()) {
|
||
|
if (!pccsd->ccsinstance.readData()){
|
||
|
pccsd->TVOC = pccsd->ccsinstance.getTVOC();
|
||
|
pccsd->eCO2 = pccsd->ccsinstance.geteCO2();
|
||
|
pccsd->device_ready = 1;
|
||
|
if ((TasmotaGlobal.global_update) &&
|
||
|
(TasmotaGlobal.humidity > 0) &&
|
||
|
(!isnan(TasmotaGlobal.temperature_celsius))) {
|
||
|
pccsd->ccsinstance.setEnvironmentalData((uint8_t)TasmotaGlobal.humidity,
|
||
|
TasmotaGlobal.temperature_celsius);
|
||
|
}
|
||
|
pccsd->reset_count = 0;
|
||
|
}
|
||
|
} else {
|
||
|
// failed, count up
|
||
|
pccsd->reset_count++;
|
||
|
if (pccsd->reset_count > RESETCOUNT) {
|
||
|
// after 30 seconds, restart
|
||
|
pccsd->ccsinstance.begin( pccsd->address);
|
||
|
pccsd->reset_count = 0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// no methods available in Adafruit library to read version data or
|
||
|
// read/set the baseline value, so we need to emulate the private methods
|
||
|
|
||
|
void CCS811ReadMailboxValue( uint8_t address, uint8_t mailbox, byte * pbuf, uint8_t buflen)
|
||
|
{
|
||
|
Wire.beginTransmission(address);
|
||
|
Wire.write(mailbox);
|
||
|
Wire.endTransmission();
|
||
|
Wire.requestFrom(address, buflen);
|
||
|
for (uint8_t i = 0; i < buflen; i++) {
|
||
|
*(pbuf + i) = Wire.read();
|
||
|
#ifdef CCS811_DEBUG
|
||
|
AddLog_P(LOG_LEVEL_DEBUG, PSTR( D_LOG_DEBUG D_PRFX_CCS811 " reading byte %u: 0%02x / %u"), i, *(pbuf + i), *(pbuf + i));
|
||
|
#endif
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void CCS811WriteMailboxValue(uint8_t address, uint8_t mailbox, byte * pbuf, uint8_t buflen)
|
||
|
{
|
||
|
#ifdef CCS811_DEBUG
|
||
|
for (uint8_t i = 0; i < buflen; i++) {
|
||
|
AddLog_P(LOG_LEVEL_DEBUG, PSTR( D_LOG_DEBUG D_PRFX_CCS811 " writing byte %u: 0%02x / %u"), i, *(pbuf + i), *(pbuf + i));
|
||
|
}
|
||
|
#endif
|
||
|
Wire.beginTransmission(address);
|
||
|
Wire.write((uint8_t)mailbox);
|
||
|
Wire.write(pbuf, buflen);
|
||
|
Wire.endTransmission();
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************\
|
||
|
* Command Sensor31
|
||
|
\*********************************************************************************************/
|
||
|
|
||
|
CCS811DATA * CmndCCS811SelectDeviceFromIndex(void) {
|
||
|
CCS811DATA * pccsd_command = NULL;
|
||
|
if (XdrvMailbox.index <= CCS811_devices_found) {
|
||
|
// select device data matching the index
|
||
|
for (i = 0, pccsd = ccsd; i < MAXDEVICECOUNT; i++, pccsd++) {
|
||
|
if (pccsd->device_index == XdrvMailbox.index) {
|
||
|
pccsd_command = pccsd;
|
||
|
#ifdef CCS811_DEBUG
|
||
|
AddLog_P(LOG_LEVEL_DEBUG, PSTR( D_LOG_DEBUG D_PRFX_CCS811 " I2C Address: 0%02x"), pccsd_command->address);
|
||
|
#endif
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return pccsd_command;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CmndCCS811HwVersion(void) {
|
||
|
CCS811DATA * pccsd = CmndCCS811SelectDeviceFromIndex();
|
||
|
if (pccsd) {
|
||
|
byte CCS811_hw_version;
|
||
|
CCS811ReadMailboxValue( pccsd->address,
|
||
|
CCS811_HW_VERSION,
|
||
|
&CCS811_hw_version,
|
||
|
sizeof(CCS811_hw_version));
|
||
|
ResponseCmndIdxNumber(CCS811_hw_version);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void CmndCCS811FwAppVersion(void) {
|
||
|
pccsd = CmndCCS811SelectDeviceFromIndex();
|
||
|
if (pccsd) {
|
||
|
byte bCCS811_fw_app_version[2];
|
||
|
char CCS811_fw_app_version[16];
|
||
|
CCS811ReadMailboxValue( pccsd->address,
|
||
|
CCS811_FW_APP_VERSION,
|
||
|
bCCS811_fw_app_version,
|
||
|
(sizeof(bCCS811_fw_app_version) / sizeof(byte)));
|
||
|
sprintf( CCS811_fw_app_version,
|
||
|
PSTR( "%x.%x.%x"),
|
||
|
(bCCS811_fw_app_version[0] >> 4), // major
|
||
|
(bCCS811_fw_app_version[0] & 0xF), // minor
|
||
|
bCCS811_fw_app_version[1]); // build
|
||
|
|
||
|
ResponseCmndIdxChar(CCS811_fw_app_version);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void CmndCCS811Baseline(void) {
|
||
|
pccsd = CmndCCS811SelectDeviceFromIndex();
|
||
|
if (pccsd) {
|
||
|
byte CCS811_baseline[2];
|
||
|
if (XdrvMailbox.data_len > 0) {
|
||
|
CCS811_baseline[0] = (XdrvMailbox.payload & 0xFF00) >> 8;
|
||
|
CCS811_baseline[1] = XdrvMailbox.payload & 0xFF;
|
||
|
CCS811WriteMailboxValue( pccsd->address,
|
||
|
CCS811_BASELINE,
|
||
|
CCS811_baseline,
|
||
|
(sizeof(CCS811_baseline) / sizeof(byte)));
|
||
|
} else {
|
||
|
CCS811ReadMailboxValue( pccsd->address,
|
||
|
CCS811_BASELINE,
|
||
|
CCS811_baseline,
|
||
|
(sizeof(CCS811_baseline) / sizeof(byte)));
|
||
|
}
|
||
|
ResponseCmndIdxNumber(((CCS811_baseline[0] << 8) + CCS811_baseline[1]));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// -----------------------------------------------------------------------------
|
||
|
|
||
|
const char HTTP_SNS_CCS811[] PROGMEM =
|
||
|
"{s}%s " D_ECO2 "{m}%d " D_UNIT_PARTS_PER_MILLION "{e}" // {s} = <tr><th>, {m} = </th><td>, {e} = </td></tr>
|
||
|
"{s}%s " D_TVOC "{m}%d " D_UNIT_PARTS_PER_BILLION "{e}";
|
||
|
|
||
|
const char * devicenamelist[] PROGMEM = { "CCS811", "CCS811_1", "CCS811_2" };
|
||
|
|
||
|
void CCS811Show(bool json)
|
||
|
{
|
||
|
uint8_t ready_count = 0;
|
||
|
for (i = 0, pccsd = ccsd; i < MAXDEVICECOUNT; i++, pccsd++) {
|
||
|
if ((pccsd->device_found) && (pccsd->device_ready)) {
|
||
|
ready_count += 1;
|
||
|
}
|
||
|
}
|
||
|
if (!ready_count) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// in upcoming loops use either one device name
|
||
|
// with no index or or two names with index
|
||
|
const char ** pdevicename;
|
||
|
const char ** pdevicename_first = devicenamelist;
|
||
|
if (ready_count > 1) {
|
||
|
pdevicename_first++;
|
||
|
}
|
||
|
|
||
|
if (json) {
|
||
|
for (i = 0, pccsd = ccsd, pdevicename = pdevicename_first; i < MAXDEVICECOUNT; i++, pccsd++) {
|
||
|
if (pccsd->device_ready) {
|
||
|
ResponseAppend_P( PSTR(",\"%s\":{\"" D_JSON_ECO2 "\":%d,\"" D_JSON_TVOC "\":%d}"),
|
||
|
*pdevicename,
|
||
|
pccsd->eCO2,
|
||
|
pccsd->TVOC);
|
||
|
pdevicename++;
|
||
|
}
|
||
|
}
|
||
|
#ifdef USE_DOMOTICZ
|
||
|
if (0 == TasmotaGlobal.tele_period) {
|
||
|
if (pccsd->device_ready) {
|
||
|
pccsd = ccsd;
|
||
|
DomoticzSensor(DZ_AIRQUALITY, pccsd->eCO2);
|
||
|
}
|
||
|
}
|
||
|
#endif // USE_DOMOTICZ
|
||
|
#ifdef USE_WEBSERVER
|
||
|
} else {
|
||
|
for (i = 0, pccsd = ccsd, pdevicename = pdevicename_first; i < MAXDEVICECOUNT; i++, pccsd++) {
|
||
|
if (pccsd->device_ready) {
|
||
|
WSContentSend_PD( HTTP_SNS_CCS811,
|
||
|
*pdevicename, pccsd->eCO2,
|
||
|
*pdevicename, pccsd->TVOC);
|
||
|
pdevicename++;
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*********************************************************************************************\
|
||
|
* Interface
|
||
|
\*********************************************************************************************/
|
||
|
|
||
|
bool Xsns31(uint8_t function)
|
||
|
{
|
||
|
if (!I2cEnabled(XI2C_24)) { return false; }
|
||
|
|
||
|
bool result = false;
|
||
|
|
||
|
if (FUNC_INIT == function) {
|
||
|
CCS811Detect();
|
||
|
}
|
||
|
else if (CCS811_devices_found) {
|
||
|
switch (function) {
|
||
|
case FUNC_EVERY_SECOND:
|
||
|
CCS811Update();
|
||
|
break;
|
||
|
case FUNC_COMMAND:
|
||
|
result = DecodeCommand( kCCS811Commands, CCS811Command);
|
||
|
break;
|
||
|
case FUNC_JSON_APPEND:
|
||
|
CCS811Show(1);
|
||
|
break;
|
||
|
#ifdef USE_WEBSERVER
|
||
|
case FUNC_WEB_SENSOR:
|
||
|
CCS811Show(0);
|
||
|
break;
|
||
|
#endif // USE_WEBSERVER
|
||
|
}
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
#endif // USE_CCS811_V2
|
||
|
#endif // USE_I2C
|