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.

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