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.

283 lines
8.8 KiB

Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
  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 "rpc.h"
  11. #include "sensor.h"
  12. #include "terminal.h"
  13. #include "ws.h"
  14. #include "libs/AsyncClientHelpers.h"
  15. const char InfluxDb_http_success[] = "HTTP/1.1 204";
  16. 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";
  17. class AsyncInfluxDB : public AsyncClient {
  18. public:
  19. constexpr static const unsigned long ClientTimeout = 5000;
  20. constexpr static const size_t DataBufferSize = 256;
  21. AsyncClientState state = AsyncClientState::Disconnected;
  22. String host;
  23. uint16_t port = 0;
  24. std::map<String, String> values;
  25. String payload;
  26. bool flush = false;
  27. uint32_t timestamp = 0;
  28. };
  29. bool _idb_enabled = false;
  30. std::unique_ptr<AsyncInfluxDB> _idb_client = nullptr;
  31. // -----------------------------------------------------------------------------
  32. void _idbInitClient() {
  33. _idb_client = std::make_unique<AsyncInfluxDB>();
  34. _idb_client->payload.reserve(AsyncInfluxDB::DataBufferSize);
  35. _idb_client->onDisconnect([](void * s, AsyncClient * ptr) {
  36. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  37. DEBUG_MSG_P(PSTR("[INFLUXDB] Disconnected\n"));
  38. client->flush = false;
  39. client->payload = "";
  40. client->timestamp = 0;
  41. client->state = AsyncClientState::Disconnected;
  42. }, nullptr);
  43. _idb_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) {
  44. DEBUG_MSG_P(PSTR("[INFLUXDB] Network timeout after %ums\n"), time);
  45. client->close(true);
  46. }, nullptr);
  47. _idb_client->onData([](void * arg, AsyncClient * ptr, void * response, size_t len) {
  48. // ref: https://docs.influxdata.com/influxdb/v1.7/tools/api/#summary-table-1
  49. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  50. if (client->state == AsyncClientState::Connected) {
  51. client->state = AsyncClientState::Disconnecting;
  52. const bool result = (len > sizeof(InfluxDb_http_success) && (0 == strncmp((char*) response, InfluxDb_http_success, strlen(InfluxDb_http_success))));
  53. DEBUG_MSG_P(PSTR("[INFLUXDB] %s response after %ums\n"), result ? "Success" : "Failure", millis() - client->timestamp);
  54. client->timestamp = millis();
  55. client->close();
  56. }
  57. }, nullptr);
  58. _idb_client->onPoll([](void * arg, AsyncClient * ptr) {
  59. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  60. unsigned long ts = millis() - client->timestamp;
  61. if (ts > AsyncInfluxDB::ClientTimeout) {
  62. DEBUG_MSG_P(PSTR("[INFLUXDB] No response after %ums\n"), ts);
  63. client->close(true);
  64. return;
  65. }
  66. if (client->payload.length()) {
  67. client->write(client->payload.c_str(), client->payload.length());
  68. client->payload = "";
  69. }
  70. });
  71. _idb_client->onConnect([](void * arg, AsyncClient * ptr) {
  72. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  73. client->timestamp = millis();
  74. client->state = AsyncClientState::Connected;
  75. DEBUG_MSG_P(PSTR("[INFLUXDB] Connected to %s:%u\n"),
  76. IPAddress(client->getRemoteAddress()).toString().c_str(),
  77. client->getRemotePort()
  78. );
  79. constexpr const int BUFFER_SIZE = 256;
  80. char headers[BUFFER_SIZE];
  81. int len = snprintf_P(headers, sizeof(headers), InfluxDb_http_template,
  82. getSetting("idbDatabase", INFLUXDB_DATABASE).c_str(),
  83. getSetting("idbUsername", INFLUXDB_USERNAME).c_str(),
  84. getSetting("idbPassword", INFLUXDB_PASSWORD).c_str(),
  85. client->host.c_str(), client->port, client->payload.length()
  86. );
  87. if ((len < 0) || (len > BUFFER_SIZE - 1)) {
  88. client->close(true);
  89. return;
  90. }
  91. client->write(headers, len);
  92. });
  93. }
  94. // -----------------------------------------------------------------------------
  95. bool _idbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  96. return (strncmp(key, "idb", 3) == 0);
  97. }
  98. void _idbWebSocketOnVisible(JsonObject& root) {
  99. root["idbVisible"] = 1;
  100. }
  101. void _idbWebSocketOnConnected(JsonObject& root) {
  102. root["idbEnabled"] = getSetting("idbEnabled", 1 == INFLUXDB_ENABLED);
  103. root["idbHost"] = getSetting("idbHost", INFLUXDB_HOST);
  104. root["idbPort"] = getSetting("idbPort", INFLUXDB_PORT);
  105. root["idbDatabase"] = getSetting("idbDatabase", INFLUXDB_DATABASE);
  106. root["idbUsername"] = getSetting("idbUsername", INFLUXDB_USERNAME);
  107. root["idbPassword"] = getSetting("idbPassword", INFLUXDB_PASSWORD);
  108. }
  109. void _idbConfigure() {
  110. _idb_enabled = getSetting("idbEnabled", 1 == INFLUXDB_ENABLED);
  111. if (_idb_enabled && (getSetting("idbHost", INFLUXDB_HOST).length() == 0)) {
  112. _idb_enabled = false;
  113. setSetting("idbEnabled", 0);
  114. }
  115. if (_idb_enabled && !_idb_client) _idbInitClient();
  116. }
  117. void _idbBrokerSensor(const String& topic, unsigned char id, double, const char* value) {
  118. idbSend(topic.c_str(), id, value);
  119. }
  120. void _idbBrokerStatus(const String& topic, unsigned char id, unsigned int value) {
  121. idbSend(topic.c_str(), id, String(int(value)).c_str());
  122. }
  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. StatusBroker::Register(_idbBrokerStatus);
  199. #if SENSOR_SUPPORT
  200. SensorReportBroker::Register(_idbBrokerSensor);
  201. #endif
  202. espurnaRegisterReload(_idbConfigure);
  203. espurnaRegisterLoop(_idbFlush);
  204. #if TERMINAL_SUPPORT
  205. terminalRegisterCommand(F("IDB.SEND"), [](const terminal::CommandContext& ctx) {
  206. if (ctx.argc != 4) {
  207. terminalError(F("idb.send <topic> <id> <value>"));
  208. return;
  209. }
  210. idbSend(ctx.argv[1].c_str(), ctx.argv[2].toInt(), ctx.argv[3].c_str());
  211. });
  212. #endif
  213. }
  214. #endif