xsns_38_az7798.ino 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /*
  2. xsns_38_az7798.ino - AZ_Instrument 7798 CO2/temperature/humidity meter 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_AZ7798
  16. #define XSNS_38 38
  17. /*********************************************************************************************\
  18. * CO2, temperature and humidity meter and data logger
  19. * Known by different names (brief survey 2018-12-16):
  20. * - AZ-Instrument 7798 (http://www.az-instrument.com.tw)
  21. * - co2meter.com AZ-0004
  22. * - Extech CO200
  23. * - BES CO7788 (https://www.aliexpress.com)
  24. * - AZ CO87 (https://www.aliexpress.com)
  25. * - no doubt there are more ...
  26. *
  27. * Hardware Serial will be selected if GPIO1 = [AZ Tx] and GPIO3 = [AZ Rx]
  28. *
  29. * Inside the meter, the serial comms wire with the red stripe goes to GPIO1.
  30. * The other one therefore to GPIO3.
  31. * WeMos D1 Mini is powered from the incoming 5V.
  32. *
  33. * This implementation was derived from xsns_15_mhz19.ino from
  34. * Sonoff-Tasmota-6.3.0 by Arthur de Beun.
  35. *
  36. * The serial comms protocol is not publicly documented, that I could find.
  37. * The info below was obtained by reverse-engineering.
  38. * Port settings: 9600 8N1
  39. * The suppied USB interface has a CP20x USB-serial bridge.
  40. * The 3-way, 2.5mm jack has tip=RxD, middle=TxD and base=0V
  41. * The TxD output swing is 3V3.
  42. *
  43. * There is never a space before the 0x0d, but the other spaces are there.
  44. *
  45. * serial number / ID
  46. * request: I 0x0d
  47. * response: i 12345678 7798V3.4 0x0d
  48. *
  49. * log info
  50. * request: M 0x0d
  51. * response: m 45 1 C 1af4 0cf4 0x0d
  52. *
  53. * 45 = number of records, but there are only 15 lines of 3 values each)
  54. * 1 = sample rate in seconds
  55. * C = celcius, F
  56. * 1af4 0cf4 = seconds since 2000-01-01 00:00:00
  57. *
  58. * start time 2014-04-30 19:35:16
  59. * end time 2014-04-30 19:35:30
  60. *
  61. * download log data
  62. * request: D 0x0d
  63. * response: m 45 1 C 1af4 0cf4 0x0d
  64. * d 174 955 698 0x0d
  65. * 174 = temp in [C * 10]
  66. * 955 = CO2 [ppm]
  67. * 698 = RH in [% * 10]
  68. * d 174 990 694 0x0d
  69. * ...
  70. * d 173 929 654 0x0d
  71. *
  72. * 15 lines in total, 1 second apart
  73. *
  74. * Sync datalogger time with PC
  75. * request: C 452295746 0x0d
  76. * response: > 0x0d
  77. *
  78. * 452295746 = seconds since 2000-01-01 00:00:00
  79. *
  80. * Identifier:
  81. * request: J -------- 1 0x0d
  82. *
  83. * the characters (dashes) in the above become the first part of the response to the I command (12345678 above)
  84. *
  85. * Set sample rate
  86. * request: S 10 0x0d
  87. * response: m 12 10 C 1af5 7be1 0x0d
  88. *
  89. * Other characters that seem to give a response:
  90. * A responds with >
  91. * so is similar to the response to C, so other characters may be required
  92. * A is the beep alarm perhaps?
  93. * parameters would be CO2 level and on/off, as per front panel P1.3 setting?
  94. *
  95. * L responds with >
  96. * L perhaps sets the limits for the good and normal levels (P1.1 and P1.2)?
  97. *
  98. * Q responds with >
  99. * Q is reset maybe (P4.1)?
  100. *
  101. * : responds with : T19.9C:C2167ppm:H57.4%
  102. * This one gives the current readings.
  103. **********************************************************************************************
  104. /*********************************************************************************************/
  105. #include <TasmotaSerial.h>
  106. #ifndef CO2_LOW
  107. #define CO2_LOW 800 // Below this CO2 value show green light
  108. #endif
  109. #ifndef CO2_HIGH
  110. #define CO2_HIGH 1200 // Above this CO2 value show red light
  111. #endif
  112. #define AZ_READ_TIMEOUT 400 // Must be way less than 1000 but enough to read 9 bytes at 9600 bps
  113. TasmotaSerial *AzSerial;
  114. const char ktype[] = "AZ7798";
  115. uint8_t az_type = 1;
  116. uint16_t az_co2 = 0;
  117. double az_temperature = 0;
  118. double az_humidity = 0;
  119. uint8_t az_received = 0;
  120. uint8_t az_state = 0;
  121. /*********************************************************************************************/
  122. void AzEverySecond(void)
  123. {
  124. az_state++;
  125. if (5 == az_state) { // every 5 seconds
  126. az_state = 0;
  127. AzSerial->flush(); // sync reception
  128. AzSerial->write(":\r", 2);
  129. az_received = 0;
  130. uint8_t az_response[32];
  131. unsigned long start = millis();
  132. uint8_t counter = 0;
  133. uint8_t i, j;
  134. uint8_t response_substr[16];
  135. do {
  136. if (AzSerial->available() > 0) {
  137. az_response[counter] = AzSerial->read();
  138. if(az_response[counter] == 0x0d) { az_received = 1; }
  139. counter++;
  140. } else {
  141. delay(5);
  142. }
  143. } while(((millis() - start) < AZ_READ_TIMEOUT) && (counter < sizeof(az_response)) && !az_received);
  144. AddLogSerial(LOG_LEVEL_DEBUG_MORE, az_response, counter);
  145. if (!az_received) {
  146. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 comms timeout"));
  147. return;
  148. }
  149. i = 0;
  150. while((az_response[i] != 'T') && (i < counter)) {i++;} // find the start of response
  151. if(az_response[i] != 'T') {
  152. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 failed to find start of response"));
  153. return;
  154. }
  155. i++; // advance to start of temperature value
  156. j = 0;
  157. // find the end of temperature
  158. while((az_response[i] != 'C') && (az_response[i] != 'F') && (i < counter)) {
  159. response_substr[j++] = az_response[i++];
  160. }
  161. if((az_response[i] != 'C') && (az_response[i] != 'F')){
  162. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 failed to find end of temperature"));
  163. return;
  164. }
  165. response_substr[j] = 0; // add null terminator
  166. az_temperature = CharToDouble((char*)response_substr); // units (C or F) depends on meter setting
  167. if(az_response[i] == 'C') { // meter transmits in degC
  168. az_temperature = ConvertTemp((float)az_temperature); // convert to degF, depending on settings
  169. } else { // meter transmits in degF
  170. az_temperature = ConvertTemp((az_temperature - 32) / 1.8); // convert to degC and then C or F depending on setting
  171. }
  172. i++; // advance to first delimiter
  173. if(az_response[i] != ':') {
  174. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 error first delimiter"));
  175. return;
  176. }
  177. i++; // advance to start of CO2
  178. if(az_response[i] != 'C') {
  179. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 error start of CO2"));
  180. return;
  181. }
  182. i++; // advance to start of CO2 value
  183. j = 0;
  184. // find the end of CO2
  185. while((az_response[i] != 'p') && (i < counter)) {
  186. response_substr[j++] = az_response[i++];
  187. }
  188. if(az_response[i] != 'p') {
  189. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 failed to find end of CO2"));
  190. return;
  191. }
  192. response_substr[j] = 0; // add null terminator
  193. az_co2 = atoi((char*)response_substr);
  194. LightSetSignal(CO2_LOW, CO2_HIGH, az_co2);
  195. i += 3; // advance to second delimiter
  196. if(az_response[i] != ':') {
  197. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 error second delimiter"));
  198. return;
  199. }
  200. i++; // advance to start of humidity
  201. if(az_response[i] != 'H') {
  202. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 error start of humidity"));
  203. return;
  204. }
  205. i++; // advance to start of humidity value
  206. j = 0;
  207. // find the end of humidity
  208. while((az_response[i] != '%') && (i < counter)) {
  209. response_substr[j++] = az_response[i++];
  210. }
  211. if(az_response[i] != '%') {
  212. AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "AZ7798 failed to find end of humidity"));
  213. return;
  214. }
  215. response_substr[j] = 0; // add null terminator
  216. az_humidity = CharToDouble((char*)response_substr);
  217. }
  218. }
  219. /*********************************************************************************************/
  220. void AzInit(void)
  221. {
  222. az_type = 0;
  223. if ((pin[GPIO_AZ_RXD] < 99) && (pin[GPIO_AZ_TXD] < 99)) {
  224. AzSerial = new TasmotaSerial(pin[GPIO_AZ_RXD], pin[GPIO_AZ_TXD], 1);
  225. if (AzSerial->begin(9600)) {
  226. if (AzSerial->hardwareSerial()) { ClaimSerial(); }
  227. az_type = 1;
  228. }
  229. }
  230. }
  231. void AzShow(boolean json)
  232. {
  233. char temperature[33];
  234. dtostrfd(az_temperature, Settings.flag2.temperature_resolution, temperature);
  235. char humidity[33];
  236. dtostrfd(az_humidity, Settings.flag2.humidity_resolution, humidity);
  237. if (json) {
  238. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,\"%s\":{\"" D_JSON_CO2 "\":%d,\"" D_JSON_TEMPERATURE "\":%s,\"" D_JSON_HUMIDITY "\":%s}"), mqtt_data, ktype, az_co2, temperature, humidity);
  239. #ifdef USE_DOMOTICZ
  240. if (0 == tele_period) DomoticzSensor(DZ_AIRQUALITY, az_co2);
  241. #endif // USE_DOMOTICZ
  242. #ifdef USE_WEBSERVER
  243. } else {
  244. snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_CO2, mqtt_data, ktype, az_co2);
  245. snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_TEMP, mqtt_data, ktype, temperature, TempUnit());
  246. snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_HUM, mqtt_data, ktype, humidity);
  247. #endif // USE_WEBSERVER
  248. }
  249. }
  250. /*********************************************************************************************\
  251. * Interface
  252. \*********************************************************************************************/
  253. boolean Xsns38(byte function)
  254. {
  255. boolean result = false;
  256. if(az_type){
  257. switch (function) {
  258. case FUNC_INIT:
  259. AzInit();
  260. break;
  261. case FUNC_EVERY_SECOND:
  262. AzEverySecond();
  263. break;
  264. case FUNC_JSON_APPEND:
  265. AzShow(1);
  266. break;
  267. #ifdef USE_WEBSERVER
  268. case FUNC_WEB_APPEND:
  269. AzShow(0);
  270. break;
  271. #endif // USE_WEBSERVER
  272. }
  273. }
  274. return result;
  275. }
  276. #endif // USE_AZ7798