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.

406 lines
12 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
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. RPN RULES MODULE
  3. Use RPNLib library (https://github.com/xoseperez/rpnlib)
  4. Copyright (C) 2019 by Xose Pérez <xose dot perez at gmail dot com>
  5. */
  6. #include "rpnrules.h"
  7. #if RPN_RULES_SUPPORT
  8. #include "broker.h"
  9. #include "mqtt.h"
  10. #include "ntp.h"
  11. #include "relay.h"
  12. #include "rpc.h"
  13. #include "sensor.h"
  14. #include "terminal.h"
  15. #include "ws.h"
  16. // -----------------------------------------------------------------------------
  17. // Custom commands
  18. // -----------------------------------------------------------------------------
  19. rpn_context _rpn_ctxt;
  20. bool _rpn_run = false;
  21. unsigned long _rpn_delay = RPN_DELAY;
  22. unsigned long _rpn_last = 0;
  23. // -----------------------------------------------------------------------------
  24. bool _rpnWebSocketOnKeyCheck(const char * key, JsonVariant& value) {
  25. return (strncmp(key, "rpn", 3) == 0);
  26. }
  27. void _rpnWebSocketOnConnected(JsonObject& root) {
  28. root["rpnSticky"] = getSetting("rpnSticky", 1 == RPN_STICKY);
  29. root["rpnDelay"] = getSetting("rpnDelay", RPN_DELAY);
  30. JsonArray& rules = root.createNestedArray("rpnRules");
  31. unsigned char i = 0;
  32. String rule = getSetting({"rpnRule", i});
  33. while (rule.length()) {
  34. rules.add(rule);
  35. rule = getSetting({"rpnRule", ++i});
  36. }
  37. #if MQTT_SUPPORT
  38. i=0;
  39. JsonArray& topics = root.createNestedArray("rpnTopics");
  40. JsonArray& names = root.createNestedArray("rpnNames");
  41. String rpn_topic = getSetting({"rpnTopic", i});
  42. while (rpn_topic.length() > 0) {
  43. String rpn_name = getSetting({"rpnName", i});
  44. topics.add(rpn_topic);
  45. names.add(rpn_name);
  46. rpn_topic = getSetting({"rpnTopic", ++i});
  47. }
  48. #endif
  49. }
  50. #if MQTT_SUPPORT
  51. void _rpnMQTTSubscribe() {
  52. unsigned char i = 0;
  53. String rpn_topic = getSetting({"rpnTopic", i});
  54. while (rpn_topic.length()) {
  55. mqttSubscribeRaw(rpn_topic.c_str());
  56. rpn_topic = getSetting({"rpnTopic", ++i});
  57. }
  58. }
  59. void _rpnMQTTCallback(unsigned int type, const char * topic, const char * payload) {
  60. if (type == MQTT_CONNECT_EVENT) {
  61. _rpnMQTTSubscribe();
  62. }
  63. if (type == MQTT_MESSAGE_EVENT) {
  64. unsigned char i = 0;
  65. String rpn_topic = getSetting({"rpnTopic", i});
  66. while (rpn_topic.length()) {
  67. if (rpn_topic.equals(topic)) {
  68. String rpn_name = getSetting({"rpnName", i});
  69. if (rpn_name.length()) {
  70. rpn_variable_set(_rpn_ctxt, rpn_name.c_str(), atof(payload));
  71. _rpn_last = millis();
  72. _rpn_run = true;
  73. break;
  74. }
  75. }
  76. rpn_topic = getSetting({"rpnTopic", ++i});
  77. }
  78. }
  79. }
  80. #endif // MQTT_SUPPORT
  81. void _rpnConfigure() {
  82. #if MQTT_SUPPORT
  83. if (mqttConnected()) _rpnMQTTSubscribe();
  84. #endif
  85. _rpn_delay = getSetting("rpnDelay", RPN_DELAY);
  86. }
  87. void _rpnBrokerCallback(const String& topic, unsigned char id, double value, const char*) {
  88. char name[32] = {0};
  89. snprintf(name, sizeof(name), "%s%u", topic.c_str(), id);
  90. rpn_variable_set(_rpn_ctxt, name, value);
  91. _rpn_last = millis();
  92. _rpn_run = true;
  93. }
  94. void _rpnBrokerStatus(const String& topic, unsigned char id, unsigned int value) {
  95. _rpnBrokerCallback(topic, id, double(value), nullptr);
  96. }
  97. #if NTP_SUPPORT
  98. bool _rpnNtpNow(rpn_context & ctxt) {
  99. if (!ntpSynced()) return false;
  100. rpn_stack_push(ctxt, now());
  101. return true;
  102. }
  103. bool _rpnNtpFunc(rpn_context & ctxt, int (*func)(time_t)) {
  104. float timestamp;
  105. rpn_stack_pop(ctxt, timestamp);
  106. rpn_stack_push(ctxt, func(time_t(timestamp)));
  107. return true;
  108. }
  109. #endif
  110. void _rpnDump() {
  111. float value;
  112. DEBUG_MSG_P(PSTR("[RPN] Stack:\n"));
  113. unsigned char num = rpn_stack_size(_rpn_ctxt);
  114. if (0 == num) {
  115. DEBUG_MSG_P(PSTR(" (empty)\n"));
  116. } else {
  117. unsigned char index = num - 1;
  118. while (rpn_stack_get(_rpn_ctxt, index, value)) {
  119. DEBUG_MSG_P(PSTR(" %02d: %s\n"), index--, String(value).c_str());
  120. }
  121. }
  122. }
  123. void _rpnInit() {
  124. // Init context
  125. rpn_init(_rpn_ctxt);
  126. // Time functions need NTP support
  127. // TODO: since 1.14.2, timelib+ntpclientlib are no longer used with latest Cores
  128. // `now` is always in UTC, `utc_...` functions to be used instead to convert time
  129. #if NTP_SUPPORT && !NTP_LEGACY_SUPPORT
  130. rpn_operator_set(_rpn_ctxt, "utc", 0, _rpnNtpNow);
  131. rpn_operator_set(_rpn_ctxt, "now", 0, _rpnNtpNow);
  132. rpn_operator_set(_rpn_ctxt, "utc_month", 1, [](rpn_context & ctxt) {
  133. return _rpnNtpFunc(ctxt, utc_month);
  134. });
  135. rpn_operator_set(_rpn_ctxt, "month", 1, [](rpn_context & ctxt) {
  136. return _rpnNtpFunc(ctxt, month);
  137. });
  138. rpn_operator_set(_rpn_ctxt, "utc_day", 1, [](rpn_context & ctxt) {
  139. return _rpnNtpFunc(ctxt, utc_day);
  140. });
  141. rpn_operator_set(_rpn_ctxt, "day", 1, [](rpn_context & ctxt) {
  142. return _rpnNtpFunc(ctxt, day);
  143. });
  144. rpn_operator_set(_rpn_ctxt, "utc_dow", 1, [](rpn_context & ctxt) {
  145. return _rpnNtpFunc(ctxt, utc_weekday);
  146. });
  147. rpn_operator_set(_rpn_ctxt, "dow", 1, [](rpn_context & ctxt) {
  148. return _rpnNtpFunc(ctxt, weekday);
  149. });
  150. rpn_operator_set(_rpn_ctxt, "utc_hour", 1, [](rpn_context & ctxt) {
  151. return _rpnNtpFunc(ctxt, utc_hour);
  152. });
  153. rpn_operator_set(_rpn_ctxt, "hour", 1, [](rpn_context & ctxt) {
  154. return _rpnNtpFunc(ctxt, hour);
  155. });
  156. rpn_operator_set(_rpn_ctxt, "utc_minute", 1, [](rpn_context & ctxt) {
  157. return _rpnNtpFunc(ctxt, utc_minute);
  158. });
  159. rpn_operator_set(_rpn_ctxt, "minute", 1, [](rpn_context & ctxt) {
  160. return _rpnNtpFunc(ctxt, minute);
  161. });
  162. #endif
  163. // TODO: 1.14.0 weekday(...) conversion seemed to have 0..6 range with Monday as 0
  164. // using classic Sunday as first, but instead of 0 it is 1
  165. // Implementation above also uses 1 for Sunday, staying compatible with TimeLib
  166. #if NTP_SUPPORT && NTP_LEGACY_SUPPORT
  167. rpn_operator_set(_rpn_ctxt, "utc", 0, [](rpn_context & ctxt) {
  168. if (!ntpSynced()) return false;
  169. rpn_stack_push(ctxt, ntpLocal2UTC(now()));
  170. return true;
  171. });
  172. rpn_operator_set(_rpn_ctxt, "now", 0, _rpnNtpNow);
  173. rpn_operator_set(_rpn_ctxt, "month", 1, [](rpn_context & ctxt) {
  174. return _rpnNtpFunc(ctxt, month);
  175. });
  176. rpn_operator_set(_rpn_ctxt, "day", 1, [](rpn_context & ctxt) {
  177. return _rpnNtpFunc(ctxt, day);
  178. });
  179. rpn_operator_set(_rpn_ctxt, "dow", 1, [](rpn_context & ctxt) {
  180. return _rpnNtpFunc(ctxt, weekday);
  181. });
  182. rpn_operator_set(_rpn_ctxt, "hour", 1, [](rpn_context & ctxt) {
  183. return _rpnNtpFunc(ctxt, hour);
  184. });
  185. rpn_operator_set(_rpn_ctxt, "minute", 1, [](rpn_context & ctxt) {
  186. return _rpnNtpFunc(ctxt, minute);
  187. });
  188. #endif
  189. // Dumps RPN stack contents
  190. rpn_operator_set(_rpn_ctxt, "debug", 0, [](rpn_context & ctxt) {
  191. _rpnDump();
  192. return true;
  193. });
  194. // Accept relay number and numeric API status value (0, 1 and 2)
  195. #if RELAY_SUPPORT
  196. rpn_operator_set(_rpn_ctxt, "relay", 2, [](rpn_context & ctxt) {
  197. float status, id;
  198. rpn_stack_pop(ctxt, id);
  199. rpn_stack_pop(ctxt, status);
  200. if (int(status) == 2) {
  201. relayToggle(int(id));
  202. } else {
  203. relayStatus(int(id), int(status) == 1);
  204. }
  205. return true;
  206. });
  207. #endif // RELAY_SUPPORT == 1
  208. // Channel operators
  209. #if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT
  210. rpn_operator_set(_rpn_ctxt, "update", 0, [](rpn_context & ctxt) {
  211. lightUpdate(true, true);
  212. return true;
  213. });
  214. rpn_operator_set(_rpn_ctxt, "black", 0, [](rpn_context & ctxt) {
  215. lightColor((unsigned long) 0);
  216. return true;
  217. });
  218. rpn_operator_set(_rpn_ctxt, "channel", 2, [](rpn_context & ctxt) {
  219. float value, id;
  220. rpn_stack_pop(ctxt, id);
  221. rpn_stack_pop(ctxt, value);
  222. lightChannel(int(id), int(value));
  223. return true;
  224. });
  225. #endif
  226. }
  227. #if TERMINAL_SUPPORT
  228. void _rpnInitCommands() {
  229. terminalRegisterCommand(F("RPN.VARS"), [](const terminal::CommandContext&) {
  230. unsigned char num = rpn_variables_size(_rpn_ctxt);
  231. if (0 == num) {
  232. DEBUG_MSG_P(PSTR("[RPN] No variables\n"));
  233. } else {
  234. DEBUG_MSG_P(PSTR("[RPN] Variables:\n"));
  235. for (unsigned char i=0; i<num; i++) {
  236. char * name = rpn_variable_name(_rpn_ctxt, i);
  237. float value;
  238. rpn_variable_get(_rpn_ctxt, name, value);
  239. DEBUG_MSG_P(PSTR(" %s: %s\n"), name, String(value).c_str());
  240. }
  241. }
  242. terminalOK();
  243. });
  244. terminalRegisterCommand(F("RPN.OPS"), [](const terminal::CommandContext&) {
  245. unsigned char num = _rpn_ctxt.operators.size();
  246. DEBUG_MSG_P(PSTR("[RPN] Operators:\n"));
  247. for (unsigned char i=0; i<num; i++) {
  248. DEBUG_MSG_P(PSTR(" %s (%d)\n"), _rpn_ctxt.operators[i].name, _rpn_ctxt.operators[i].argc);
  249. }
  250. terminalOK();
  251. });
  252. terminalRegisterCommand(F("RPN.TEST"), [](const terminal::CommandContext& ctx) {
  253. if (ctx.argc == 2) {
  254. DEBUG_MSG_P(PSTR("[RPN] Running \"%s\"\n"), ctx.argv[1].c_str());
  255. rpn_process(_rpn_ctxt, ctx.argv[1].c_str(), true);
  256. _rpnDump();
  257. rpn_stack_clear(_rpn_ctxt);
  258. terminalOK();
  259. } else {
  260. terminalError(F("Wrong arguments"));
  261. }
  262. });
  263. }
  264. #endif
  265. void _rpnRun() {
  266. unsigned char i = 0;
  267. String rule = getSetting({"rpnRule", i});
  268. while (rule.length()) {
  269. //DEBUG_MSG_P(PSTR("[RPN] Running \"%s\"\n"), rule.c_str());
  270. rpn_process(_rpn_ctxt, rule.c_str(), true);
  271. //_rpnDump();
  272. rule = getSetting({"rpnRule", ++i});
  273. rpn_stack_clear(_rpn_ctxt);
  274. }
  275. if (!getSetting("rpnSticky", 1 == RPN_STICKY)) {
  276. rpn_variables_clear(_rpn_ctxt);
  277. }
  278. }
  279. void _rpnLoop() {
  280. if (_rpn_run && (millis() - _rpn_last > _rpn_delay)) {
  281. _rpnRun();
  282. _rpn_run = false;
  283. }
  284. }
  285. void rpnSetup() {
  286. // Init context
  287. _rpnInit();
  288. // Load & cache settings
  289. _rpnConfigure();
  290. // Terminal commands
  291. #if TERMINAL_SUPPORT
  292. _rpnInitCommands();
  293. #endif
  294. // Websockets
  295. #if WEB_SUPPORT
  296. wsRegister()
  297. .onVisible([](JsonObject& root) { root["rpnVisible"] = 1; })
  298. .onConnected(_rpnWebSocketOnConnected)
  299. .onKeyCheck(_rpnWebSocketOnKeyCheck);
  300. #endif
  301. // MQTT
  302. #if MQTT_SUPPORT
  303. mqttRegister(_rpnMQTTCallback);
  304. #endif
  305. #if NTP_SUPPORT
  306. NtpBroker::Register([](const NtpTick tick, time_t timestamp, const String& datetime) {
  307. static const String tick_every_hour(F("tick1h"));
  308. static const String tick_every_minute(F("tick1m"));
  309. const char* ptr =
  310. (tick == NtpTick::EveryMinute) ? tick_every_minute.c_str() :
  311. (tick == NtpTick::EveryHour) ? tick_every_hour.c_str() : nullptr;
  312. if (ptr != nullptr) {
  313. rpn_variable_set(_rpn_ctxt, ptr, timestamp);
  314. _rpn_last = millis();
  315. _rpn_run = true;
  316. }
  317. });
  318. #endif
  319. StatusBroker::Register(_rpnBrokerStatus);
  320. #if SENSOR_SUPPORT
  321. SensorReadBroker::Register(_rpnBrokerCallback);
  322. #endif
  323. espurnaRegisterReload(_rpnConfigure);
  324. espurnaRegisterLoop(_rpnLoop);
  325. }
  326. #endif // RPN_RULES_SUPPORT