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.

470 lines
13 KiB

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