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.

412 lines
11 KiB

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