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.

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