From a2752bf5003386a8d90c550242eae03ec6ca28b8 Mon Sep 17 00:00:00 2001 From: Mark Ruys Date: Wed, 1 Jan 2025 14:26:29 +0100 Subject: [PATCH] Support S88 sensor (#22733) * Support S88 sensor Also: - More stable model detection - More robust against errors - Better logging * Support S88 --- tasmota/include/i18n.h | 1 + .../tasmota_xsns_sensor/xsns_17_senseair.ino | 270 ++++++++++++------ 2 files changed, 178 insertions(+), 93 deletions(-) diff --git a/tasmota/include/i18n.h b/tasmota/include/i18n.h index bfff6c971..9e2c36b80 100644 --- a/tasmota/include/i18n.h +++ b/tasmota/include/i18n.h @@ -884,6 +884,7 @@ #define D_LOG_LVGL "LVG: " // LVGL graphics engine #define D_LOG_THERMOSTAT "THE: " // Thermostat driver #define D_LOG_SENSOR "SNS: " // Sensor driver +#define D_LOG_SENSEAIR "SNA: " // Senseair driver /********************************************************************************************/ diff --git a/tasmota/tasmota_xsns_sensor/xsns_17_senseair.ino b/tasmota/tasmota_xsns_sensor/xsns_17_senseair.ino index a2a20196c..804c4d929 100644 --- a/tasmota/tasmota_xsns_sensor/xsns_17_senseair.ino +++ b/tasmota/tasmota_xsns_sensor/xsns_17_senseair.ino @@ -19,18 +19,48 @@ #ifdef USE_SENSEAIR /*********************************************************************************************\ - * SenseAir K30, K70 and S8 - CO2 sensor + * SenseAir K30, K70, S8 and S88 - CO2 sensor * * Adapted from EspEasy plugin P052 by Mikael Trieb (mikael__AT__triebconsulting.se) * * Hardware Serial will be selected if GPIO1 = [SAir Rx] and GPIO3 = [SAir Tx] \*********************************************************************************************/ +/*********************************************************************************************\ + * aSENSE tSENSE K30 S8 S88 + * IR1 MeterStatus MeterStatus MeterStatus MeterStatus MeterStatus + * IR4 Space CO2 Space CO2 Space CO2 Space CO2 Space CO2 + * IR5 Space Temp Space Temp - - Space Temp + * IR6 - RH - - - + * IR25 - - - - ETC Low + * IR27 - - - Type Id Low Type Id Low + * +\*********************************************************************************************/ + #define XSNS_17 17 #define SENSEAIR_MODBUS_SPEED 9600 -#define SENSEAIR_DEVICE_ADDRESS 0xFE // Any address -#define SENSEAIR_READ_REGISTER 0x04 // Command Read +#define SENSEAIR_BROADCAST_ADDRESS 0xFE + +#define COMMAND_READ_INPUT_REGISTER 0x04 + +#define IR_METER_STATUS 0 +#define IR_SPACE_CO2 3 +#define IR_SPACE_TEMP 4 // Not valid for S8 sensors +#define IR_SPACE_HUMIDITY 5 // Only valid for Kx0 sensors +#define IR_TEMP_ADJUSTMENT 10 +#define IR_SENSOR_ETC_LOW 24 +#define IR_SENSOR_ETC_HIGH 25 +#define IR_SENSOR_TYPE_ID_LOW 26 +#define IR_SENSOR_TYPE_ID_HIGH 27 +#define IR_RELAY_STATE 28 + +#define SENSOR_TYPE_INIT 0 +#define SENSOR_TYPE_UNKNOWN 1 +#define SENSOR_TYPE_NONE 2 +#define SENSOR_TYPE_KX0 3 +#define SENSOR_TYPE_S8 4 +#define SENSOR_TYPE_S88 5 #ifndef CO2_LOW #define CO2_LOW 800 // Below this CO2 value show green light @@ -42,117 +72,172 @@ #include TasmotaModbus *SenseairModbus; -const char kSenseairTypes[] PROGMEM = "Kx0|S8"; +const char kSenseairTypes[] PROGMEM = "Kx0|S8|S88"; -uint8_t senseair_type = 1; -char senseair_types[7]; +uint8_t senseair_type = SENSOR_TYPE_INIT; +char senseair_types[4]; uint16_t senseair_co2 = 0; float senseair_temperature = 0; float senseair_humidity = 0; -//uint8_t senseair_state = 0; +const uint8_t input_registers[] = { + IR_SENSOR_ETC_LOW, + IR_SENSOR_TYPE_ID_LOW, + IR_METER_STATUS, + IR_SPACE_CO2, + IR_SPACE_TEMP, + IR_SPACE_HUMIDITY, +}; -const uint8_t start_addresses[] { 0x1A, 0x00, 0x03, 0x04, 0x05, 0x1C, 0x0A }; +#define INPUT_REGISTERS_LOOP_START 2 +#define INPUT_REGISTERS_LOOP_END_KX0 (sizeof input_registers / sizeof *input_registers) +#define INPUT_REGISTERS_LOOP_END_S88 (INPUT_REGISTERS_LOOP_END_KX0 - 1) +#define INPUT_REGISTERS_LOOP_END_S8 (INPUT_REGISTERS_LOOP_END_KX0 - 2) uint8_t senseair_read_state = 0; uint8_t senseair_send_retry = 0; -void Senseair250ms(void) // Every 250 mSec +void Senseair250ms(void) { -// senseair_state++; -// if (6 == senseair_state) { // Every 300 mSec -// senseair_state = 0; + if (senseair_type == SENSOR_TYPE_INIT || senseair_type == SENSOR_TYPE_NONE) { + return; + } + bool data_ready = SenseairModbus->ReceiveReady(); + + if (data_ready) { uint16_t value = 0; - bool data_ready = SenseairModbus->ReceiveReady(); - if (data_ready) { - uint8_t error = SenseairModbus->Receive16BitRegister(&value); - if (error) { - AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "SenseAir read register %02X gave response error %d"), (uint16_t)start_addresses[senseair_read_state], error); - } else { - switch(senseair_read_state) { - case 0: // 0x1A (26) READ_TYPE_LOW - S8: fe 04 02 01 77 ec 92 - senseair_type = 2; - AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "SenseAir type id low %04X"), value); - break; - case 1: // 0x00 (0) READ_ERRORLOG - fe 04 02 00 00 ad 24 - if (value) { - AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "SenseAir error %04X"), value); + // Read register. + uint8_t error = SenseairModbus->Receive16BitRegister(&value); + + if (senseair_type == SENSOR_TYPE_UNKNOWN) { + // Try to determine sensor model. + switch (input_registers[senseair_read_state]) { + case IR_SENSOR_ETC_LOW: + if (error == 0) { + senseair_type = SENSOR_TYPE_S88; } break; - case 2: // 0x03 (3) READ_CO2 - fe 04 02 06 2c af 59 - senseair_co2 = value; -#ifdef USE_LIGHT - LightSetSignal(CO2_LOW, CO2_HIGH, senseair_co2); -#endif // USE_LIGHT - break; - case 3: // 0x04 (4) READ_TEMPERATURE - S8: fe 84 02 f2 f1 - Illegal Data Address - senseair_temperature = ConvertTemp((float)value / 100); - break; - case 4: // 0x05 (5) READ_HUMIDITY - S8: fe 84 02 f2 f1 - Illegal Data Address - senseair_humidity = ConvertHumidity((float)value / 100); - break; - case 5: // 0x1C (28) READ_RELAY_STATE - S8: fe 04 02 01 54 ad 4b - firmware version - { - bool relay_state = value >> 8 & 1; - AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "SenseAir relay state %d"), relay_state); - break; + case IR_SENSOR_TYPE_ID_LOW: + switch (error) { + case 0: + senseair_type = SENSOR_TYPE_S8; + break; + case 2: // Illegal Data Address + senseair_type = SENSOR_TYPE_KX0; + break; } - case 6: // 0x0A (10) READ_TEMP_ADJUSTMENT - S8: fe 84 02 f2 f1 - Illegal Data Address - AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "SenseAir temp adjustment %d"), value); - break; - } - - senseair_read_state++; - if (2 == senseair_type) { // S8 - if (3 == senseair_read_state) { - senseair_read_state = 1; - } - } else { // K30, K70 - if (sizeof(start_addresses) == senseair_read_state) { - senseair_read_state = 1; - } - } + break; + } + if (senseair_type != SENSOR_TYPE_UNKNOWN) { + GetTextIndexed(senseair_types, sizeof(senseair_types), senseair_type - SENSOR_TYPE_KX0, kSenseairTypes); + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_SENSEAIR "%s detected"), senseair_types); } } - if (0 == senseair_send_retry || data_ready) { - senseair_send_retry = 5; - SenseairModbus->Send(SENSEAIR_DEVICE_ADDRESS, SENSEAIR_READ_REGISTER, (uint16_t)start_addresses[senseair_read_state], 1); + if (error) { + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_SENSEAIR "Reg %d error %d"), input_registers[senseair_read_state], error); } else { - senseair_send_retry--; + // Process register contents. + switch(input_registers[senseair_read_state]) { + case IR_METER_STATUS: + switch (senseair_type) { + case SENSOR_TYPE_S8: value &= 0x7f; break; + case SENSOR_TYPE_S88: value &= 0xff; break; + } + if (value) { + // Out of range or Warm Up is expected after power on + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_SENSEAIR "Meter status 0x%04X"), value); + } + break; + case IR_SPACE_CO2: + senseair_co2 = value; +#ifdef USE_LIGHT + LightSetSignal(CO2_LOW, CO2_HIGH, senseair_co2); +#endif // USE_LIGHT + break; + case IR_SPACE_TEMP: + senseair_temperature = ConvertTemp((float)value / 100); + break; + case IR_SPACE_HUMIDITY: + senseair_humidity = ConvertHumidity((float)value / 100); + break; + } } -// } + // Find the next register to be read. + senseair_read_state++; + uint8_t input_registers_loop_end; + switch (senseair_type) { + case SENSOR_TYPE_KX0: + input_registers_loop_end = INPUT_REGISTERS_LOOP_END_KX0; + break; + case SENSOR_TYPE_S8: + input_registers_loop_end = INPUT_REGISTERS_LOOP_END_S8; + break; + case SENSOR_TYPE_S88: + input_registers_loop_end = INPUT_REGISTERS_LOOP_END_S88; + break; + } + if (senseair_read_state == input_registers_loop_end) { + if (senseair_type == SENSOR_TYPE_UNKNOWN) { + senseair_read_state = 0; + } else { + senseair_read_state = INPUT_REGISTERS_LOOP_START; + } + } + } + + if (0 == senseair_send_retry || data_ready) { + // Send the command (again). + senseair_send_retry = 5; + SenseairModbus->Send(SENSEAIR_BROADCAST_ADDRESS, COMMAND_READ_INPUT_REGISTER, input_registers[senseair_read_state], 1); + } else { + senseair_send_retry--; + } } /*********************************************************************************************/ void SenseairInit(void) { - senseair_type = 0; + if (senseair_type != SENSOR_TYPE_INIT) { + return; + } + + senseair_type = SENSOR_TYPE_NONE; if (PinUsed(GPIO_SAIR_RX) && PinUsed(GPIO_SAIR_TX)) { SenseairModbus = new TasmotaModbus(Pin(GPIO_SAIR_RX), Pin(GPIO_SAIR_TX)); uint8_t result = SenseairModbus->Begin(SENSEAIR_MODBUS_SPEED); if (result) { - if (2 == result) { ClaimSerial(); } - senseair_type = 1; + if (result == 2) { + // We have hardware serial, so claim it + ClaimSerial(); + } + senseair_type = SENSOR_TYPE_UNKNOWN; } } } -void SenseairShow(bool json) +void SenseairShow(uint32_t function) { - GetTextIndexed(senseair_types, sizeof(senseair_types), senseair_type -1, kSenseairTypes); + if (senseair_type < SENSOR_TYPE_KX0) { + return; + } - if (json) { + if (function == FUNC_JSON_APPEND) { ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_CO2 "\":%d"), senseair_types, senseair_co2); - if (senseair_type != 2) { - ResponseAppend_P(PSTR(",")); - ResponseAppendTHD(senseair_temperature, senseair_humidity); - } + switch (senseair_type) { + case SENSOR_TYPE_S88: + ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE "\":%1_f"), &senseair_temperature); + break; + case SENSOR_TYPE_KX0: + ResponseAppend_P(PSTR(",")); + ResponseAppendTHD(senseair_temperature, senseair_humidity); + break; + } ResponseJsonEnd(); #ifdef USE_DOMOTICZ if (0 == TasmotaGlobal.tele_period) { @@ -162,8 +247,13 @@ void SenseairShow(bool json) #ifdef USE_WEBSERVER } else { WSContentSend_PD(HTTP_SNS_CO2, senseair_types, senseair_co2); - if (senseair_type != 2) { - WSContentSend_THD(senseair_types, senseair_temperature, senseair_humidity); + switch (senseair_type) { + case SENSOR_TYPE_S88: + WSContentSend_Temp(senseair_types, senseair_temperature); + break; + case SENSOR_TYPE_KX0: + WSContentSend_THD(senseair_types, senseair_temperature, senseair_humidity); + break; } #endif // USE_WEBSERVER } @@ -175,27 +265,21 @@ void SenseairShow(bool json) bool Xsns17(uint32_t function) { - bool result = false; - - if (senseair_type) { - switch (function) { - case FUNC_INIT: - SenseairInit(); - break; - case FUNC_EVERY_250_MSECOND: - Senseair250ms(); - break; - case FUNC_JSON_APPEND: - SenseairShow(1); - break; + switch (function) { + case FUNC_INIT: + SenseairInit(); + break; + case FUNC_EVERY_250_MSECOND: + Senseair250ms(); + break; + case FUNC_JSON_APPEND: #ifdef USE_WEBSERVER - case FUNC_WEB_SENSOR: - SenseairShow(0); - break; + case FUNC_WEB_SENSOR: #endif // USE_WEBSERVER - } + SenseairShow(function); + break; } - return result; + return false; } #endif // USE_SENSEAIR