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.

483 lines
14 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. if ((0 == code) && _tspk_tries) {
  157. _tspk_flush = true;
  158. DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries);
  159. } else {
  160. _tspkClearQueue();
  161. }
  162. client->close(true);
  163. _tspk_client_state = tspk_state_t::NONE;
  164. }
  165. }
  166. } while (_tspk_client_state != tspk_state_t::NONE);
  167. }, nullptr);
  168. _tspk_client->onConnect([](void * arg, AsyncClient * client) {
  169. _tspk_state = AsyncClientState::Disconnected;
  170. AsyncThingspeak* tspk_client = reinterpret_cast<AsyncThingspeak*>(client);
  171. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), tspk_client->address.host.c_str(), tspk_client->address.port);
  172. #if THINGSPEAK_USE_SSL
  173. uint8_t fp[20] = {0};
  174. sslFingerPrintArray(THINGSPEAK_FINGERPRINT, fp);
  175. SSL * ssl = tspk_client->getSSL();
  176. if (ssl_match_fingerprint(ssl, fp) != SSL_OK) {
  177. DEBUG_MSG_P(PSTR("[THINGSPEAK] Warning: certificate doesn't match\n"));
  178. }
  179. #endif
  180. DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), tspk_client->address.path.c_str(), _tspk_data.c_str());
  181. char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + tspk_client->address.path.length() + tspk_client->address.host.length() + 1];
  182. snprintf_P(headers, sizeof(headers),
  183. THINGSPEAK_REQUEST_TEMPLATE,
  184. tspk_client->address.path.c_str(),
  185. tspk_client->address.host.c_str(),
  186. _tspk_data.length()
  187. );
  188. client->write(headers);
  189. client->write(_tspk_data.c_str());
  190. }, nullptr);
  191. }
  192. void _tspkPost(const String& address) {
  193. if (_tspk_state != AsyncClientState::Disconnected) return;
  194. _tspk_client_ts = millis();
  195. _tspk_state = _tspk_client->connect(address)
  196. ? AsyncClientState::Connecting
  197. : AsyncClientState::Disconnected;
  198. if (_tspk_state == AsyncClientState::Disconnected) {
  199. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
  200. _tspk_client->close(true);
  201. }
  202. }
  203. #else // THINGSPEAK_USE_ASYNC
  204. #if THINGSPEAK_USE_SSL && (SECURE_CLIENT == SECURE_CLIENT_BEARSSL)
  205. SecureClientConfig _tspk_sc_config {
  206. "THINGSPEAK",
  207. []() -> int {
  208. return getSetting("tspkScCheck", THINGSPEAK_SECURE_CLIENT_CHECK);
  209. },
  210. []() -> PGM_P {
  211. return _tspk_client_trusted_root_ca;
  212. },
  213. []() -> String {
  214. return getSetting("tspkFP", THINGSPEAK_FINGERPRINT);
  215. },
  216. []() -> uint16_t {
  217. return getSetting("tspkScMFLN", THINGSPEAK_SECURE_CLIENT_MFLN);
  218. },
  219. true
  220. };
  221. #endif // THINGSPEAK_USE_SSL && SECURE_CLIENT_BEARSSL
  222. void _tspkPost(WiFiClient* client, const URL& url) {
  223. if (!client->connect(url.host.c_str(), url.port)) {
  224. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
  225. return;
  226. }
  227. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%u\n"), url.host.c_str(), url.port);
  228. DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), url.path.c_str(), _tspk_data.c_str());
  229. char headers[strlen_P(THINGSPEAK_REQUEST_TEMPLATE) + url.path.length() + url.host.length() + 1];
  230. snprintf_P(headers, sizeof(headers),
  231. THINGSPEAK_REQUEST_TEMPLATE,
  232. url.path.c_str(),
  233. url.host.c_str(),
  234. _tspk_data.length()
  235. );
  236. client->print(headers);
  237. client->print(_tspk_data);
  238. nice_delay(100);
  239. const auto response = client->readString();
  240. int pos = response.indexOf("\r\n\r\n");
  241. unsigned int code = (pos > 0) ? response.substring(pos + 4).toInt() : 0;
  242. DEBUG_MSG_P(PSTR("[THINGSPEAK] Response value: %u\n"), code);
  243. client->stop();
  244. _tspk_last_flush = millis();
  245. if ((0 == code) && _tspk_tries) {
  246. _tspk_flush = true;
  247. DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-enqueuing %u more time(s)\n"), _tspk_tries);
  248. } else {
  249. _tspkClearQueue();
  250. }
  251. }
  252. void _tspkPost(const String& address) {
  253. const URL url(address);
  254. #if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
  255. if (url.protocol == "https") {
  256. const int check = _ota_sc_config.on_check();
  257. if (!ntpSynced() && (check == SECURE_CLIENT_CHECK_CA)) {
  258. DEBUG_MSG_P(PSTR("[THINGSPEAK] Time not synced! Cannot use CA validation\n"));
  259. return;
  260. }
  261. auto client = std::make_unique<SecureClient>(_tspk_sc_config);
  262. if (!client->beforeConnected()) {
  263. return;
  264. }
  265. _tspkPost(&client->get(), url);
  266. return;
  267. }
  268. #endif
  269. if (url.protocol == "http") {
  270. auto client = std::make_unique<WiFiClient>();
  271. _tspkPost(client.get(), url);
  272. return;
  273. }
  274. }
  275. #endif // THINGSPEAK_USE_ASYNC
  276. void _tspkEnqueue(unsigned char index, const char * payload) {
  277. DEBUG_MSG_P(PSTR("[THINGSPEAK] Enqueuing field #%u with value %s\n"), index, payload);
  278. --index;
  279. if (_tspk_queue[index] != NULL) free(_tspk_queue[index]);
  280. _tspk_queue[index] = strdup(payload);
  281. }
  282. void _tspkClearQueue() {
  283. _tspk_tries = THINGSPEAK_TRIES;
  284. if (_tspk_clear) {
  285. for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
  286. if (_tspk_queue[id] != NULL) {
  287. free(_tspk_queue[id]);
  288. _tspk_queue[id] = NULL;
  289. }
  290. }
  291. }
  292. }
  293. void _tspkFlush() {
  294. if (!_tspk_flush) return;
  295. if (millis() - _tspk_last_flush < THINGSPEAK_MIN_INTERVAL) return;
  296. #if THINGSPEAK_USE_ASYNC
  297. if (_tspk_state != AsyncClientState::Disconnected) return;
  298. #endif
  299. _tspk_last_flush = millis();
  300. _tspk_flush = false;
  301. _tspk_data.reserve(tspkDataBufferSize);
  302. // Walk the fields, numbered 1...THINGSPEAK_FIELDS
  303. for (unsigned char id=0; id<THINGSPEAK_FIELDS; id++) {
  304. if (_tspk_queue[id] != NULL) {
  305. if (_tspk_data.length() > 0) _tspk_data.concat("&");
  306. char buf[32] = {0};
  307. snprintf_P(buf, sizeof(buf), PSTR("field%u=%s"), (id + 1), _tspk_queue[id]);
  308. _tspk_data.concat(buf);
  309. }
  310. }
  311. // POST data if any
  312. if (_tspk_data.length()) {
  313. _tspk_data.concat("&api_key=");
  314. _tspk_data.concat(getSetting<String>("tspkKey", THINGSPEAK_APIKEY));
  315. --_tspk_tries;
  316. _tspkPost(getSetting("tspkAddress", THINGSPEAK_ADDRESS));
  317. }
  318. }
  319. // -----------------------------------------------------------------------------
  320. bool tspkEnqueueRelay(unsigned char index, bool status) {
  321. if (!_tspk_enabled) return true;
  322. unsigned char id = getSetting({"tspkRelay", index}, 0);
  323. if (id > 0) {
  324. _tspkEnqueue(id, status ? "1" : "0");
  325. return true;
  326. }
  327. return false;
  328. }
  329. bool tspkEnqueueMeasurement(unsigned char index, const char * payload) {
  330. if (!_tspk_enabled) return true;
  331. const auto id = getSetting({"tspkMagnitude", index}, 0);
  332. if (id > 0) {
  333. _tspkEnqueue(id, payload);
  334. return true;
  335. }
  336. return false;
  337. }
  338. void tspkFlush() {
  339. _tspk_flush = true;
  340. }
  341. bool tspkEnabled() {
  342. return _tspk_enabled;
  343. }
  344. void tspkSetup() {
  345. _tspkConfigure();
  346. #if WEB_SUPPORT
  347. wsRegister()
  348. .onVisible(_tspkWebSocketOnVisible)
  349. .onConnected(_tspkWebSocketOnConnected)
  350. .onKeyCheck(_tspkWebSocketOnKeyCheck);
  351. #endif
  352. #if BROKER_SUPPORT
  353. StatusBroker::Register(_tspkBrokerCallback);
  354. #endif
  355. DEBUG_MSG_P(PSTR("[THINGSPEAK] Async %s, SSL %s\n"),
  356. THINGSPEAK_USE_ASYNC ? "ENABLED" : "DISABLED",
  357. THINGSPEAK_USE_SSL ? "ENABLED" : "DISABLED"
  358. );
  359. // Main callbacks
  360. espurnaRegisterLoop(tspkLoop);
  361. espurnaRegisterReload(_tspkConfigure);
  362. }
  363. void tspkLoop() {
  364. if (!_tspk_enabled) return;
  365. if (!wifiConnected() || (WiFi.getMode() != WIFI_STA)) return;
  366. _tspkFlush();
  367. }
  368. #endif