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.

421 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. #if RELAY_SUPPORT
  111. bool _rpnRelayStatus(rpn_context & ctxt, bool force) {
  112. float status, id;
  113. rpn_stack_pop(ctxt, id);
  114. rpn_stack_pop(ctxt, status);
  115. if (int(status) == 2) {
  116. relayToggle(int(id));
  117. } else if (force || (relayStatusTarget(int(id)) != (int(status) == 1))) {
  118. relayStatus(int(id), int(status) == 1);
  119. }
  120. return true;
  121. }
  122. #endif
  123. void _rpnDump() {
  124. float value;
  125. DEBUG_MSG_P(PSTR("[RPN] Stack:\n"));
  126. unsigned char num = rpn_stack_size(_rpn_ctxt);
  127. if (0 == num) {
  128. DEBUG_MSG_P(PSTR(" (empty)\n"));
  129. } else {
  130. unsigned char index = num - 1;
  131. while (rpn_stack_get(_rpn_ctxt, index, value)) {
  132. DEBUG_MSG_P(PSTR(" %02d: %s\n"), index--, String(value).c_str());
  133. }
  134. }
  135. }
  136. void _rpnInit() {
  137. // Init context
  138. rpn_init(_rpn_ctxt);
  139. // Time functions need NTP support
  140. // TODO: since 1.15.0, timelib+ntpclientlib are no longer used with latest Cores
  141. // `now` is always in UTC, `utc_...` functions to be used instead to convert time
  142. #if NTP_SUPPORT && !NTP_LEGACY_SUPPORT
  143. rpn_operator_set(_rpn_ctxt, "utc", 0, _rpnNtpNow);
  144. rpn_operator_set(_rpn_ctxt, "now", 0, _rpnNtpNow);
  145. rpn_operator_set(_rpn_ctxt, "utc_month", 1, [](rpn_context & ctxt) {
  146. return _rpnNtpFunc(ctxt, utc_month);
  147. });
  148. rpn_operator_set(_rpn_ctxt, "month", 1, [](rpn_context & ctxt) {
  149. return _rpnNtpFunc(ctxt, month);
  150. });
  151. rpn_operator_set(_rpn_ctxt, "utc_day", 1, [](rpn_context & ctxt) {
  152. return _rpnNtpFunc(ctxt, utc_day);
  153. });
  154. rpn_operator_set(_rpn_ctxt, "day", 1, [](rpn_context & ctxt) {
  155. return _rpnNtpFunc(ctxt, day);
  156. });
  157. rpn_operator_set(_rpn_ctxt, "utc_dow", 1, [](rpn_context & ctxt) {
  158. return _rpnNtpFunc(ctxt, utc_weekday);
  159. });
  160. rpn_operator_set(_rpn_ctxt, "dow", 1, [](rpn_context & ctxt) {
  161. return _rpnNtpFunc(ctxt, weekday);
  162. });
  163. rpn_operator_set(_rpn_ctxt, "utc_hour", 1, [](rpn_context & ctxt) {
  164. return _rpnNtpFunc(ctxt, utc_hour);
  165. });
  166. rpn_operator_set(_rpn_ctxt, "hour", 1, [](rpn_context & ctxt) {
  167. return _rpnNtpFunc(ctxt, hour);
  168. });
  169. rpn_operator_set(_rpn_ctxt, "utc_minute", 1, [](rpn_context & ctxt) {
  170. return _rpnNtpFunc(ctxt, utc_minute);
  171. });
  172. rpn_operator_set(_rpn_ctxt, "minute", 1, [](rpn_context & ctxt) {
  173. return _rpnNtpFunc(ctxt, minute);
  174. });
  175. #endif
  176. // TODO: 1.14.0 weekday(...) conversion seemed to have 0..6 range with Monday as 0
  177. // using classic Sunday as first, but instead of 0 it is 1
  178. // Implementation above also uses 1 for Sunday, staying compatible with TimeLib
  179. #if NTP_SUPPORT && NTP_LEGACY_SUPPORT
  180. rpn_operator_set(_rpn_ctxt, "utc", 0, [](rpn_context & ctxt) {
  181. if (!ntpSynced()) return false;
  182. rpn_stack_push(ctxt, ntpLocal2UTC(now()));
  183. return true;
  184. });
  185. rpn_operator_set(_rpn_ctxt, "now", 0, _rpnNtpNow);
  186. rpn_operator_set(_rpn_ctxt, "month", 1, [](rpn_context & ctxt) {
  187. return _rpnNtpFunc(ctxt, month);
  188. });
  189. rpn_operator_set(_rpn_ctxt, "day", 1, [](rpn_context & ctxt) {
  190. return _rpnNtpFunc(ctxt, day);
  191. });
  192. rpn_operator_set(_rpn_ctxt, "dow", 1, [](rpn_context & ctxt) {
  193. return _rpnNtpFunc(ctxt, weekday);
  194. });
  195. rpn_operator_set(_rpn_ctxt, "hour", 1, [](rpn_context & ctxt) {
  196. return _rpnNtpFunc(ctxt, hour);
  197. });
  198. rpn_operator_set(_rpn_ctxt, "minute", 1, [](rpn_context & ctxt) {
  199. return _rpnNtpFunc(ctxt, minute);
  200. });
  201. #endif
  202. // Dumps RPN stack contents
  203. rpn_operator_set(_rpn_ctxt, "debug", 0, [](rpn_context & ctxt) {
  204. _rpnDump();
  205. return true;
  206. });
  207. // Accept relay number and numeric API status value (0, 1 and 2)
  208. #if RELAY_SUPPORT
  209. // apply status and reset timers when called
  210. rpn_operator_set(_rpn_ctxt, "relay_reset", 2, [](rpn_context & ctxt) {
  211. return _rpnRelayStatus(ctxt, true);
  212. });
  213. // only update status when target status differs, keep running timers
  214. rpn_operator_set(_rpn_ctxt, "relay", 2, [](rpn_context & ctxt) {
  215. return _rpnRelayStatus(ctxt, false);
  216. });
  217. #endif // RELAY_SUPPORT == 1
  218. // Channel operators
  219. #if RELAY_PROVIDER == RELAY_PROVIDER_LIGHT
  220. rpn_operator_set(_rpn_ctxt, "update", 0, [](rpn_context & ctxt) {
  221. lightUpdate(true, true);
  222. return true;
  223. });
  224. rpn_operator_set(_rpn_ctxt, "black", 0, [](rpn_context & ctxt) {
  225. lightColor((unsigned long) 0);
  226. return true;
  227. });
  228. rpn_operator_set(_rpn_ctxt, "channel", 2, [](rpn_context & ctxt) {
  229. float value, id;
  230. rpn_stack_pop(ctxt, id);
  231. rpn_stack_pop(ctxt, value);
  232. lightChannel(int(id), int(value));
  233. return true;
  234. });
  235. #endif
  236. }
  237. #if TERMINAL_SUPPORT
  238. void _rpnInitCommands() {
  239. terminalRegisterCommand(F("RPN.VARS"), [](const terminal::CommandContext&) {
  240. unsigned char num = rpn_variables_size(_rpn_ctxt);
  241. if (0 == num) {
  242. DEBUG_MSG_P(PSTR("[RPN] No variables\n"));
  243. } else {
  244. DEBUG_MSG_P(PSTR("[RPN] Variables:\n"));
  245. for (unsigned char i=0; i<num; i++) {
  246. char * name = rpn_variable_name(_rpn_ctxt, i);
  247. float value;
  248. rpn_variable_get(_rpn_ctxt, name, value);
  249. DEBUG_MSG_P(PSTR(" %s: %s\n"), name, String(value).c_str());
  250. }
  251. }
  252. terminalOK();
  253. });
  254. terminalRegisterCommand(F("RPN.OPS"), [](const terminal::CommandContext&) {
  255. unsigned char num = _rpn_ctxt.operators.size();
  256. DEBUG_MSG_P(PSTR("[RPN] Operators:\n"));
  257. for (unsigned char i=0; i<num; i++) {
  258. DEBUG_MSG_P(PSTR(" %s (%d)\n"), _rpn_ctxt.operators[i].name, _rpn_ctxt.operators[i].argc);
  259. }
  260. terminalOK();
  261. });
  262. terminalRegisterCommand(F("RPN.TEST"), [](const terminal::CommandContext& ctx) {
  263. if (ctx.argc == 2) {
  264. DEBUG_MSG_P(PSTR("[RPN] Running \"%s\"\n"), ctx.argv[1].c_str());
  265. rpn_process(_rpn_ctxt, ctx.argv[1].c_str(), true);
  266. _rpnDump();
  267. rpn_stack_clear(_rpn_ctxt);
  268. terminalOK();
  269. } else {
  270. terminalError(F("Wrong arguments"));
  271. }
  272. });
  273. }
  274. #endif
  275. void _rpnRun() {
  276. unsigned char i = 0;
  277. String rule = getSetting({"rpnRule", i});
  278. while (rule.length()) {
  279. //DEBUG_MSG_P(PSTR("[RPN] Running \"%s\"\n"), rule.c_str());
  280. rpn_process(_rpn_ctxt, rule.c_str(), true);
  281. //_rpnDump();
  282. rule = getSetting({"rpnRule", ++i});
  283. rpn_stack_clear(_rpn_ctxt);
  284. }
  285. if (!getSetting("rpnSticky", 1 == RPN_STICKY)) {
  286. rpn_variables_clear(_rpn_ctxt);
  287. }
  288. }
  289. void _rpnLoop() {
  290. if (_rpn_run && (millis() - _rpn_last > _rpn_delay)) {
  291. _rpnRun();
  292. _rpn_run = false;
  293. }
  294. }
  295. void rpnSetup() {
  296. // Init context
  297. _rpnInit();
  298. // Load & cache settings
  299. _rpnConfigure();
  300. // Terminal commands
  301. #if TERMINAL_SUPPORT
  302. _rpnInitCommands();
  303. #endif
  304. // Websockets
  305. #if WEB_SUPPORT
  306. wsRegister()
  307. .onVisible([](JsonObject& root) { root["rpnVisible"] = 1; })
  308. .onConnected(_rpnWebSocketOnConnected)
  309. .onKeyCheck(_rpnWebSocketOnKeyCheck);
  310. #endif
  311. // MQTT
  312. #if MQTT_SUPPORT
  313. mqttRegister(_rpnMQTTCallback);
  314. #endif
  315. #if NTP_SUPPORT
  316. NtpBroker::Register([](const NtpTick tick, time_t timestamp, const String& datetime) {
  317. static const String tick_every_hour(F("tick1h"));
  318. static const String tick_every_minute(F("tick1m"));
  319. const char* ptr =
  320. (tick == NtpTick::EveryMinute) ? tick_every_minute.c_str() :
  321. (tick == NtpTick::EveryHour) ? tick_every_hour.c_str() : nullptr;
  322. if (ptr != nullptr) {
  323. rpn_variable_set(_rpn_ctxt, ptr, timestamp);
  324. _rpn_last = millis();
  325. _rpn_run = true;
  326. }
  327. });
  328. #endif
  329. StatusBroker::Register(_rpnBrokerStatus);
  330. #if SENSOR_SUPPORT
  331. SensorReadBroker::Register(_rpnBrokerCallback);
  332. #endif
  333. espurnaRegisterReload(_rpnConfigure);
  334. espurnaRegisterLoop(_rpnLoop);
  335. }
  336. #endif // RPN_RULES_SUPPORT