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.

288 lines
8.9 KiB

  1. /*
  2. INFLUXDB MODULE
  3. Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. */
  5. #include "influxdb.h"
  6. #if INFLUXDB_SUPPORT
  7. #include <map>
  8. #include <memory>
  9. #include "broker.h"
  10. #include "ws.h"
  11. #include "terminal.h"
  12. #include "libs/AsyncClientHelpers.h"
  13. const char InfluxDb_http_success[] = "HTTP/1.1 204";
  14. const char InfluxDb_http_template[] PROGMEM = "POST /write?db=%s&u=%s&p=%s HTTP/1.1\r\nHost: %s:%u\r\nContent-Length: %d\r\n\r\n";
  15. class AsyncInfluxDB : public AsyncClient {
  16. public:
  17. constexpr static const unsigned long ClientTimeout = 5000;
  18. constexpr static const size_t DataBufferSize = 256;
  19. AsyncClientState state = AsyncClientState::Disconnected;
  20. String host;
  21. uint16_t port = 0;
  22. std::map<String, String> values;
  23. String payload;
  24. bool flush = false;
  25. uint32_t timestamp = 0;
  26. };
  27. bool _idb_enabled = false;
  28. std::unique_ptr<AsyncInfluxDB> _idb_client = nullptr;
  29. // -----------------------------------------------------------------------------
  30. void _idbInitClient() {
  31. _idb_client = std::make_unique<AsyncInfluxDB>();
  32. _idb_client->payload.reserve(AsyncInfluxDB::DataBufferSize);
  33. _idb_client->onDisconnect([](void * s, AsyncClient * ptr) {
  34. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  35. DEBUG_MSG_P(PSTR("[INFLUXDB] Disconnected\n"));
  36. client->flush = false;
  37. client->payload = "";
  38. client->timestamp = 0;
  39. client->state = AsyncClientState::Disconnected;
  40. }, nullptr);
  41. _idb_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) {
  42. DEBUG_MSG_P(PSTR("[INFLUXDB] Network timeout after %ums\n"), time);
  43. client->close(true);
  44. }, nullptr);
  45. _idb_client->onData([](void * arg, AsyncClient * ptr, void * response, size_t len) {
  46. // ref: https://docs.influxdata.com/influxdb/v1.7/tools/api/#summary-table-1
  47. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  48. if (client->state == AsyncClientState::Connected) {
  49. client->state = AsyncClientState::Disconnecting;
  50. const bool result = (len > sizeof(InfluxDb_http_success) && (0 == strncmp((char*) response, InfluxDb_http_success, strlen(InfluxDb_http_success))));
  51. DEBUG_MSG_P(PSTR("[INFLUXDB] %s response after %ums\n"), result ? "Success" : "Failure", millis() - client->timestamp);
  52. client->timestamp = millis();
  53. client->close();
  54. }
  55. }, nullptr);
  56. _idb_client->onPoll([](void * arg, AsyncClient * ptr) {
  57. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  58. unsigned long ts = millis() - client->timestamp;
  59. if (ts > AsyncInfluxDB::ClientTimeout) {
  60. DEBUG_MSG_P(PSTR("[INFLUXDB] No response after %ums\n"), ts);
  61. client->close(true);
  62. return;
  63. }
  64. if (client->payload.length()) {
  65. client->write(client->payload.c_str(), client->payload.length());
  66. client->payload = "";
  67. }
  68. });
  69. _idb_client->onConnect([](void * arg, AsyncClient * ptr) {
  70. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  71. client->timestamp = millis();
  72. client->state = AsyncClientState::Connected;
  73. DEBUG_MSG_P(PSTR("[INFLUXDB] Connected to %s:%u\n"),
  74. IPAddress(client->getRemoteAddress()).toString().c_str(),
  75. client->getRemotePort()
  76. );
  77. constexpr const int BUFFER_SIZE = 256;
  78. char headers[BUFFER_SIZE];
  79. int len = snprintf_P(headers, sizeof(headers), InfluxDb_http_template,
  80. getSetting("idbDatabase", INFLUXDB_DATABASE).c_str(),
  81. getSetting("idbUsername", INFLUXDB_USERNAME).c_str(),
  82. getSetting("idbPassword", INFLUXDB_PASSWORD).c_str(),
  83. client->host.c_str(), client->port, client->payload.length()
  84. );
  85. if ((len < 0) || (len > BUFFER_SIZE - 1)) {
  86. client->close(true);
  87. return;
  88. }
  89. client->write(headers, len);
  90. });
  91. }
  92. // -----------------------------------------------------------------------------
  93. bool _idbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  94. return (strncmp(key, "idb", 3) == 0);
  95. }
  96. void _idbWebSocketOnVisible(JsonObject& root) {
  97. root["idbVisible"] = 1;
  98. }
  99. void _idbWebSocketOnConnected(JsonObject& root) {
  100. root["idbEnabled"] = getSetting("idbEnabled", 1 == INFLUXDB_ENABLED);
  101. root["idbHost"] = getSetting("idbHost", INFLUXDB_HOST);
  102. root["idbPort"] = getSetting("idbPort", INFLUXDB_PORT);
  103. root["idbDatabase"] = getSetting("idbDatabase", INFLUXDB_DATABASE);
  104. root["idbUsername"] = getSetting("idbUsername", INFLUXDB_USERNAME);
  105. root["idbPassword"] = getSetting("idbPassword", INFLUXDB_PASSWORD);
  106. }
  107. void _idbConfigure() {
  108. _idb_enabled = getSetting("idbEnabled", 1 == INFLUXDB_ENABLED);
  109. if (_idb_enabled && (getSetting("idbHost", INFLUXDB_HOST).length() == 0)) {
  110. _idb_enabled = false;
  111. setSetting("idbEnabled", 0);
  112. }
  113. if (_idb_enabled && !_idb_client) _idbInitClient();
  114. }
  115. #if BROKER_SUPPORT
  116. void _idbBrokerSensor(const String& topic, unsigned char id, double, const char* value) {
  117. idbSend(topic.c_str(), id, value);
  118. }
  119. void _idbBrokerStatus(const String& topic, unsigned char id, unsigned int value) {
  120. idbSend(topic.c_str(), id, String(int(value)).c_str());
  121. }
  122. #endif // BROKER_SUPPORT
  123. // -----------------------------------------------------------------------------
  124. bool idbSend(const char * topic, const char * payload) {
  125. if (!_idb_enabled) return false;
  126. if (_idb_client->state != AsyncClientState::Disconnected) return false;
  127. _idb_client->values[topic] = payload;
  128. _idb_client->flush = true;
  129. return true;
  130. }
  131. void _idbSend(const String& host, const uint16_t port) {
  132. if (_idb_client->state != AsyncClientState::Disconnected) return;
  133. DEBUG_MSG_P(PSTR("[INFLUXDB] Sending to %s:%u\n"), host.c_str(), port);
  134. // TODO: cache `Host: <host>:<port>` header instead of storing things separately?
  135. _idb_client->host = host;
  136. _idb_client->port = port;
  137. _idb_client->timestamp = millis();
  138. _idb_client->state = _idb_client->connect(host.c_str(), port)
  139. ? AsyncClientState::Connecting
  140. : AsyncClientState::Disconnected;
  141. if (_idb_client->state == AsyncClientState::Disconnected) {
  142. DEBUG_MSG_P(PSTR("[INFLUXDB] Connection to %s:%u failed\n"), host.c_str(), port);
  143. _idb_client->close(true);
  144. }
  145. }
  146. void _idbFlush() {
  147. // Clean-up client object when not in use
  148. if (_idb_client && !_idb_enabled && (_idb_client->state == AsyncClientState::Disconnected)) {
  149. _idb_client = nullptr;
  150. }
  151. // Wait until current connection is finished
  152. if (!_idb_client) return;
  153. if (!_idb_client->flush) return;
  154. if (_idb_client->state != AsyncClientState::Disconnected) return;
  155. // Wait until connected
  156. if (!wifiConnected()) return;
  157. const auto host = getSetting("idbHost", INFLUXDB_HOST);
  158. const auto port = getSetting("idbPort", static_cast<uint16_t>(INFLUXDB_PORT));
  159. // TODO: should we always store specific pairs like tspk keeps relay / sensor readings?
  160. // note that we also send heartbeat data, persistent values should be flagged
  161. const String device = getSetting("hostname");
  162. _idb_client->payload = "";
  163. for (auto& pair : _idb_client->values) {
  164. if (!isNumber(pair.second.c_str())) {
  165. String quoted;
  166. quoted.reserve(pair.second.length() + 2);
  167. quoted += '"';
  168. quoted += pair.second;
  169. quoted += '"';
  170. pair.second = quoted;
  171. }
  172. char buffer[128] = {0};
  173. snprintf_P(buffer, sizeof(buffer),
  174. PSTR("%s,device=%s value=%s\n"),
  175. pair.first.c_str(), device.c_str(), pair.second.c_str()
  176. );
  177. _idb_client->payload += buffer;
  178. }
  179. _idb_client->values.clear();
  180. _idbSend(host, port);
  181. }
  182. bool idbSend(const char * topic, unsigned char id, const char * payload) {
  183. char measurement[64];
  184. snprintf(measurement, sizeof(measurement), "%s,id=%d", topic, id);
  185. return idbSend(measurement, payload);
  186. }
  187. bool idbEnabled() {
  188. return _idb_enabled;
  189. }
  190. void idbSetup() {
  191. _idbConfigure();
  192. #if WEB_SUPPORT
  193. wsRegister()
  194. .onVisible(_idbWebSocketOnVisible)
  195. .onConnected(_idbWebSocketOnConnected)
  196. .onKeyCheck(_idbWebSocketOnKeyCheck);
  197. #endif
  198. #if BROKER_SUPPORT
  199. StatusBroker::Register(_idbBrokerStatus);
  200. SensorReportBroker::Register(_idbBrokerSensor);
  201. #endif
  202. espurnaRegisterReload(_idbConfigure);
  203. espurnaRegisterLoop(_idbFlush);
  204. #if TERMINAL_SUPPORT
  205. terminalRegisterCommand(F("IDB.SEND"), [](Embedis* e) {
  206. if (e->argc != 4) {
  207. terminalError(F("idb.send <topic> <id> <value>"));
  208. return;
  209. }
  210. const String topic = e->argv[1];
  211. const auto id = atoi(e->argv[2]);
  212. const String value = e->argv[3];
  213. idbSend(topic.c_str(), id, value.c_str());
  214. });
  215. #endif
  216. }
  217. #endif