Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

414 lines
11 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. /*
  2. THINGSPEAK MODULE
  3. Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #if THINGSPEAK_SUPPORT
  6. #if THINGSPEAK_USE_ASYNC
  7. #include <ESPAsyncTCP.h>
  8. #else
  9. #include <ESP8266WiFi.h>
  10. #endif
  11. #define THINGSPEAK_DATA_BUFFER_SIZE 256
  12. const char THINGSPEAK_REQUEST_TEMPLATE[] PROGMEM =
  13. "POST %s HTTP/1.1\r\n"
  14. "Host: %s\r\n"
  15. "User-Agent: ESPurna\r\n"
  16. "Connection: close\r\n"
  17. "Content-Type: application/x-www-form-urlencoded\r\n"
  18. "Content-Length: %d\r\n\r\n";
  19. bool _tspk_enabled = false;
  20. bool _tspk_clear = false;
  21. char * _tspk_queue[THINGSPEAK_FIELDS] = {NULL};
  22. String _tspk_data;
  23. bool _tspk_flush = false;
  24. unsigned long _tspk_last_flush = 0;
  25. unsigned char _tspk_tries = THINGSPEAK_TRIES;
  26. #if THINGSPEAK_USE_ASYNC
  27. AsyncClient * _tspk_client;
  28. bool _tspk_connecting = false;
  29. bool _tspk_connected = false;
  30. #endif
  31. // -----------------------------------------------------------------------------
  32. #if BROKER_SUPPORT
  33. void _tspkBrokerCallback(const unsigned char type, const char * topic, unsigned char id, const char * payload) {
  34. // Process status messages
  35. if (BROKER_MSG_TYPE_STATUS == type) {
  36. tspkEnqueueRelay(id, (char *) payload);
  37. tspkFlush();
  38. }
  39. // Porcess sensor messages
  40. if (BROKER_MSG_TYPE_SENSOR == type) {
  41. //tspkEnqueueMeasurement(id, (char *) payload);
  42. //tspkFlush();
  43. }
  44. }
  45. #endif // BROKER_SUPPORT
  46. #if WEB_SUPPORT
  47. bool _tspkWebSocketOnReceive(const char * key, JsonVariant& value) {
  48. return (strncmp(key, "tspk", 4) == 0);
  49. }
  50. void _tspkWebSocketOnSend(JsonObject& root) {
  51. unsigned char visible = 0;
  52. root["tspkEnabled"] = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1;
  53. root["tspkKey"] = getSetting("tspkKey");
  54. root["tspkClear"] = getSetting("tspkClear", THINGSPEAK_CLEAR_CACHE).toInt() == 1;
  55. JsonArray& relays = root.createNestedArray("tspkRelays");
  56. for (byte i=0; i<relayCount(); i++) {
  57. relays.add(getSetting("tspkRelay", i, 0).toInt());
  58. }
  59. if (relayCount() > 0) visible = 1;
  60. #if SENSOR_SUPPORT
  61. _sensorWebSocketMagnitudes(root, "tspk");
  62. visible = visible || (magnitudeCount() > 0);
  63. #endif
  64. root["tspkVisible"] = visible;
  65. }
  66. #endif
  67. void _tspkConfigure() {
  68. _tspk_clear = getSetting("tspkClear", THINGSPEAK_CLEAR_CACHE).toInt() == 1;
  69. _tspk_enabled = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1;
  70. if (_tspk_enabled && (getSetting("tspkKey").length() == 0)) {
  71. _tspk_enabled = false;
  72. setSetting("tspkEnabled", 0);
  73. }
  74. if (_tspk_enabled && !_tspk_client) _tspkInitClient();
  75. }
  76. #if THINGSPEAK_USE_ASYNC
  77. enum class tspk_state_t : uint8_t {
  78. NONE,
  79. HEADERS,
  80. BODY
  81. };
  82. tspk_state_t _tspk_client_state = tspk_state_t::NONE;
  83. unsigned long _tspk_client_ts = 0;
  84. constexpr const unsigned long THINGSPEAK_CLIENT_TIMEOUT = 5000;
  85. void _tspkInitClient() {
  86. _tspk_client = new AsyncClient();
  87. _tspk_client->onDisconnect([](void * s, AsyncClient * client) {
  88. DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n"));
  89. _tspk_data = "";
  90. _tspk_client_ts = 0;
  91. _tspk_last_flush = millis();
  92. _tspk_connected = false;
  93. _tspk_connecting = false;
  94. _tspk_client_state = tspk_state_t::NONE;
  95. }, nullptr);
  96. _tspk_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) {
  97. DEBUG_MSG_P(PSTR("[THINGSPEAK] Network timeout after %ums\n"), time);
  98. client->close(true);
  99. }, nullptr);
  100. _tspk_client->onPoll([](void * s, AsyncClient * client) {
  101. uint32_t ts = millis() - _tspk_client_ts;
  102. if (ts > THINGSPEAK_CLIENT_TIMEOUT) {
  103. DEBUG_MSG_P(PSTR("[THINGSPEAK] No response after %ums\n"), ts);
  104. client->close(true);
  105. }
  106. }, nullptr);
  107. _tspk_client->onData([](void * arg, AsyncClient * client, void * response, size_t len) {
  108. char * p = nullptr;
  109. do {
  110. p = nullptr;
  111. switch (_tspk_client_state) {
  112. case tspk_state_t::NONE:
  113. {
  114. p = strnstr(reinterpret_cast<const char *>(response), "HTTP/1.1 200 OK", len);
  115. if (!p) {
  116. client->close(true);
  117. return;
  118. }
  119. _tspk_client_state = tspk_state_t::HEADERS;
  120. continue;
  121. }
  122. case tspk_state_t::HEADERS:
  123. {
  124. p = strnstr(reinterpret_cast<const char *>(response), "\r\n\r\n", len);
  125. if (!p) return;
  126. _tspk_client_state = tspk_state_t::BODY;
  127. }
  128. case tspk_state_t::BODY:
  129. {
  130. if (!p) {
  131. p = strnstr(reinterpret_cast<const char *>(response), "\r\n\r\n", len);
  132. if (!p) return;
  133. }
  134. unsigned int code = (p) ? atoi(&p[4]) : 0;
  135. DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
  136. if ((0 == code) && _tspk_tries) {
  137. _tspk_flush = true;
  138. DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries);
  139. } else {
  140. _tspkClearQueue();
  141. }
  142. client->close(true);
  143. _tspk_client_state = tspk_state_t::NONE;
  144. }
  145. }
  146. } while (_tspk_client_state != tspk_state_t::NONE);
  147. }, nullptr);
  148. _tspk_client->onConnect([](void * arg, AsyncClient * client) {
  149. _tspk_connected = true;
  150. _tspk_connecting = false;
  151. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT);
  152. #if THINGSPEAK_USE_SSL
  153. uint8_t fp[20] = {0};
  154. sslFingerPrintArray(THINGSPEAK_FINGERPRINT, fp);
  155. SSL * ssl = _tspk_client->getSSL();
  156. if (ssl_match_fingerprint(ssl, fp) != SSL_OK) {
  157. DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n"));
  158. }
  159. #endif
  160. DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str());
  161. char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1];
  162. snprintf_P(headers, sizeof(headers),
  163. THINGSPEAK_REQUEST_TEMPLATE,
  164. THINGSPEAK_URL,
  165. THINGSPEAK_HOST,
  166. _tspk_data.length()
  167. );
  168. client->write(headers);
  169. client->write(_tspk_data.c_str());
  170. }, nullptr);
  171. }
  172. void _tspkPost() {
  173. if (_tspk_connected || _tspk_connecting) return;
  174. _tspk_client_ts = millis();
  175. #if ASYNC_TCP_SSL_ENABLED
  176. bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT, THINGSPEAK_USE_SSL);
  177. #else
  178. bool connected = _tspk_client->connect(THINGSPEAK_HOST, THINGSPEAK_PORT);
  179. #endif
  180. _tspk_connecting = connected;
  181. if (!connected) {
  182. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
  183. _tspk_client->close(true);
  184. }
  185. }
  186. #else // THINGSPEAK_USE_ASYNC
  187. void _tspkPost() {
  188. #if THINGSPEAK_USE_SSL
  189. WiFiClientSecure _tspk_client;
  190. #else
  191. WiFiClient _tspk_client;
  192. #endif
  193. if (_tspk_client.connect(THINGSPEAK_HOST, THINGSPEAK_PORT)) {
  194. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), THINGSPEAK_HOST, THINGSPEAK_PORT);
  195. if (!_tspk_client.verify(THINGSPEAK_FINGERPRINT, THINGSPEAK_HOST)) {
  196. DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n"));
  197. }
  198. DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), THINGSPEAK_URL, _tspk_data.c_str());
  199. char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + strlen(THINGSPEAK_URL) + strlen(THINGSPEAK_HOST) + 1];
  200. snprintf_P(headers, sizeof(headers),
  201. THINGSPEAK_REQUEST_TEMPLATE,
  202. THINGSPEAK_URL,
  203. THINGSPEAK_HOST,
  204. _tspk_data.length()
  205. );
  206. _tspk_client.print(headers);
  207. _tspk_client.print(_tspk_data);
  208. nice_delay(100);
  209. String response = _tspk_client.readString();
  210. int pos = response.indexOf("\r\n\r\n");
  211. unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0;
  212. DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
  213. _tspk_client.stop();
  214. _tspk_last_flush = millis();
  215. if ((0 == code) && _tspk_tries) {
  216. _tspk_flush = true;
  217. DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries);
  218. } else {
  219. _tspkClearQueue();
  220. }
  221. return;
  222. }
  223. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
  224. }
  225. #endif // THINGSPEAK_USE_ASYNC
  226. void _tspkEnqueue(unsigned char index, char * payload) {
  227. DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%u with value %s\n"), index, payload);
  228. --index;
  229. if (_tspk_queue[index] != NULL) free(_tspk_queue[index]);
  230. _tspk_queue[index] = strdup(payload);
  231. }
  232. void _tspkClearQueue() {
  233. _tspk_tries = THINGSPEAK_TRIES;
  234. if (_tspk_clear) {
  235. for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
  236. if (_tspk_queue[id] != NULL) {
  237. free(_tspk_queue[id]);
  238. _tspk_queue[id] = NULL;
  239. }
  240. }
  241. }
  242. }
  243. void _tspkFlush() {
  244. if (!_tspk_flush) return;
  245. if (millis() - _tspk_last_flush < THINGSPEAK_MIN_INTERVAL) return;
  246. if (_tspk_connected || _tspk_connecting) return;
  247. _tspk_last_flush = millis();
  248. _tspk_flush = false;
  249. _tspk_data.reserve(THINGSPEAK_DATA_BUFFER_SIZE);
  250. // Walk the fields, numbered 1...THINGSPEAK_FIELDS
  251. for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
  252. if (_tspk_queue[id] != NULL) {
  253. if (_tspk_data.length() > 0) _tspk_data.concat("&");
  254. char buf[32] = {0};
  255. snprintf_P(buf, sizeof(buf), PSTR("field%u=%s"), (id + 1), _tspk_queue[id]);
  256. _tspk_data.concat(buf);
  257. }
  258. }
  259. // POST data if any
  260. if (_tspk_data.length()) {
  261. _tspk_data.concat("&api_key=");
  262. _tspk_data.concat(getSetting("tspkKey"));
  263. --_tspk_tries;
  264. _tspkPost();
  265. }
  266. }
  267. // -----------------------------------------------------------------------------
  268. bool tspkEnqueueRelay(unsigned char index, char * payload) {
  269. if (!_tspk_enabled) return true;
  270. unsigned char id = getSetting("tspkRelay", index, 0).toInt();
  271. if (id > 0) {
  272. _tspkEnqueue(id, payload);
  273. return true;
  274. }
  275. return false;
  276. }
  277. bool tspkEnqueueMeasurement(unsigned char index, char * payload) {
  278. if (!_tspk_enabled) return true;
  279. unsigned char id = getSetting("tspkMagnitude", index, 0).toInt();
  280. if (id > 0) {
  281. _tspkEnqueue(id, payload);
  282. return true;
  283. }
  284. return false;
  285. }
  286. void tspkFlush() {
  287. _tspk_flush = true;
  288. }
  289. bool tspkEnabled() {
  290. return _tspk_enabled;
  291. }
  292. void tspkSetup() {
  293. _tspkConfigure();
  294. #if WEB_SUPPORT
  295. wsOnSendRegister(_tspkWebSocketOnSend);
  296. wsOnReceiveRegister(_tspkWebSocketOnReceive);
  297. #endif
  298. #if BROKER_SUPPORT
  299. brokerRegister(_tspkBrokerCallback);
  300. #endif
  301. DEBUG_MSG_P(PSTR("[THINGSPEAK] Async %s, SSL %s\n"),
  302. THINGSPEAK_USE_ASYNC ? "ENABLED" : "DISABLED",
  303. THINGSPEAK_USE_SSL ? "ENABLED" : "DISABLED"
  304. );
  305. // Main callbacks
  306. espurnaRegisterLoop(tspkLoop);
  307. espurnaRegisterReload(_tspkConfigure);
  308. }
  309. void tspkLoop() {
  310. if (!_tspk_enabled) return;
  311. if (!wifiConnected() || (WiFi.getMode() != WIFI_STA)) return;
  312. _tspkFlush();
  313. }
  314. #endif