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.

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