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.

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