2018-12-18 09:22:41 +00:00
|
|
|
/*
|
2019-10-27 10:13:24 +00:00
|
|
|
xsns_38_az7798.ino - AZ_Instrument 7798 CO2/temperature/humidity meter support for Tasmota
|
2018-12-18 09:22:41 +00:00
|
|
|
|
2022-12-10 15:55:04 +00:00
|
|
|
Copyright (C) 2021 adebeun
|
2018-12-18 09:22:41 +00:00
|
|
|
|
|
|
|
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_AZ7798
|
|
|
|
|
|
|
|
#define XSNS_38 38
|
|
|
|
|
|
|
|
/*********************************************************************************************\
|
|
|
|
* CO2, temperature and humidity meter and data logger
|
|
|
|
* Known by different names (brief survey 2018-12-16):
|
|
|
|
* - AZ-Instrument 7798 (http://www.az-instrument.com.tw)
|
|
|
|
* - co2meter.com AZ-0004
|
|
|
|
* - Extech CO200
|
|
|
|
* - BES CO7788 (https://www.aliexpress.com)
|
|
|
|
* - AZ CO87 (https://www.aliexpress.com)
|
|
|
|
* - no doubt there are more ...
|
|
|
|
*
|
|
|
|
* Hardware Serial will be selected if GPIO1 = [AZ Tx] and GPIO3 = [AZ Rx]
|
|
|
|
*
|
|
|
|
* Inside the meter, the serial comms wire with the red stripe goes to GPIO1.
|
|
|
|
* The other one therefore to GPIO3.
|
|
|
|
* WeMos D1 Mini is powered from the incoming 5V.
|
|
|
|
*
|
|
|
|
* This implementation was derived from xsns_15_mhz19.ino from
|
2019-10-27 10:13:24 +00:00
|
|
|
* Tasmota-6.3.0 by Arthur de Beun.
|
2018-12-18 09:22:41 +00:00
|
|
|
*
|
|
|
|
* The serial comms protocol is not publicly documented, that I could find.
|
|
|
|
* The info below was obtained by reverse-engineering.
|
|
|
|
* Port settings: 9600 8N1
|
|
|
|
* The suppied USB interface has a CP20x USB-serial bridge.
|
|
|
|
* The 3-way, 2.5mm jack has tip=RxD, middle=TxD and base=0V
|
|
|
|
* The TxD output swing is 3V3.
|
|
|
|
*
|
|
|
|
* There is never a space before the 0x0d, but the other spaces are there.
|
|
|
|
*
|
|
|
|
* serial number / ID
|
|
|
|
* request: I 0x0d
|
|
|
|
* response: i 12345678 7798V3.4 0x0d
|
|
|
|
*
|
|
|
|
* log info
|
|
|
|
* request: M 0x0d
|
|
|
|
* response: m 45 1 C 1af4 0cf4 0x0d
|
|
|
|
*
|
|
|
|
* 45 = number of records, but there are only 15 lines of 3 values each)
|
|
|
|
* 1 = sample rate in seconds
|
|
|
|
* C = celcius, F
|
|
|
|
* 1af4 0cf4 = seconds since 2000-01-01 00:00:00
|
|
|
|
*
|
|
|
|
* start time 2014-04-30 19:35:16
|
|
|
|
* end time 2014-04-30 19:35:30
|
|
|
|
*
|
|
|
|
* download log data
|
|
|
|
* request: D 0x0d
|
|
|
|
* response: m 45 1 C 1af4 0cf4 0x0d
|
|
|
|
* d 174 955 698 0x0d
|
|
|
|
* 174 = temp in [C * 10]
|
|
|
|
* 955 = CO2 [ppm]
|
|
|
|
* 698 = RH in [% * 10]
|
|
|
|
* d 174 990 694 0x0d
|
|
|
|
* ...
|
|
|
|
* d 173 929 654 0x0d
|
|
|
|
*
|
|
|
|
* 15 lines in total, 1 second apart
|
|
|
|
*
|
|
|
|
* Sync datalogger time with PC
|
|
|
|
* request: C 452295746 0x0d
|
|
|
|
* response: > 0x0d
|
|
|
|
*
|
|
|
|
* 452295746 = seconds since 2000-01-01 00:00:00
|
|
|
|
*
|
|
|
|
* Identifier:
|
|
|
|
* request: J -------- 1 0x0d
|
|
|
|
*
|
|
|
|
* the characters (dashes) in the above become the first part of the response to the I command (12345678 above)
|
|
|
|
*
|
|
|
|
* Set sample rate
|
|
|
|
* request: S 10 0x0d
|
|
|
|
* response: m 12 10 C 1af5 7be1 0x0d
|
|
|
|
*
|
|
|
|
* Other characters that seem to give a response:
|
|
|
|
* A responds with >
|
|
|
|
* so is similar to the response to C, so other characters may be required
|
|
|
|
* A is the beep alarm perhaps?
|
|
|
|
* parameters would be CO2 level and on/off, as per front panel P1.3 setting?
|
|
|
|
*
|
|
|
|
* L responds with >
|
|
|
|
* L perhaps sets the limits for the good and normal levels (P1.1 and P1.2)?
|
|
|
|
*
|
|
|
|
* Q responds with >
|
|
|
|
* Q is reset maybe (P4.1)?
|
|
|
|
*
|
|
|
|
* : responds with : T19.9C:C2167ppm:H57.4%
|
|
|
|
* This one gives the current readings.
|
2018-12-24 17:14:25 +00:00
|
|
|
\*********************************************************************************************/
|
2018-12-18 09:22:41 +00:00
|
|
|
|
|
|
|
#include <TasmotaSerial.h>
|
|
|
|
|
|
|
|
#ifndef CO2_LOW
|
|
|
|
#define CO2_LOW 800 // Below this CO2 value show green light
|
|
|
|
#endif
|
|
|
|
#ifndef CO2_HIGH
|
|
|
|
#define CO2_HIGH 1200 // Above this CO2 value show red light
|
|
|
|
#endif
|
|
|
|
|
2019-07-06 06:46:59 +01:00
|
|
|
#define AZ_READ_TIMEOUT 400 // Must be way less than 1000 but enough to read 25 bytes at 9600 bps
|
|
|
|
|
|
|
|
#define AZ_CLOCK_UPDATE_INTERVAL (24UL * 60 * 60) // periodically update clock display (24 hours)
|
|
|
|
#define AZ_EPOCH (946684800UL) // 2000-01-01 00:00:00
|
2018-12-18 09:22:41 +00:00
|
|
|
|
|
|
|
TasmotaSerial *AzSerial;
|
|
|
|
|
|
|
|
const char ktype[] = "AZ7798";
|
|
|
|
uint8_t az_type = 1;
|
|
|
|
uint16_t az_co2 = 0;
|
|
|
|
double az_temperature = 0;
|
|
|
|
double az_humidity = 0;
|
|
|
|
uint8_t az_received = 0;
|
|
|
|
uint8_t az_state = 0;
|
2019-07-06 06:46:59 +01:00
|
|
|
unsigned long az_clock_update = 10; // timer for periodically updating clock display
|
2018-12-18 09:22:41 +00:00
|
|
|
|
|
|
|
/*********************************************************************************************/
|
|
|
|
|
2018-12-18 19:14:55 +00:00
|
|
|
void AzEverySecond(void)
|
2018-12-18 09:22:41 +00:00
|
|
|
{
|
2019-07-06 06:46:59 +01:00
|
|
|
unsigned long start = millis();
|
|
|
|
|
2018-12-18 09:22:41 +00:00
|
|
|
az_state++;
|
|
|
|
if (5 == az_state) { // every 5 seconds
|
|
|
|
az_state = 0;
|
|
|
|
|
|
|
|
AzSerial->flush(); // sync reception
|
|
|
|
AzSerial->write(":\r", 2);
|
|
|
|
az_received = 0;
|
|
|
|
|
|
|
|
uint8_t az_response[32];
|
|
|
|
uint8_t counter = 0;
|
|
|
|
uint8_t i, j;
|
|
|
|
uint8_t response_substr[16];
|
|
|
|
|
|
|
|
do {
|
|
|
|
if (AzSerial->available() > 0) {
|
|
|
|
az_response[counter] = AzSerial->read();
|
|
|
|
if(az_response[counter] == 0x0d) { az_received = 1; }
|
|
|
|
counter++;
|
|
|
|
} else {
|
|
|
|
delay(5);
|
|
|
|
}
|
|
|
|
} while(((millis() - start) < AZ_READ_TIMEOUT) && (counter < sizeof(az_response)) && !az_received);
|
|
|
|
|
2019-01-17 16:48:34 +00:00
|
|
|
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, az_response, counter);
|
2018-12-18 09:22:41 +00:00
|
|
|
|
|
|
|
if (!az_received) {
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 comms timeout"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
i = 0;
|
|
|
|
while((az_response[i] != 'T') && (i < counter)) {i++;} // find the start of response
|
|
|
|
if(az_response[i] != 'T') {
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 failed to find start of response"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
i++; // advance to start of temperature value
|
|
|
|
j = 0;
|
|
|
|
// find the end of temperature
|
|
|
|
while((az_response[i] != 'C') && (az_response[i] != 'F') && (i < counter)) {
|
|
|
|
response_substr[j++] = az_response[i++];
|
|
|
|
}
|
|
|
|
if((az_response[i] != 'C') && (az_response[i] != 'F')){
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 failed to find end of temperature"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
response_substr[j] = 0; // add null terminator
|
2019-07-01 17:20:43 +01:00
|
|
|
az_temperature = CharToFloat((char*)response_substr); // units (C or F) depends on meter setting
|
2018-12-18 09:22:41 +00:00
|
|
|
if(az_response[i] == 'C') { // meter transmits in degC
|
|
|
|
az_temperature = ConvertTemp((float)az_temperature); // convert to degF, depending on settings
|
|
|
|
} else { // meter transmits in degF
|
2018-12-18 19:31:00 +00:00
|
|
|
az_temperature = ConvertTemp((az_temperature - 32) / 1.8); // convert to degC and then C or F depending on setting
|
2018-12-18 09:22:41 +00:00
|
|
|
}
|
|
|
|
i++; // advance to first delimiter
|
|
|
|
if(az_response[i] != ':') {
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 error first delimiter"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
i++; // advance to start of CO2
|
|
|
|
if(az_response[i] != 'C') {
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 error start of CO2"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
i++; // advance to start of CO2 value
|
|
|
|
j = 0;
|
|
|
|
// find the end of CO2
|
|
|
|
while((az_response[i] != 'p') && (i < counter)) {
|
|
|
|
response_substr[j++] = az_response[i++];
|
|
|
|
}
|
|
|
|
if(az_response[i] != 'p') {
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 failed to find end of CO2"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
response_substr[j] = 0; // add null terminator
|
|
|
|
az_co2 = atoi((char*)response_substr);
|
2020-04-03 17:34:02 +01:00
|
|
|
#ifdef USE_LIGHT
|
2018-12-18 09:22:41 +00:00
|
|
|
LightSetSignal(CO2_LOW, CO2_HIGH, az_co2);
|
2020-04-03 17:34:02 +01:00
|
|
|
#endif // USE_LIGHT
|
2018-12-18 09:22:41 +00:00
|
|
|
i += 3; // advance to second delimiter
|
|
|
|
if(az_response[i] != ':') {
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 error second delimiter"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
i++; // advance to start of humidity
|
|
|
|
if(az_response[i] != 'H') {
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 error start of humidity"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
i++; // advance to start of humidity value
|
|
|
|
j = 0;
|
|
|
|
// find the end of humidity
|
|
|
|
while((az_response[i] != '%') && (i < counter)) {
|
|
|
|
response_substr[j++] = az_response[i++];
|
|
|
|
}
|
|
|
|
if(az_response[i] != '%') {
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 failed to find end of humidity"));
|
2018-12-18 09:22:41 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
response_substr[j] = 0; // add null terminator
|
2019-07-01 17:20:43 +01:00
|
|
|
az_humidity = ConvertHumidity(CharToFloat((char*)response_substr));
|
2018-12-18 09:22:41 +00:00
|
|
|
}
|
2019-07-06 06:46:59 +01:00
|
|
|
|
|
|
|
// update the clock from network time
|
2019-08-17 15:49:17 +01:00
|
|
|
if ((az_clock_update == 0) && (LocalTime() > AZ_EPOCH)) {
|
2019-07-06 06:46:59 +01:00
|
|
|
char tmpString[16];
|
2019-08-17 15:49:17 +01:00
|
|
|
sprintf(tmpString, "C %d\r", (int)(LocalTime() - AZ_EPOCH));
|
2019-07-06 06:46:59 +01:00
|
|
|
AzSerial->write(tmpString);
|
|
|
|
// discard the response
|
|
|
|
do {
|
|
|
|
if (AzSerial->available() > 0) {
|
|
|
|
if(AzSerial->read() == 0x0d) { break; }
|
|
|
|
} else {
|
|
|
|
delay(5);
|
|
|
|
}
|
|
|
|
} while(((millis() - start) < AZ_READ_TIMEOUT));
|
|
|
|
az_clock_update = AZ_CLOCK_UPDATE_INTERVAL;
|
2021-01-23 16:10:06 +00:00
|
|
|
AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 clock updated"));
|
2019-07-06 06:46:59 +01:00
|
|
|
} else {
|
|
|
|
az_clock_update--;
|
|
|
|
}
|
2018-12-18 09:22:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************************************/
|
|
|
|
|
2018-12-18 19:14:55 +00:00
|
|
|
void AzInit(void)
|
2018-12-18 09:22:41 +00:00
|
|
|
{
|
|
|
|
az_type = 0;
|
2020-04-27 15:47:29 +01:00
|
|
|
if (PinUsed(GPIO_AZ_RXD) && PinUsed(GPIO_AZ_TXD)) {
|
|
|
|
AzSerial = new TasmotaSerial(Pin(GPIO_AZ_RXD), Pin(GPIO_AZ_TXD), 1);
|
2018-12-18 09:22:41 +00:00
|
|
|
if (AzSerial->begin(9600)) {
|
|
|
|
if (AzSerial->hardwareSerial()) { ClaimSerial(); }
|
|
|
|
az_type = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-28 13:08:33 +00:00
|
|
|
void AzShow(bool json)
|
2018-12-18 09:22:41 +00:00
|
|
|
{
|
|
|
|
if (json) {
|
2020-03-17 15:29:59 +00:00
|
|
|
ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_CO2 "\":%d,"), ktype, az_co2);
|
|
|
|
ResponseAppendTHD(az_temperature, az_humidity);
|
|
|
|
ResponseJsonEnd();
|
2018-12-18 09:22:41 +00:00
|
|
|
#ifdef USE_DOMOTICZ
|
2020-10-29 12:37:09 +00:00
|
|
|
if (0 == TasmotaGlobal.tele_period) DomoticzSensor(DZ_AIRQUALITY, az_co2);
|
2018-12-18 09:22:41 +00:00
|
|
|
#endif // USE_DOMOTICZ
|
|
|
|
#ifdef USE_WEBSERVER
|
|
|
|
} else {
|
2019-03-19 16:31:43 +00:00
|
|
|
WSContentSend_PD(HTTP_SNS_CO2, ktype, az_co2);
|
2020-03-17 15:29:59 +00:00
|
|
|
WSContentSend_THD(ktype, az_temperature, az_humidity);
|
2018-12-18 09:22:41 +00:00
|
|
|
#endif // USE_WEBSERVER
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************************************\
|
|
|
|
* Interface
|
|
|
|
\*********************************************************************************************/
|
|
|
|
|
2022-11-11 09:44:56 +00:00
|
|
|
bool Xsns38(uint32_t function)
|
2018-12-18 09:22:41 +00:00
|
|
|
{
|
2019-01-28 13:08:33 +00:00
|
|
|
bool result = false;
|
2018-12-18 09:22:41 +00:00
|
|
|
|
|
|
|
if(az_type){
|
|
|
|
switch (function) {
|
|
|
|
case FUNC_INIT:
|
|
|
|
AzInit();
|
|
|
|
break;
|
|
|
|
case FUNC_EVERY_SECOND:
|
|
|
|
AzEverySecond();
|
|
|
|
break;
|
|
|
|
case FUNC_JSON_APPEND:
|
|
|
|
AzShow(1);
|
|
|
|
break;
|
|
|
|
#ifdef USE_WEBSERVER
|
2019-03-19 16:31:43 +00:00
|
|
|
case FUNC_WEB_SENSOR:
|
2018-12-18 09:22:41 +00:00
|
|
|
AzShow(0);
|
|
|
|
break;
|
|
|
|
#endif // USE_WEBSERVER
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // USE_AZ7798
|