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.

318 lines
9.9 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 "mqtt.h"
  10. #include "rpc.h"
  11. #include "relay.h"
  12. #include "sensor.h"
  13. #include "terminal.h"
  14. #include "ws.h"
  15. #include <ESPAsyncTCP.h>
  16. #include "libs/AsyncClientHelpers.h"
  17. const char InfluxDb_http_success[] = "HTTP/1.1 204";
  18. 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";
  19. class AsyncInfluxDB : public AsyncClient {
  20. public:
  21. constexpr static const unsigned long ClientTimeout = 5000;
  22. constexpr static const size_t DataBufferSize = 256;
  23. AsyncClientState state = AsyncClientState::Disconnected;
  24. String host;
  25. uint16_t port = 0;
  26. std::map<String, String> values;
  27. String payload;
  28. bool flush = false;
  29. uint32_t timestamp = 0;
  30. };
  31. bool _idb_enabled = false;
  32. std::unique_ptr<AsyncInfluxDB> _idb_client = nullptr;
  33. // -----------------------------------------------------------------------------
  34. void _idbInitClient() {
  35. _idb_client = std::make_unique<AsyncInfluxDB>();
  36. _idb_client->payload.reserve(AsyncInfluxDB::DataBufferSize);
  37. _idb_client->onDisconnect([](void * s, AsyncClient * ptr) {
  38. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  39. DEBUG_MSG_P(PSTR("[INFLUXDB] Disconnected\n"));
  40. client->flush = false;
  41. client->payload = "";
  42. client->timestamp = 0;
  43. client->state = AsyncClientState::Disconnected;
  44. }, nullptr);
  45. _idb_client->onTimeout([](void * s, AsyncClient * client, uint32_t time) {
  46. DEBUG_MSG_P(PSTR("[INFLUXDB] Network timeout after %ums\n"), time);
  47. client->close(true);
  48. }, nullptr);
  49. _idb_client->onData([](void * arg, AsyncClient * ptr, void * response, size_t len) {
  50. // ref: https://docs.influxdata.com/influxdb/v1.7/tools/api/#summary-table-1
  51. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  52. if (client->state == AsyncClientState::Connected) {
  53. client->state = AsyncClientState::Disconnecting;
  54. const bool result = (len > sizeof(InfluxDb_http_success) && (0 == strncmp((char*) response, InfluxDb_http_success, strlen(InfluxDb_http_success))));
  55. DEBUG_MSG_P(PSTR("[INFLUXDB] %s response after %ums\n"), result ? "Success" : "Failure", millis() - client->timestamp);
  56. client->timestamp = millis();
  57. client->close();
  58. }
  59. }, nullptr);
  60. _idb_client->onPoll([](void * arg, AsyncClient * ptr) {
  61. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  62. unsigned long ts = millis() - client->timestamp;
  63. if (ts > AsyncInfluxDB::ClientTimeout) {
  64. DEBUG_MSG_P(PSTR("[INFLUXDB] No response after %ums\n"), ts);
  65. client->close(true);
  66. return;
  67. }
  68. if (client->payload.length()) {
  69. client->write(client->payload.c_str(), client->payload.length());
  70. client->payload = "";
  71. }
  72. });
  73. _idb_client->onConnect([](void * arg, AsyncClient * ptr) {
  74. auto *client = reinterpret_cast<AsyncInfluxDB*>(ptr);
  75. client->timestamp = millis();
  76. client->state = AsyncClientState::Connected;
  77. DEBUG_MSG_P(PSTR("[INFLUXDB] Connected to %s:%u\n"),
  78. IPAddress(client->getRemoteAddress()).toString().c_str(),
  79. client->getRemotePort()
  80. );
  81. constexpr const int BUFFER_SIZE = 256;
  82. char headers[BUFFER_SIZE];
  83. int len = snprintf_P(headers, sizeof(headers), InfluxDb_http_template,
  84. getSetting("idbDatabase", INFLUXDB_DATABASE).c_str(),
  85. getSetting("idbUsername", INFLUXDB_USERNAME).c_str(),
  86. getSetting("idbPassword", INFLUXDB_PASSWORD).c_str(),
  87. client->host.c_str(), client->port, client->payload.length()
  88. );
  89. if ((len < 0) || (len > BUFFER_SIZE - 1)) {
  90. client->close(true);
  91. return;
  92. }
  93. client->write(headers, len);
  94. });
  95. }
  96. // -----------------------------------------------------------------------------
  97. bool _idbWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  98. return (strncmp(key, "idb", 3) == 0);
  99. }
  100. void _idbWebSocketOnVisible(JsonObject& root) {
  101. root["idbVisible"] = 1;
  102. }
  103. void _idbWebSocketOnConnected(JsonObject& root) {
  104. root["idbEnabled"] = getSetting("idbEnabled", 1 == INFLUXDB_ENABLED);
  105. root["idbHost"] = getSetting("idbHost", INFLUXDB_HOST);
  106. root["idbPort"] = getSetting("idbPort", INFLUXDB_PORT);
  107. root["idbDatabase"] = getSetting("idbDatabase", INFLUXDB_DATABASE);
  108. root["idbUsername"] = getSetting("idbUsername", INFLUXDB_USERNAME);
  109. root["idbPassword"] = getSetting("idbPassword", INFLUXDB_PASSWORD);
  110. }
  111. void _idbConfigure() {
  112. _idb_enabled = getSetting("idbEnabled", 1 == INFLUXDB_ENABLED);
  113. if (_idb_enabled && (getSetting("idbHost", INFLUXDB_HOST).length() == 0)) {
  114. _idb_enabled = false;
  115. setSetting("idbEnabled", 0);
  116. }
  117. if (_idb_enabled && !_idb_client) _idbInitClient();
  118. }
  119. void _idbSendSensor(const String& topic, unsigned char id, double, const char* value) {
  120. idbSend(topic.c_str(), id, value);
  121. }
  122. void _idbSendStatus(size_t id, bool status) {
  123. idbSend(MQTT_TOPIC_RELAY, id, status ? "1" : "0"); // "status" ?
  124. }
  125. // -----------------------------------------------------------------------------
  126. bool idbSend(const char * topic, const char * payload) {
  127. if (!_idb_enabled) return false;
  128. if (_idb_client->state != AsyncClientState::Disconnected) return false;
  129. _idb_client->values[topic] = payload;
  130. _idb_client->flush = true;
  131. return true;
  132. }
  133. void _idbSend(const String& host, const uint16_t port) {
  134. if (_idb_client->state != AsyncClientState::Disconnected) return;
  135. DEBUG_MSG_P(PSTR("[INFLUXDB] Sending to %s:%u\n"), host.c_str(), port);
  136. // TODO: cache `Host: <host>:<port>` header instead of storing things separately?
  137. _idb_client->host = host;
  138. _idb_client->port = port;
  139. _idb_client->timestamp = millis();
  140. _idb_client->state = _idb_client->connect(host.c_str(), port)
  141. ? AsyncClientState::Connecting
  142. : AsyncClientState::Disconnected;
  143. if (_idb_client->state == AsyncClientState::Disconnected) {
  144. DEBUG_MSG_P(PSTR("[INFLUXDB] Connection to %s:%u failed\n"), host.c_str(), port);
  145. _idb_client->close(true);
  146. }
  147. }
  148. void _idbFlush() {
  149. // Clean-up client object when not in use
  150. if (_idb_client && !_idb_enabled && (_idb_client->state == AsyncClientState::Disconnected)) {
  151. _idb_client = nullptr;
  152. }
  153. // Wait until current connection is finished
  154. if (!_idb_client) return;
  155. if (!_idb_client->flush) return;
  156. if (_idb_client->state != AsyncClientState::Disconnected) return;
  157. // Wait until connected
  158. if (!wifiConnected()) return;
  159. const auto host = getSetting("idbHost", INFLUXDB_HOST);
  160. const auto port = getSetting("idbPort", static_cast<uint16_t>(INFLUXDB_PORT));
  161. // TODO: should we always store specific pairs like tspk keeps relay / sensor readings?
  162. // note that we also send heartbeat data, persistent values should be flagged
  163. const String device = getSetting("hostname");
  164. _idb_client->payload = "";
  165. for (auto& pair : _idb_client->values) {
  166. if (!isNumber(pair.second)) {
  167. String quoted;
  168. quoted.reserve(pair.second.length() + 2);
  169. quoted += '"';
  170. quoted += pair.second;
  171. quoted += '"';
  172. pair.second = quoted;
  173. }
  174. char buffer[128] = {0};
  175. snprintf_P(buffer, sizeof(buffer),
  176. PSTR("%s,device=%s value=%s\n"),
  177. pair.first.c_str(), device.c_str(), pair.second.c_str()
  178. );
  179. _idb_client->payload += buffer;
  180. }
  181. _idb_client->values.clear();
  182. _idbSend(host, port);
  183. }
  184. bool idbSend(const char * topic, unsigned char id, const char * payload) {
  185. char measurement[64];
  186. snprintf(measurement, sizeof(measurement), "%s,id=%d", topic, id);
  187. return idbSend(measurement, payload);
  188. }
  189. bool idbEnabled() {
  190. return _idb_enabled;
  191. }
  192. bool _idbHeartbeat(heartbeat::Mask mask) {
  193. if (mask & heartbeat::Report::Uptime)
  194. idbSend(MQTT_TOPIC_UPTIME, String(systemUptime()).c_str());
  195. if (mask & heartbeat::Report::Freeheap) {
  196. auto stats = systemHeapStats();
  197. idbSend(MQTT_TOPIC_FREEHEAP, String(stats.available).c_str());
  198. }
  199. if (mask & heartbeat::Report::Rssi)
  200. idbSend(MQTT_TOPIC_RSSI, String(WiFi.RSSI()).c_str());
  201. if ((mask & heartbeat::Report::Vcc) && (ADC_MODE_VALUE == ADC_VCC))
  202. idbSend(MQTT_TOPIC_VCC, String(ESP.getVcc()).c_str());
  203. if (mask & heartbeat::Report::Loadavg)
  204. idbSend(MQTT_TOPIC_LOADAVG, String(systemLoadAverage()).c_str());
  205. if (mask & heartbeat::Report::Ssid)
  206. idbSend(MQTT_TOPIC_SSID, WiFi.SSID().c_str());
  207. if (mask & heartbeat::Report::Bssid)
  208. idbSend(MQTT_TOPIC_BSSID, WiFi.BSSIDstr().c_str());
  209. return true;
  210. }
  211. void idbSetup() {
  212. systemHeartbeat(_idbHeartbeat);
  213. systemHeartbeat(_idbHeartbeat,
  214. getSetting("idbHbMode", heartbeat::currentMode()),
  215. getSetting("idbHbIntvl", heartbeat::currentInterval()));
  216. _idbConfigure();
  217. #if WEB_SUPPORT
  218. wsRegister()
  219. .onVisible(_idbWebSocketOnVisible)
  220. .onConnected(_idbWebSocketOnConnected)
  221. .onKeyCheck(_idbWebSocketOnKeyCheck);
  222. #endif
  223. #if RELAY_SUPPORT
  224. relaySetStatusChange(_idbSendStatus);
  225. #endif
  226. #if SENSOR_SUPPORT
  227. sensorSetMagnitudeReport(_idbSendSensor);
  228. #endif
  229. espurnaRegisterReload(_idbConfigure);
  230. espurnaRegisterLoop(_idbFlush);
  231. #if TERMINAL_SUPPORT
  232. terminalRegisterCommand(F("IDB.SEND"), [](const terminal::CommandContext& ctx) {
  233. if (ctx.argc != 4) {
  234. terminalError(F("idb.send <topic> <id> <value>"));
  235. return;
  236. }
  237. idbSend(ctx.argv[1].c_str(), ctx.argv[2].toInt(), ctx.argv[3].c_str());
  238. });
  239. #endif
  240. }
  241. #endif