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.

401 lines
12 KiB

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