xsns_17_senseair.ino 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. /*
  2. xsns_17_senseair.ino - SenseAir CO2 sensor support for Sonoff-Tasmota
  3. Copyright (C) 2018 Theo Arends
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. #ifdef USE_SENSEAIR
  16. /*********************************************************************************************\
  17. * SenseAir K30, K70 and S8 - CO2 sensor
  18. *
  19. * Adapted from EspEasy plugin P052 by Mikael Trieb (mikael__AT__triebconsulting.se)
  20. *
  21. * Hardware Serial will be selected if GPIO1 = [SAir Rx] and GPIO3 = [SAir Tx]
  22. \*********************************************************************************************/
  23. #define XSNS_17 17
  24. #define SENSEAIR_MODBUS_SPEED 9600
  25. #define SENSEAIR_DEVICE_ADDRESS 0xFE // Any address
  26. #define SENSEAIR_READ_REGISTER 0x04 // Command Read
  27. #ifndef CO2_LOW
  28. #define CO2_LOW 800 // Below this CO2 value show green light
  29. #endif
  30. #ifndef CO2_HIGH
  31. #define CO2_HIGH 1200 // Above this CO2 value show red light
  32. #endif
  33. #include <TasmotaModbus.h>
  34. TasmotaModbus *SenseairModbus;
  35. const char kSenseairTypes[] PROGMEM = "Kx0|S8";
  36. uint8_t senseair_type = 1;
  37. char senseair_types[7];
  38. uint16_t senseair_co2 = 0;
  39. float senseair_temperature = 0;
  40. float senseair_humidity = 0;
  41. //uint8_t senseair_state = 0;
  42. const uint8_t start_addresses[] { 0x1A, 0x00, 0x03, 0x04, 0x05, 0x1C, 0x0A };
  43. uint8_t senseair_read_state = 0;
  44. uint8_t senseair_send_retry = 0;
  45. void Senseair250ms(void) // Every 250 mSec
  46. {
  47. // senseair_state++;
  48. // if (6 == senseair_state) { // Every 300 mSec
  49. // senseair_state = 0;
  50. uint16_t value = 0;
  51. bool data_ready = SenseairModbus->ReceiveReady();
  52. if (data_ready) {
  53. uint8_t error = SenseairModbus->Receive16BitRegister(&value);
  54. if (error) {
  55. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_DEBUG "SenseAir response error %d"), error);
  56. AddLog(LOG_LEVEL_DEBUG);
  57. } else {
  58. switch(senseair_read_state) {
  59. case 0: // 0x1A (26) READ_TYPE_LOW - S8: fe 04 02 01 77 ec 92
  60. senseair_type = 2;
  61. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_DEBUG "SenseAir type id low %04X"), value);
  62. AddLog(LOG_LEVEL_DEBUG);
  63. break;
  64. case 1: // 0x00 (0) READ_ERRORLOG - fe 04 02 00 00 ad 24
  65. if (value) {
  66. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_DEBUG "SenseAir error %04X"), value);
  67. AddLog(LOG_LEVEL_DEBUG);
  68. }
  69. break;
  70. case 2: // 0x03 (3) READ_CO2 - fe 04 02 06 2c af 59
  71. senseair_co2 = value;
  72. LightSetSignal(CO2_LOW, CO2_HIGH, senseair_co2);
  73. break;
  74. case 3: // 0x04 (4) READ_TEMPERATURE - S8: fe 84 02 f2 f1 - Illegal Data Address
  75. senseair_temperature = ConvertTemp((float)value / 100);
  76. break;
  77. case 4: // 0x05 (5) READ_HUMIDITY - S8: fe 84 02 f2 f1 - Illegal Data Address
  78. senseair_humidity = (float)value / 100;
  79. break;
  80. case 5: // 0x1C (28) READ_RELAY_STATE - S8: fe 04 02 01 54 ad 4b - firmware version
  81. {
  82. bool relay_state = value >> 8 & 1;
  83. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_DEBUG "SenseAir relay state %d"), relay_state);
  84. AddLog(LOG_LEVEL_DEBUG);
  85. break;
  86. }
  87. case 6: // 0x0A (10) READ_TEMP_ADJUSTMENT - S8: fe 84 02 f2 f1 - Illegal Data Address
  88. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_DEBUG "SenseAir temp adjustment %d"), value);
  89. AddLog(LOG_LEVEL_DEBUG);
  90. break;
  91. }
  92. }
  93. senseair_read_state++;
  94. if (2 == senseair_type) { // S8
  95. if (3 == senseair_read_state) {
  96. senseair_read_state = 1;
  97. }
  98. } else { // K30, K70
  99. if (sizeof(start_addresses) == senseair_read_state) {
  100. senseair_read_state = 1;
  101. }
  102. }
  103. }
  104. if (0 == senseair_send_retry || data_ready) {
  105. senseair_send_retry = 5;
  106. SenseairModbus->Send(SENSEAIR_DEVICE_ADDRESS, SENSEAIR_READ_REGISTER, (uint16_t)start_addresses[senseair_read_state], 1);
  107. } else {
  108. senseair_send_retry--;
  109. }
  110. // }
  111. }
  112. /*********************************************************************************************/
  113. void SenseairInit(void)
  114. {
  115. senseair_type = 0;
  116. if ((pin[GPIO_SAIR_RX] < 99) && (pin[GPIO_SAIR_TX] < 99)) {
  117. SenseairModbus = new TasmotaModbus(pin[GPIO_SAIR_RX], pin[GPIO_SAIR_TX]);
  118. uint8_t result = SenseairModbus->Begin(SENSEAIR_MODBUS_SPEED);
  119. if (result) {
  120. if (2 == result) { ClaimSerial(); }
  121. senseair_type = 1;
  122. }
  123. }
  124. }
  125. void SenseairShow(boolean json)
  126. {
  127. char temperature[33];
  128. dtostrfd(senseair_temperature, Settings.flag2.temperature_resolution, temperature);
  129. char humidity[33];
  130. dtostrfd(senseair_humidity, Settings.flag2.temperature_resolution, humidity);
  131. GetTextIndexed(senseair_types, sizeof(senseair_types), senseair_type -1, kSenseairTypes);
  132. if (json) {
  133. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,\"%s\":{\"" D_JSON_CO2 "\":%d"), mqtt_data, senseair_types, senseair_co2);
  134. if (senseair_type != 2) {
  135. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,\"" D_JSON_TEMPERATURE "\":%s,\"" D_JSON_HUMIDITY "\":%s"), mqtt_data, temperature, humidity);
  136. }
  137. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s}"), mqtt_data);
  138. #ifdef USE_DOMOTICZ
  139. if (0 == tele_period) DomoticzSensor(DZ_AIRQUALITY, senseair_co2);
  140. #endif // USE_DOMOTICZ
  141. #ifdef USE_WEBSERVER
  142. } else {
  143. snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_CO2, mqtt_data, senseair_types, senseair_co2);
  144. if (senseair_type != 2) {
  145. snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_TEMP, mqtt_data, senseair_types, temperature, TempUnit());
  146. snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_HUM, mqtt_data, senseair_types, humidity);
  147. }
  148. #endif // USE_WEBSERVER
  149. }
  150. }
  151. /*********************************************************************************************\
  152. * Interface
  153. \*********************************************************************************************/
  154. boolean Xsns17(byte function)
  155. {
  156. boolean result = false;
  157. if (senseair_type) {
  158. switch (function) {
  159. case FUNC_INIT:
  160. SenseairInit();
  161. break;
  162. case FUNC_EVERY_250_MSECOND:
  163. Senseair250ms();
  164. break;
  165. case FUNC_JSON_APPEND:
  166. SenseairShow(1);
  167. break;
  168. #ifdef USE_WEBSERVER
  169. case FUNC_WEB_APPEND:
  170. SenseairShow(0);
  171. break;
  172. #endif // USE_WEBSERVER
  173. }
  174. }
  175. return result;
  176. }
  177. #endif // USE_SENSEAIR