Mirror of espurna firmware for wireless switches and more
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.

779 lines
18 KiB

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 "espurna.h"
  6. #if THINGSPEAK_SUPPORT
  7. #include "mqtt.h"
  8. #include "relay.h"
  9. #include "rpc.h"
  10. #include "sensor.h"
  11. #include "thingspeak.h"
  12. #include "ws.h"
  13. #include <memory>
  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. namespace espurna {
  23. namespace thingspeak {
  24. namespace {
  25. using TimeSource = espurna::time::CoreClock;
  26. } // namespace
  27. namespace build {
  28. static constexpr size_t Unset { 0 };
  29. static constexpr size_t Fields { THINGSPEAK_FIELDS };
  30. static constexpr auto FlushInterval = espurna::duration::Milliseconds(THINGSPEAK_MIN_INTERVAL);
  31. static constexpr size_t Retries { THINGSPEAK_TRIES };
  32. static constexpr size_t BufferSize { 256 };
  33. PROGMEM_STRING(ApiKey, THINGSPEAK_APIKEY);
  34. PROGMEM_STRING(Address, THINGSPEAK_ADDRESS);
  35. constexpr bool enabled() {
  36. return 1 == THINGSPEAK_ENABLED;
  37. }
  38. constexpr bool clearCache() {
  39. return 1 == THINGSPEAK_CLEAR_CACHE;
  40. }
  41. } // namespace build
  42. namespace settings {
  43. namespace keys {
  44. PROGMEM_STRING(Enabled, "tspkEnabled");
  45. PROGMEM_STRING(ApiKey, "tspkKey");
  46. PROGMEM_STRING(ClearCache, "tspkClear");
  47. PROGMEM_STRING(Address, "tspkAddress");
  48. PROGMEM_STRING(Relay, "tspkRelay");
  49. PROGMEM_STRING(Magnitude, "tspkMagnitude");
  50. #if THINGSPEAK_USE_SSL && (SECURE_CLIENT != SECURE_CLIENT_NONE)
  51. PROGMEM_STRING(Check, "tspkScCheck");
  52. PROGMEM_STRING(Fingerprint, "tspkFP");
  53. PROGMEM_STRING(Mfln, "tspkMfln");
  54. #endif
  55. } // namespace keys
  56. namespace {
  57. bool enabled() {
  58. return getSetting(FPSTR(keys::Enabled), build::enabled());
  59. }
  60. bool clearCache() {
  61. return getSetting(FPSTR(keys::ClearCache), build::clearCache());
  62. }
  63. String apiKey() {
  64. return getSetting(FPSTR(keys::ApiKey), FPSTR(build::ApiKey));
  65. }
  66. String address() {
  67. return getSetting(FPSTR(keys::Address), FPSTR(build::Address));
  68. }
  69. #if RELAY_SUPPORT
  70. size_t relay(size_t index) {
  71. return getSetting({FPSTR(keys::Relay), index}, build::Unset);
  72. }
  73. #endif
  74. #if SENSOR_SUPPORT
  75. size_t magnitude(size_t index) {
  76. return getSetting({FPSTR(keys::Magnitude), index}, build::Unset);
  77. }
  78. #endif
  79. } // namespace
  80. } // namespace settings
  81. // -----------------------------------------------------------------------------
  82. namespace client {
  83. namespace internal {
  84. namespace {
  85. bool enabled = false;
  86. bool clear = false;
  87. String fields[build::Fields];
  88. TimeSource::time_point last_flush;
  89. size_t retries = 0;
  90. bool flush = false;
  91. String data;
  92. } // namespace
  93. } // namespace internal
  94. void schedule_flush() {
  95. internal::flush = true;
  96. }
  97. void enqueue(size_t index, const String& payload) {
  98. if ((index > 0) && (index <= std::size(internal::fields))) {
  99. internal::fields[--index] = payload;
  100. return;
  101. }
  102. }
  103. void enqueue(size_t index, bool status) {
  104. enqueue(index, status ? String('1') : String('0'));
  105. }
  106. void value(size_t index, double status) {
  107. enqueue(index, String(status, 3));
  108. }
  109. #if RELAY_SUPPORT
  110. bool enqueueRelay(size_t index, bool status) {
  111. if (internal::enabled) {
  112. auto relayIndex = settings::relay(index);
  113. if (relayIndex) {
  114. enqueue(relayIndex, status);
  115. schedule_flush();
  116. return true;
  117. }
  118. }
  119. return false;
  120. }
  121. void onRelayStatus(size_t index, bool status) {
  122. enqueueRelay(index, status);
  123. }
  124. #endif
  125. #if SENSOR_SUPPORT
  126. bool enqueueMagnitude(size_t index, const String& value) {
  127. if (internal::enabled) {
  128. auto magnitudeIndex = settings::magnitude(index);
  129. if (magnitudeIndex) {
  130. enqueue(magnitudeIndex, value);
  131. schedule_flush();
  132. return true;
  133. }
  134. }
  135. return false;
  136. }
  137. #endif
  138. void maybe_retry(const String& body) {
  139. DEBUG_MSG_P(PSTR("[THINGSPEAK] Response: %s\n"), body.c_str());
  140. if ((!body.length() || body.equals(F("0"))) && (internal::retries < build::Retries)) {
  141. DEBUG_MSG_P(PSTR("[THINGSPEAK] Re-scheduling flush, attempt %u / %u\n"),
  142. ++internal::retries, build::Retries);
  143. schedule_flush();
  144. return;
  145. }
  146. internal::retries = 0;
  147. if (internal::clear) {
  148. for (auto& field : internal::fields) {
  149. field = "";
  150. }
  151. }
  152. }
  153. #if !THINGSPEAK_USE_ASYNC
  154. namespace sync {
  155. namespace internal {
  156. namespace {
  157. #if THINGSPEAK_USE_SSL && (SECURE_CLIENT != SECURE_CLIENT_NONE)
  158. #if THINGSPEAK_SECURE_CLIENT_INCLUDE_CA
  159. #include "static/thingspeak_client_trusted_root_ca.h"
  160. #else
  161. #include "static/digicert_high_assurance_pem.h"
  162. #define trusted_root _ssl_digicert_high_assurance_ev_root_ca
  163. #endif
  164. #if (SECURE_CLIENT == SECURE_CLIENT_BEARSSL)
  165. static constexpr int Check { THINGSPEAK_SECURE_CLIENT_CHECK };
  166. static constexpr uint16_t Mfln { THINGSPEAK_SECURE_CLIENT_MFLN };
  167. PROGMEM_STRING(Tag, "THINGSPEAK");
  168. PROGMEM_STRING(Fingerprint, THINGSPEAK_FINGERPRINT);
  169. SecureClientConfig secure_config {
  170. .tag = Tag,
  171. .on_check = []() -> int {
  172. return getSetting(FPSTR(settings::keys::Check), Check);
  173. },
  174. .on_certificate = []() -> const char* {
  175. return trusted_root;
  176. },
  177. .on_fingerprint = []() -> String {
  178. return getSetting(FPSTR(settings::keys::Fingerprint), FPSTR(Fingerprint));
  179. },
  180. .on_mfln = []() -> uint16_t {
  181. return getSetting(FPSTR(settings::keys::Mfln), Mfln);
  182. },
  183. .debug = true,
  184. };
  185. #endif
  186. #undef trusted_root
  187. #endif
  188. } // namespace
  189. } // namesapce internal
  190. namespace {
  191. void send(WiFiClient& client, const URL& url, const String& data) {
  192. DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"), url.path.c_str(), data.c_str());
  193. HTTPClient http;
  194. http.begin(client, url.host, url.port, url.path,
  195. url.protocol.equals(F("https")));
  196. const auto app = buildApp();
  197. http.addHeader(F("User-Agent"), String(app.name));
  198. http.addHeader(F("Content-Type"), F("application/x-www-form-urlencoded"));
  199. const auto response = http.POST(data);
  200. String body;
  201. if (response == 200) {
  202. if (http.getSize()) {
  203. body = http.getString();
  204. }
  205. } else {
  206. DEBUG_MSG_P(PSTR("[THINGSPEAK] ERROR: HTTP %d\n"), response);
  207. }
  208. if (body.length()) {
  209. DEBUG_MSG_P(PSTR("[THINGSPEAK] Response: %s\n"), body.c_str());
  210. } else {
  211. DEBUG_MSG_P(PSTR("[THINGSPEAK] Empty body\n"));
  212. }
  213. maybe_retry(body);
  214. }
  215. void send(const String& address, const String& data) {
  216. const URL url(address);
  217. #if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
  218. if (url.protocol.equals(F("https"))) {
  219. const int check = internal::secure_config.on_check();
  220. if (!ntpSynced() && (check == SECURE_CLIENT_CHECK_CA)) {
  221. DEBUG_MSG_P(PSTR("[THINGSPEAK] Time not synced! Cannot use CA validation\n"));
  222. return;
  223. }
  224. auto client = std::make_unique<SecureClient>(internal::secure_config);
  225. if (!client->beforeConnected()) {
  226. return;
  227. }
  228. send(client->get(), url, data);
  229. return;
  230. }
  231. #endif
  232. if (url.protocol.equals(F("http"))) {
  233. auto client = std::make_unique<WiFiClient>();
  234. send(*client.get(), url, data);
  235. return;
  236. }
  237. }
  238. } // namespace
  239. } // namespace sync
  240. #endif
  241. #if THINGSPEAK_USE_ASYNC
  242. namespace async {
  243. namespace {
  244. class Client {
  245. public:
  246. static constexpr auto Timeout = espurna::duration::Seconds(15);
  247. using Completion = void(*)(const String&);
  248. using ClientState = AsyncClientState;
  249. enum class ParserState {
  250. Init,
  251. Headers,
  252. Body,
  253. End,
  254. };
  255. bool send(const String& data, Completion completion) {
  256. if (_client_state == ClientState::Disconnected) {
  257. _data = data;
  258. _completion = completion;
  259. if (!_client) {
  260. _client = std::make_unique<AsyncClient>();
  261. _client->onDisconnect(Client::_onDisconnected, this);
  262. _client->onConnect(Client::_onConnect, this);
  263. _client->onTimeout(Client::_onTimeout, this);
  264. _client->onPoll(Client::_onPoll, this);
  265. _client->onData(Client::_onData, this);
  266. }
  267. _connection_start = TimeSource::now();
  268. _client_state = ClientState::Connecting;
  269. if (_client->connect(_address.host.c_str(), _address.port)) {
  270. return true;
  271. }
  272. _client->close(true);
  273. }
  274. return false;
  275. }
  276. bool send(const String& address, const String& data, Completion completion) {
  277. _address = URL(address);
  278. return send(data, completion);
  279. }
  280. void disconnect() {
  281. if (_client_state == ClientState::Disconnected) {
  282. _client = nullptr;
  283. }
  284. }
  285. const URL& address() const {
  286. return _address;
  287. }
  288. explicit operator bool() const {
  289. return _client_state != ClientState::Disconnected;
  290. }
  291. private:
  292. void onDisconnected() {
  293. DEBUG_MSG_P(PSTR("[THINGSPEAK] Disconnected\n"));
  294. _parser_state = ParserState::Init;
  295. _client_state = ClientState::Disconnected;
  296. _data = "";
  297. }
  298. void onTimeout(uint32_t timestamp) {
  299. DEBUG_MSG_P(PSTR("[THINGSPEAK] ERROR: Network timeout after %ums\n"), timestamp);
  300. _client->close(true);
  301. }
  302. bool _sendPendingData() {
  303. if (!_data.length()) {
  304. return true;
  305. }
  306. size_t wrote = _client->write(_data.c_str(), _data.length());
  307. if (wrote == _data.length()) {
  308. _data = "";
  309. return true;
  310. }
  311. return false;
  312. }
  313. void onPoll() {
  314. if (_client_state != ClientState::Connected) {
  315. return;
  316. }
  317. if (!_sendPendingData()) {
  318. return;
  319. }
  320. const auto now = TimeSource::now();
  321. if (now - _connection_start > Timeout) {
  322. DEBUG_MSG_P(PSTR("[THINGSPEAK] ERROR: Timeout after %ums\n"),
  323. (now - _connection_start).count());
  324. _client->close(true);
  325. }
  326. }
  327. void onConnect() {
  328. _parser_state = ParserState::Init;
  329. _client_state = ClientState::Connected;
  330. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connected to %s:%hu\n"),
  331. _address.host.c_str(), _address.port);
  332. DEBUG_MSG_P(PSTR("[THINGSPEAK] POST %s?%s\n"),
  333. _address.path.c_str(), _data.c_str());
  334. static constexpr size_t HeadersSize { 256 };
  335. String headers;
  336. headers.reserve(HeadersSize);
  337. auto append = [&](const String& key, const String& value) {
  338. headers += key;
  339. headers += F(": ");
  340. headers += value;
  341. headers += F("\r\n");
  342. };
  343. headers += F("POST ");
  344. headers += _address.path;
  345. headers += F(" HTTP/1.1");
  346. headers += F("\r\n");
  347. const auto app = buildApp();
  348. append(F("Host"), _address.host);
  349. append(F("User-Agent"), String(app.name));
  350. append(F("Connection"), F("close"));
  351. append(F("Content-Type"), F("application/x-www-form-urlencoded"));
  352. append(F("Content-Length"), String(_data.length(), 10));
  353. headers += F("\r\n");
  354. _client->write(headers.c_str(), headers.length());
  355. _sendPendingData();
  356. }
  357. void onData(const uint8_t* data, size_t len) {
  358. if (_data.length()) {
  359. _parser_state = ParserState::End;
  360. _client->close(true);
  361. return;
  362. }
  363. PROGMEM_STRING(Status, "HTTP/1.1 200 OK");
  364. PROGMEM_STRING(Break, "\r\n\r\n");
  365. auto begin = reinterpret_cast<const char*>(data);
  366. auto end = begin + len;
  367. const char* ptr { nullptr };
  368. do {
  369. switch (_parser_state) {
  370. case ParserState::End:
  371. break;
  372. case ParserState::Init:
  373. {
  374. ptr = strnstr(begin, Status, len);
  375. if (!ptr) {
  376. _client->close(true);
  377. return;
  378. }
  379. _parser_state = ParserState::Headers;
  380. break;
  381. }
  382. case ParserState::Headers:
  383. {
  384. ptr = strnstr(ptr, Break, len);
  385. if (!ptr) {
  386. return;
  387. }
  388. ptr = ptr + __builtin_strlen(Break);
  389. _parser_state = ParserState::Body;
  390. }
  391. case ParserState::Body:
  392. {
  393. if (!ptr) {
  394. ptr = begin;
  395. }
  396. if (end - ptr) {
  397. String body;
  398. body.concat(ptr, end - ptr);
  399. _completion(body);
  400. _client->close(true);
  401. _parser_state = ParserState::End;
  402. }
  403. return;
  404. }
  405. }
  406. } while (_parser_state != ParserState::End);
  407. }
  408. static void _onDisconnected(void* ptr, AsyncClient*) {
  409. reinterpret_cast<Client*>(ptr)->onDisconnected();
  410. }
  411. static void _onConnect(void* ptr, AsyncClient*) {
  412. reinterpret_cast<Client*>(ptr)->onConnect();
  413. }
  414. static void _onTimeout(void* ptr, AsyncClient*, uint32_t timestamp) {
  415. reinterpret_cast<Client*>(ptr)->onTimeout(timestamp);
  416. }
  417. static void _onPoll(void* ptr, AsyncClient*) {
  418. reinterpret_cast<Client*>(ptr)->onPoll();
  419. }
  420. static void _onData(void* ptr, AsyncClient*, const void* data, size_t len) {
  421. reinterpret_cast<Client*>(ptr)->onData(reinterpret_cast<const uint8_t*>(data), len);
  422. }
  423. ParserState _parser_state = ParserState::Init;
  424. ClientState _client_state = ClientState::Disconnected;
  425. TimeSource::time_point _connection_start;
  426. URL _address;
  427. Completion _completion;
  428. String _data;
  429. std::unique_ptr<AsyncClient> _client;
  430. };
  431. } // namespace
  432. namespace internal {
  433. namespace {
  434. Client client;
  435. } // namespace
  436. } // namespace internal
  437. namespace {
  438. void send(const String& address, const String& data) {
  439. if (internal::client) {
  440. return;
  441. }
  442. if (!internal::client.send(address, data, maybe_retry)) {
  443. DEBUG_MSG_P(PSTR("[THINGSPEAK] Connection failed\n"));
  444. }
  445. }
  446. } // namespace
  447. } // namespace async
  448. #endif
  449. bool ready() {
  450. #if THINGSPEAK_USE_ASYNC
  451. return !static_cast<bool>(async::internal::client);
  452. #else
  453. return true;
  454. #endif
  455. }
  456. void send(const String& address, const String& data) {
  457. #if THINGSPEAK_USE_ASYNC
  458. async::send(address, data);
  459. #else
  460. sync::send(address, data);
  461. #endif
  462. }
  463. void flush() {
  464. static bool initial { true };
  465. if (!internal::flush) {
  466. return;
  467. }
  468. const auto now = TimeSource::now();
  469. if (!initial && ((now - internal::last_flush) < build::FlushInterval)) {
  470. return;
  471. }
  472. if (!ready()) {
  473. return;
  474. }
  475. initial = false;
  476. internal::last_flush = now;
  477. internal::flush = false;
  478. internal::data.reserve(build::BufferSize);
  479. if (internal::data.length()) {
  480. internal::data = "";
  481. }
  482. // Walk the fields, IDs are mapped to indexes of the array
  483. for (size_t id = 0; id < std::size(internal::fields); ++id) {
  484. if (internal::fields[id].length()) {
  485. if (internal::data.length() > 0) {
  486. internal::data.concat('&');
  487. }
  488. char buf[32] = {0};
  489. snprintf_P(buf, sizeof(buf), PSTR("field%u=%s"),
  490. (id + 1), internal::fields[id].c_str());
  491. internal::data.concat(buf);
  492. }
  493. }
  494. // POST data if any
  495. if (internal::data.length()) {
  496. internal::data.concat(F("&api_key="));
  497. internal::data.concat(settings::apiKey());
  498. send(settings::address(), internal::data);
  499. }
  500. internal::data = "";
  501. }
  502. void configure() {
  503. internal::enabled = settings::enabled();
  504. const auto key = settings::apiKey();
  505. if (internal::enabled && !key.length()) {
  506. internal::enabled = false;
  507. setSetting(FPSTR(settings::keys::Enabled), "0");
  508. }
  509. internal::clear = settings::clearCache();
  510. }
  511. void loop() {
  512. if (!internal::enabled) {
  513. return;
  514. }
  515. if (wifiConnected() || wifiConnectable()) {
  516. flush();
  517. }
  518. }
  519. } // namespace client
  520. #if WEB_SUPPORT
  521. namespace web {
  522. namespace {
  523. PROGMEM_STRING(Prefix, "tspk");
  524. bool onKeyCheck(StringView key, const JsonVariant&) {
  525. return espurna::settings::query::samePrefix(key, Prefix);
  526. }
  527. void onVisible(JsonObject& root) {
  528. bool module { false };
  529. #if RELAY_SUPPORT
  530. module = module || (relayCount() > 0);
  531. #endif
  532. #if SENSOR_SUPPORT
  533. module = module || (magnitudeCount() > 0);
  534. #endif
  535. if (module) {
  536. wsPayloadModule(root, Prefix);
  537. }
  538. }
  539. void onConnected(JsonObject& root) {
  540. root[FPSTR(settings::keys::Enabled)] = settings::enabled();
  541. root[FPSTR(settings::keys::ApiKey)] = settings::apiKey();
  542. root[FPSTR(settings::keys::ClearCache)] = settings::clearCache();
  543. root[FPSTR(settings::keys::Address)] = settings::address();
  544. #if RELAY_SUPPORT
  545. JsonArray& relays = root.createNestedArray(F("tspkRelays"));
  546. for (size_t i = 0; i < relayCount(); ++i) {
  547. relays.add(settings::relay(i));
  548. }
  549. #endif
  550. #if SENSOR_SUPPORT
  551. sensorWebSocketMagnitudes(root, Prefix, [](JsonArray& out, size_t index) {
  552. out.add(settings::magnitude(index));
  553. });
  554. #endif
  555. }
  556. void setup() {
  557. wsRegister()
  558. .onKeyCheck(onKeyCheck)
  559. .onVisible(onVisible)
  560. .onConnected(onConnected);
  561. }
  562. } // namespace
  563. } // namespace web
  564. #endif
  565. void setup() {
  566. client::configure();
  567. #if WEB_SUPPORT
  568. web::setup();
  569. #endif
  570. #if RELAY_SUPPORT
  571. relayOnStatusChange(client::onRelayStatus);
  572. for (size_t index = 0; index < relayCount(); ++index) {
  573. client::enqueueRelay(index, relayStatus(index));
  574. }
  575. #endif
  576. espurnaRegisterLoop(client::loop);
  577. espurnaRegisterReload(client::configure);
  578. }
  579. } // namespace thingspeak
  580. } // namespace espurna
  581. // -----------------------------------------------------------------------------
  582. #if RELAY_SUPPORT
  583. bool tspkEnqueueRelay(size_t index, bool status) {
  584. return ::espurna::thingspeak::client::enqueueRelay(index, status);
  585. }
  586. #endif
  587. #if SENSOR_SUPPORT
  588. bool tspkEnqueueMagnitude(unsigned char index, const String& value) {
  589. return ::espurna::thingspeak::client::enqueueMagnitude(index, value);
  590. }
  591. #endif
  592. void tspkFlush() {
  593. ::espurna::thingspeak::client::schedule_flush();
  594. }
  595. bool tspkEnabled() {
  596. return ::espurna::thingspeak::client::internal::enabled;
  597. }
  598. void tspkSetup() {
  599. ::espurna::thingspeak::setup();
  600. }
  601. #endif