2017-12-05 16:46:26 +00:00
|
|
|
/*
|
2017-12-22 13:55:24 +00:00
|
|
|
xsns_15_mhz19.ino - MH-Z19(B) CO2 sensor support for Sonoff-Tasmota
|
2017-12-05 16:46:26 +00:00
|
|
|
|
2017-12-19 11:14:41 +00:00
|
|
|
Copyright (C) 2018 Theo Arends
|
2017-12-05 16:46:26 +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/>.
|
|
|
|
*/
|
|
|
|
|
2017-12-19 11:14:41 +00:00
|
|
|
#ifdef USE_MHZ19
|
2017-12-05 16:46:26 +00:00
|
|
|
/*********************************************************************************************\
|
|
|
|
* MH-Z19 - CO2 sensor
|
|
|
|
*
|
2018-05-08 16:07:55 +01:00
|
|
|
* Adapted from EspEasy plugin P049 by Dmitry (rel22 ___ inbox.ru)
|
2018-05-10 16:21:26 +01:00
|
|
|
*
|
|
|
|
* Hardware Serial will be selected if GPIO1 = [MHZ Rx] and GPIO3 = [MHZ Tx]
|
2017-12-05 16:46:26 +00:00
|
|
|
**********************************************************************************************
|
|
|
|
* Filter usage
|
|
|
|
*
|
|
|
|
* Select filter usage on low stability readings
|
|
|
|
\*********************************************************************************************/
|
|
|
|
|
2018-11-06 16:33:51 +00:00
|
|
|
#define XSNS_15 15
|
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
enum MhzFilterOptions {MHZ19_FILTER_OFF, MHZ19_FILTER_OFF_ALLSAMPLES, MHZ19_FILTER_FAST, MHZ19_FILTER_MEDIUM, MHZ19_FILTER_SLOW};
|
2017-12-05 16:46:26 +00:00
|
|
|
|
|
|
|
#define MHZ19_FILTER_OPTION MHZ19_FILTER_FAST
|
|
|
|
|
|
|
|
/*********************************************************************************************\
|
|
|
|
* Source: http://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf
|
|
|
|
*
|
|
|
|
* Automatic Baseline Correction (ABC logic function)
|
|
|
|
*
|
|
|
|
* ABC logic function refers to that sensor itself do zero point judgment and automatic calibration procedure
|
|
|
|
* intelligently after a continuous operation period. The automatic calibration cycle is every 24 hours after powered on.
|
|
|
|
*
|
|
|
|
* The zero point of automatic calibration is 400ppm.
|
|
|
|
*
|
|
|
|
* This function is usually suitable for indoor air quality monitor such as offices, schools and homes,
|
|
|
|
* not suitable for greenhouse, farm and refrigeratory where this function should be off.
|
|
|
|
*
|
|
|
|
* Please do zero calibration timely, such as manual or commend calibration.
|
|
|
|
\*********************************************************************************************/
|
|
|
|
|
|
|
|
#define MHZ19_ABC_ENABLE 1 // Automatic Baseline Correction (0 = off, 1 = on (default))
|
|
|
|
|
|
|
|
/*********************************************************************************************/
|
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
#include <TasmotaSerial.h>
|
2017-12-08 13:14:10 +00:00
|
|
|
|
2017-12-27 13:10:55 +00:00
|
|
|
#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
|
2017-12-08 13:14:10 +00:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
#define MHZ19_READ_TIMEOUT 400 // Must be way less than 1000 but enough to read 9 bytes at 9600 bps
|
2017-12-25 16:41:12 +00:00
|
|
|
#define MHZ19_RETRY_COUNT 8
|
2017-12-08 13:14:10 +00:00
|
|
|
|
2017-12-27 13:10:55 +00:00
|
|
|
TasmotaSerial *MhzSerial;
|
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
const char kMhzTypes[] PROGMEM = "MHZ19|MHZ19B";
|
2017-12-08 13:14:10 +00:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
enum MhzCommands { MHZ_CMND_READPPM, MHZ_CMND_ABCENABLE, MHZ_CMND_ABCDISABLE, MHZ_CMND_ZEROPOINT, MHZ_CMND_RESET };
|
|
|
|
const uint8_t kMhzCommands[][2] PROGMEM = {
|
|
|
|
{0x86,0x00}, // mhz_cmnd_read_ppm
|
|
|
|
{0x79,0xA0}, // mhz_cmnd_abc_enable
|
|
|
|
{0x79,0x00}, // mhz_cmnd_abc_disable
|
|
|
|
{0x87,0x00}, // mhz_cmnd_zeropoint
|
|
|
|
{0x8D,0x00}}; // mhz_cmnd_reset
|
2017-12-08 13:14:10 +00:00
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
uint8_t mhz_type = 1;
|
|
|
|
uint16_t mhz_last_ppm = 0;
|
|
|
|
uint8_t mhz_filter = MHZ19_FILTER_OPTION;
|
|
|
|
bool mhz_abc_enable = MHZ19_ABC_ENABLE;
|
|
|
|
bool mhz_abc_must_apply = false;
|
|
|
|
char mhz_types[7];
|
2017-12-08 13:14:10 +00:00
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
float mhz_temperature = 0;
|
|
|
|
uint8_t mhz_retry = MHZ19_RETRY_COUNT;
|
2018-05-08 16:07:55 +01:00
|
|
|
uint8_t mhz_received = 0;
|
2018-05-08 16:05:49 +01:00
|
|
|
uint8_t mhz_state = 0;
|
2018-05-08 16:04:35 +01:00
|
|
|
|
|
|
|
/*********************************************************************************************/
|
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
byte MhzCalculateChecksum(byte *array)
|
|
|
|
{
|
|
|
|
byte checksum = 0;
|
|
|
|
for (byte i = 1; i < 8; i++) {
|
|
|
|
checksum += array[i];
|
|
|
|
}
|
|
|
|
checksum = 255 - checksum;
|
|
|
|
return (checksum +1);
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t MhzSendCmd(byte command_id)
|
|
|
|
{
|
|
|
|
uint8_t mhz_send[9] = { 0 };
|
|
|
|
|
|
|
|
mhz_send[0] = 0xFF; // Start byte, fixed
|
|
|
|
mhz_send[1] = 0x01; // Sensor number, 0x01 by default
|
|
|
|
memcpy_P(&mhz_send[2], kMhzCommands[command_id], sizeof(kMhzCommands[0]));
|
|
|
|
/*
|
|
|
|
mhz_send[4] = 0x00;
|
|
|
|
mhz_send[5] = 0x00;
|
|
|
|
mhz_send[6] = 0x00;
|
|
|
|
mhz_send[7] = 0x00;
|
|
|
|
*/
|
|
|
|
mhz_send[8] = MhzCalculateChecksum(mhz_send);
|
2018-05-09 12:43:22 +01:00
|
|
|
|
2018-05-10 16:21:26 +01:00
|
|
|
return MhzSerial->write(mhz_send, sizeof(mhz_send));
|
2018-05-08 16:07:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************************************/
|
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
bool MhzCheckAndApplyFilter(uint16_t ppm, uint8_t s)
|
2017-12-05 16:46:26 +00:00
|
|
|
{
|
|
|
|
if (1 == s) {
|
|
|
|
return false; // S==1 => "A" version sensor bootup, do not use values.
|
|
|
|
}
|
2017-12-25 16:41:12 +00:00
|
|
|
if (mhz_last_ppm < 400 || mhz_last_ppm > 5000) {
|
2017-12-05 16:46:26 +00:00
|
|
|
// Prevent unrealistic values during start-up with filtering enabled.
|
|
|
|
// Just assume the entered value is correct.
|
2017-12-25 16:41:12 +00:00
|
|
|
mhz_last_ppm = ppm;
|
2017-12-05 16:46:26 +00:00
|
|
|
return true;
|
|
|
|
}
|
2017-12-25 16:41:12 +00:00
|
|
|
int32_t difference = ppm - mhz_last_ppm;
|
|
|
|
if (s > 0 && s < 64 && mhz_filter != MHZ19_FILTER_OFF) {
|
2017-12-05 16:46:26 +00:00
|
|
|
// Not the "B" version of the sensor, S value is used.
|
|
|
|
// S==0 => "B" version, else "A" version
|
|
|
|
// The S value is an indication of the stability of the reading.
|
|
|
|
// S == 64 represents a stable reading and any lower value indicates (unusual) fast change.
|
|
|
|
// Now we increase the delay filter for low values of S and increase response time when the
|
|
|
|
// value is more stable.
|
|
|
|
// This will make the reading useful in more turbulent environments,
|
|
|
|
// where the sensor would report more rapid change of measured values.
|
2017-12-08 13:14:10 +00:00
|
|
|
difference *= s;
|
2017-12-05 16:46:26 +00:00
|
|
|
difference /= 64;
|
|
|
|
}
|
2017-12-25 16:41:12 +00:00
|
|
|
if (MHZ19_FILTER_OFF == mhz_filter) {
|
2017-12-08 13:14:10 +00:00
|
|
|
if (s != 0 && s != 64) {
|
|
|
|
return false;
|
2017-12-05 16:46:26 +00:00
|
|
|
}
|
2017-12-08 13:14:10 +00:00
|
|
|
} else {
|
2017-12-25 16:41:12 +00:00
|
|
|
difference >>= (mhz_filter -1);
|
2017-12-05 16:46:26 +00:00
|
|
|
}
|
2017-12-25 16:41:12 +00:00
|
|
|
mhz_last_ppm = static_cast<uint16_t>(mhz_last_ppm + difference);
|
2017-12-05 16:46:26 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
void MhzEverySecond()
|
2017-12-05 16:46:26 +00:00
|
|
|
{
|
2017-12-25 16:41:12 +00:00
|
|
|
mhz_state++;
|
2018-05-09 12:43:22 +01:00
|
|
|
if (8 == mhz_state) { // Every 8 sec start a MH-Z19 measuring cycle (which takes 1005 +5% ms)
|
2017-12-25 16:41:12 +00:00
|
|
|
mhz_state = 0;
|
2017-12-05 16:46:26 +00:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
if (mhz_retry) {
|
|
|
|
mhz_retry--;
|
|
|
|
if (!mhz_retry) {
|
|
|
|
mhz_last_ppm = 0;
|
|
|
|
mhz_temperature = 0;
|
|
|
|
}
|
|
|
|
}
|
2017-12-19 11:42:01 +00:00
|
|
|
|
2018-05-10 16:21:26 +01:00
|
|
|
MhzSerial->flush(); // Sync reception
|
2018-05-08 16:07:55 +01:00
|
|
|
MhzSendCmd(MHZ_CMND_READPPM);
|
|
|
|
mhz_received = 0;
|
|
|
|
}
|
2018-05-08 16:05:49 +01:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
if ((mhz_state > 2) && !mhz_received) { // Start reading response after 3 seconds every second until received
|
|
|
|
uint8_t mhz_response[9];
|
2017-12-05 16:46:26 +00:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
unsigned long start = millis();
|
|
|
|
uint8_t counter = 0;
|
|
|
|
while (((millis() - start) < MHZ19_READ_TIMEOUT) && (counter < 9)) {
|
2018-05-10 16:21:26 +01:00
|
|
|
if (MhzSerial->available() > 0) {
|
|
|
|
mhz_response[counter++] = MhzSerial->read();
|
2018-05-08 16:07:55 +01:00
|
|
|
} else {
|
|
|
|
delay(5);
|
2018-05-08 16:05:49 +01:00
|
|
|
}
|
2018-05-08 16:07:55 +01:00
|
|
|
}
|
2018-02-13 13:30:30 +00:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
AddLogSerial(LOG_LEVEL_DEBUG_MORE, mhz_response, counter);
|
2018-02-13 13:30:30 +00:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
if (counter < 9) {
|
|
|
|
// AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "MH-Z19 comms timeout"));
|
|
|
|
return;
|
|
|
|
}
|
2017-12-05 16:46:26 +00:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
byte crc = MhzCalculateChecksum(mhz_response);
|
|
|
|
if (mhz_response[8] != crc) {
|
|
|
|
// AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "MH-Z19 crc error"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (0xFF != mhz_response[0] || 0x86 != mhz_response[1]) {
|
|
|
|
// AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "MH-Z19 bad response"));
|
|
|
|
return;
|
|
|
|
}
|
2017-12-25 16:41:12 +00:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
mhz_received = 1;
|
2018-05-08 16:05:49 +01:00
|
|
|
|
2018-05-08 16:07:55 +01:00
|
|
|
uint16_t u = (mhz_response[6] << 8) | mhz_response[7];
|
|
|
|
if (15000 == u) { // During (and only ever at) sensor boot, 'u' is reported as 15000
|
|
|
|
if (!mhz_abc_enable) {
|
|
|
|
// After bootup of the sensor the ABC will be enabled.
|
|
|
|
// Thus only actively disable after bootup.
|
|
|
|
mhz_abc_must_apply = true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
uint16_t ppm = (mhz_response[2] << 8) | mhz_response[3];
|
|
|
|
mhz_temperature = ConvertTemp((float)mhz_response[4] - 40);
|
|
|
|
uint8_t s = mhz_response[5];
|
|
|
|
mhz_type = (s) ? 1 : 2;
|
|
|
|
if (MhzCheckAndApplyFilter(ppm, s)) {
|
|
|
|
mhz_retry = MHZ19_RETRY_COUNT;
|
|
|
|
LightSetSignal(CO2_LOW, CO2_HIGH, mhz_last_ppm);
|
|
|
|
|
|
|
|
if (0 == s || 64 == s) { // Reading is stable.
|
|
|
|
if (mhz_abc_must_apply) {
|
|
|
|
mhz_abc_must_apply = false;
|
|
|
|
if (mhz_abc_enable) {
|
|
|
|
MhzSendCmd(MHZ_CMND_ABCENABLE);
|
|
|
|
} else {
|
|
|
|
MhzSendCmd(MHZ_CMND_ABCDISABLE);
|
2017-12-05 16:46:26 +00:00
|
|
|
}
|
|
|
|
}
|
2018-05-08 16:05:49 +01:00
|
|
|
}
|
2018-05-08 16:07:55 +01:00
|
|
|
|
2017-12-05 16:46:26 +00:00
|
|
|
}
|
|
|
|
}
|
2017-12-25 16:41:12 +00:00
|
|
|
|
2017-12-05 16:46:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-24 16:31:20 +00:00
|
|
|
/*********************************************************************************************\
|
|
|
|
* Command Sensor15
|
2018-11-06 16:33:51 +00:00
|
|
|
*
|
|
|
|
* 0 - (Not implemented) ABC Off
|
|
|
|
* 1 - (Not implemented) ABC On
|
|
|
|
* 2 - Manual start = ABC Off
|
|
|
|
* 3 - (Not implemented) Optional filter settings
|
|
|
|
* 9 - Reset
|
2018-01-24 16:31:20 +00:00
|
|
|
\*********************************************************************************************/
|
|
|
|
|
|
|
|
bool MhzCommandSensor()
|
|
|
|
{
|
|
|
|
boolean serviced = true;
|
|
|
|
|
|
|
|
switch (XdrvMailbox.payload) {
|
|
|
|
case 2:
|
2018-05-08 16:07:55 +01:00
|
|
|
MhzSendCmd(MHZ_CMND_ZEROPOINT);
|
2018-01-24 16:31:20 +00:00
|
|
|
snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_SENSOR_INDEX_SVALUE, XSNS_15, D_JSON_ZERO_POINT_CALIBRATION);
|
|
|
|
break;
|
2018-05-08 16:07:55 +01:00
|
|
|
case 9:
|
|
|
|
MhzSendCmd(MHZ_CMND_RESET);
|
|
|
|
snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_SENSOR_INDEX_SVALUE, XSNS_15, D_JSON_RESET);
|
|
|
|
break;
|
2018-01-24 16:31:20 +00:00
|
|
|
default:
|
|
|
|
serviced = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return serviced;
|
|
|
|
}
|
|
|
|
|
2017-12-19 11:14:41 +00:00
|
|
|
/*********************************************************************************************/
|
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
void MhzInit()
|
2017-12-05 16:46:26 +00:00
|
|
|
{
|
2017-12-25 16:41:12 +00:00
|
|
|
mhz_type = 0;
|
2017-12-19 11:14:41 +00:00
|
|
|
if ((pin[GPIO_MHZ_RXD] < 99) && (pin[GPIO_MHZ_TXD] < 99)) {
|
2018-05-10 16:21:26 +01:00
|
|
|
MhzSerial = new TasmotaSerial(pin[GPIO_MHZ_RXD], pin[GPIO_MHZ_TXD], 1);
|
|
|
|
if (MhzSerial->begin(9600)) {
|
|
|
|
if (MhzSerial->hardwareSerial()) { ClaimSerial(); }
|
2017-12-25 16:41:12 +00:00
|
|
|
mhz_type = 1;
|
2017-12-19 11:14:41 +00:00
|
|
|
}
|
2018-05-10 16:21:26 +01:00
|
|
|
|
2017-12-08 13:14:10 +00:00
|
|
|
}
|
2017-12-05 16:46:26 +00:00
|
|
|
}
|
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
void MhzShow(boolean json)
|
2017-12-05 16:46:26 +00:00
|
|
|
{
|
2017-12-19 11:14:41 +00:00
|
|
|
char temperature[10];
|
2017-12-25 16:41:12 +00:00
|
|
|
dtostrfd(mhz_temperature, Settings.flag2.temperature_resolution, temperature);
|
|
|
|
GetTextIndexed(mhz_types, sizeof(mhz_types), mhz_type -1, kMhzTypes);
|
2017-12-05 16:46:26 +00:00
|
|
|
|
2017-12-19 11:14:41 +00:00
|
|
|
if (json) {
|
2017-12-29 13:35:05 +00:00
|
|
|
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,\"%s\":{\"" D_JSON_CO2 "\":%d,\"" D_JSON_TEMPERATURE "\":%s}"), mqtt_data, mhz_types, mhz_last_ppm, temperature);
|
2017-12-05 16:46:26 +00:00
|
|
|
#ifdef USE_DOMOTICZ
|
2018-04-11 09:11:20 +01:00
|
|
|
if (0 == tele_period) DomoticzSensor(DZ_AIRQUALITY, mhz_last_ppm);
|
2017-12-05 16:46:26 +00:00
|
|
|
#endif // USE_DOMOTICZ
|
|
|
|
#ifdef USE_WEBSERVER
|
2017-12-19 11:14:41 +00:00
|
|
|
} else {
|
2017-12-25 16:41:12 +00:00
|
|
|
snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_CO2, mqtt_data, mhz_types, mhz_last_ppm);
|
|
|
|
snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_TEMP, mqtt_data, mhz_types, temperature, TempUnit());
|
2017-12-05 16:46:26 +00:00
|
|
|
#endif // USE_WEBSERVER
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************************************\
|
|
|
|
* Interface
|
|
|
|
\*********************************************************************************************/
|
|
|
|
|
|
|
|
boolean Xsns15(byte function)
|
|
|
|
{
|
|
|
|
boolean result = false;
|
|
|
|
|
2017-12-25 16:41:12 +00:00
|
|
|
if (mhz_type) {
|
2017-12-05 16:46:26 +00:00
|
|
|
switch (function) {
|
2017-12-25 16:41:12 +00:00
|
|
|
case FUNC_INIT:
|
|
|
|
MhzInit();
|
|
|
|
break;
|
2018-05-08 16:07:55 +01:00
|
|
|
case FUNC_EVERY_SECOND:
|
|
|
|
MhzEverySecond();
|
2017-12-05 16:46:26 +00:00
|
|
|
break;
|
2018-01-24 16:31:20 +00:00
|
|
|
case FUNC_COMMAND:
|
|
|
|
if (XSNS_15 == XdrvMailbox.index) {
|
|
|
|
result = MhzCommandSensor();
|
|
|
|
}
|
|
|
|
break;
|
2017-12-25 16:41:12 +00:00
|
|
|
case FUNC_JSON_APPEND:
|
|
|
|
MhzShow(1);
|
2017-12-05 16:46:26 +00:00
|
|
|
break;
|
|
|
|
#ifdef USE_WEBSERVER
|
2017-12-25 16:41:12 +00:00
|
|
|
case FUNC_WEB_APPEND:
|
|
|
|
MhzShow(0);
|
2017-12-05 16:46:26 +00:00
|
|
|
break;
|
|
|
|
#endif // USE_WEBSERVER
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2017-12-19 11:14:41 +00:00
|
|
|
#endif // USE_MHZ19
|