xdrv_09_timers.ino 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. /*
  2. xdrv_09_timers.ino - timer support for Sonoff-Tasmota
  3. Copyright (C) 2018 Theo Arends
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. #ifdef USE_TIMERS
  16. /*********************************************************************************************\
  17. * Timers
  18. *
  19. * Arm a timer using one or all of the following JSON values:
  20. * {"Arm":1,"Mode":0,"Time":"09:23","Window":0,"Days":"--TW--S","Repeat":1,"Output":1,"Action":1}
  21. *
  22. * Arm 0 = Off, 1 = On
  23. * Mode 0 = Schedule, 1 = Sunrise, 2 = Sunset
  24. * Time hours:minutes
  25. * Window minutes (0..15)
  26. * Days 7 day character mask starting with Sunday (SMTWTFS). 0 or - = Off, any other value = On
  27. * Repeat 0 = Execute once, 1 = Execute again
  28. * Output 1..16
  29. * Action 0 = Off, 1 = On, 2 = Toggle, 3 = Blink or Rule if USE_RULES enabled
  30. *
  31. \*********************************************************************************************/
  32. #define XDRV_09 9
  33. enum TimerCommands { CMND_TIMER, CMND_TIMERS
  34. #ifdef USE_SUNRISE
  35. , CMND_LATITUDE, CMND_LONGITUDE
  36. #endif
  37. };
  38. const char kTimerCommands[] PROGMEM = D_CMND_TIMER "|" D_CMND_TIMERS
  39. #ifdef USE_SUNRISE
  40. "|" D_CMND_LATITUDE "|" D_CMND_LONGITUDE
  41. #endif
  42. ;
  43. uint16_t timer_last_minute = 60;
  44. int8_t timer_window[MAX_TIMERS] = { 0 };
  45. #ifdef USE_SUNRISE
  46. /*********************************************************************************************\
  47. * Sunrise and sunset (+13k code)
  48. *
  49. * https://forum.arduino.cc/index.php?topic=218280.0
  50. * Source: C-Programm von http://lexikon.astronomie.info/zeitgleichung/neu.html
  51. * Rewrite for Arduino by 'jurs' for German Arduino forum
  52. \*********************************************************************************************/
  53. const double pi2 = TWO_PI;
  54. const double pi = PI;
  55. const double RAD = DEG_TO_RAD;
  56. double JulianischesDatum(void)
  57. {
  58. // Gregorianischer Kalender
  59. int Gregor;
  60. int Jahr = RtcTime.year;
  61. int Monat = RtcTime.month;
  62. int Tag = RtcTime.day_of_month;
  63. if (Monat <= 2) {
  64. Monat += 12;
  65. Jahr -= 1;
  66. }
  67. Gregor = (Jahr / 400) - (Jahr / 100) + (Jahr / 4); // Gregorianischer Kalender
  68. return 2400000.5 + 365.0*Jahr - 679004.0 + Gregor + (int)(30.6001 * (Monat +1)) + Tag + 0.5;
  69. }
  70. double InPi(double x)
  71. {
  72. int n = (int)(x / pi2);
  73. x = x - n*pi2;
  74. if (x < 0) x += pi2;
  75. return x;
  76. }
  77. double eps(double T)
  78. {
  79. // Neigung der Erdachse
  80. return RAD * (23.43929111 + (-46.8150*T - 0.00059*T*T + 0.001813*T*T*T)/3600.0);
  81. }
  82. double BerechneZeitgleichung(double *DK,double T)
  83. {
  84. double RA_Mittel = 18.71506921 + 2400.0513369*T +(2.5862e-5 - 1.72e-9*T)*T*T;
  85. double M = InPi(pi2 * (0.993133 + 99.997361*T));
  86. double L = InPi(pi2 * (0.7859453 + M/pi2 + (6893.0*sin(M)+72.0*sin(2.0*M)+6191.2*T) / 1296.0e3));
  87. double e = eps(T);
  88. double RA = atan(tan(L)*cos(e));
  89. if (RA < 0.0) RA += pi;
  90. if (L > pi) RA += pi;
  91. RA = 24.0*RA/pi2;
  92. *DK = asin(sin(e)*sin(L));
  93. // Damit 0<=RA_Mittel<24
  94. RA_Mittel = 24.0 * InPi(pi2*RA_Mittel/24.0)/pi2;
  95. double dRA = RA_Mittel - RA;
  96. if (dRA < -12.0) dRA += 24.0;
  97. if (dRA > 12.0) dRA -= 24.0;
  98. dRA = dRA * 1.0027379;
  99. return dRA;
  100. }
  101. void DuskTillDawn(uint8_t *hour_up,uint8_t *minute_up, uint8_t *hour_down, uint8_t *minute_down)
  102. {
  103. double JD2000 = 2451545.0;
  104. double JD = JulianischesDatum();
  105. double T = (JD - JD2000) / 36525.0;
  106. double DK;
  107. /*
  108. h (D) = -0.8333 normaler SA & SU-Gang
  109. h (D) = -6.0 civile Dämmerung
  110. h (D) = -12.0 nautische Dämmerung
  111. h (D) = -18.0 astronomische Dämmerung
  112. */
  113. // double h = -50/60.0*RAD;
  114. double h = SUNRISE_DAWN_ANGLE *RAD;
  115. double B = (((double)Settings.latitude)/1000000) * RAD; // geographische Breite
  116. double GeographischeLaenge = ((double)Settings.longitude)/1000000;
  117. // double Zeitzone = 0; //Weltzeit
  118. // double Zeitzone = 1; //Winterzeit
  119. // double Zeitzone = 2.0; //Sommerzeit
  120. double Zeitzone = ((double)time_timezone) / 60;
  121. double Zeitgleichung = BerechneZeitgleichung(&DK, T);
  122. double Minuten = Zeitgleichung * 60.0;
  123. double Zeitdifferenz = 12.0*acos((sin(h) - sin(B)*sin(DK)) / (cos(B)*cos(DK)))/pi;
  124. double AufgangOrtszeit = 12.0 - Zeitdifferenz - Zeitgleichung;
  125. double UntergangOrtszeit = 12.0 + Zeitdifferenz - Zeitgleichung;
  126. double AufgangWeltzeit = AufgangOrtszeit - GeographischeLaenge / 15.0;
  127. double UntergangWeltzeit = UntergangOrtszeit - GeographischeLaenge / 15.0;
  128. double Aufgang = AufgangWeltzeit + Zeitzone; // In Stunden
  129. if (Aufgang < 0.0) {
  130. Aufgang += 24.0;
  131. } else {
  132. if (Aufgang >= 24.0) Aufgang -= 24.0;
  133. }
  134. double Untergang = UntergangWeltzeit + Zeitzone;
  135. if (Untergang < 0.0) {
  136. Untergang += 24.0;
  137. } else {
  138. if (Untergang >= 24.0) Untergang -= 24.0;
  139. }
  140. int AufgangMinuten = (int)(60.0*(Aufgang - (int)Aufgang)+0.5);
  141. int AufgangStunden = (int)Aufgang;
  142. if (AufgangMinuten >= 60.0) {
  143. AufgangMinuten -= 60.0;
  144. AufgangStunden++;
  145. } else {
  146. if (AufgangMinuten < 0.0) {
  147. AufgangMinuten += 60.0;
  148. AufgangStunden--;
  149. if (AufgangStunden < 0.0) AufgangStunden += 24.0;
  150. }
  151. }
  152. int UntergangMinuten = (int)(60.0*(Untergang - (int)Untergang)+0.5);
  153. int UntergangStunden = (int)Untergang;
  154. if (UntergangMinuten >= 60.0) {
  155. UntergangMinuten -= 60.0;
  156. UntergangStunden++;
  157. } else {
  158. if (UntergangMinuten<0) {
  159. UntergangMinuten += 60.0;
  160. UntergangStunden--;
  161. if (UntergangStunden < 0.0) UntergangStunden += 24.0;
  162. }
  163. }
  164. *hour_up = AufgangStunden;
  165. *minute_up = AufgangMinuten;
  166. *hour_down = UntergangStunden;
  167. *minute_down = UntergangMinuten;
  168. }
  169. void ApplyTimerOffsets(Timer *duskdawn)
  170. {
  171. uint8_t hour[2];
  172. uint8_t minute[2];
  173. Timer stored = (Timer)*duskdawn;
  174. // replace hours, minutes by sunrise
  175. DuskTillDawn(&hour[0], &minute[0], &hour[1], &minute[1]);
  176. uint8_t mode = (duskdawn->mode -1) &1;
  177. duskdawn->time = (hour[mode] *60) + minute[mode];
  178. // apply offsets, check for over- and underflows
  179. uint16_t timeBuffer;
  180. if ((uint16_t)stored.time > 719) {
  181. // negative offset, time after 12:00
  182. timeBuffer = (uint16_t)stored.time - 720;
  183. // check for underflow
  184. if (timeBuffer > (uint16_t)duskdawn->time) {
  185. timeBuffer = 1440 - (timeBuffer - (uint16_t)duskdawn->time);
  186. duskdawn->days = duskdawn->days >> 1;
  187. duskdawn->days |= (stored.days << 6);
  188. } else {
  189. timeBuffer = (uint16_t)duskdawn->time - timeBuffer;
  190. }
  191. } else {
  192. // positive offset
  193. timeBuffer = (uint16_t)duskdawn->time + (uint16_t)stored.time;
  194. // check for overflow
  195. if (timeBuffer > 1440) {
  196. timeBuffer -= 1440;
  197. duskdawn->days = duskdawn->days << 1;
  198. duskdawn->days |= (stored.days >> 6);
  199. }
  200. }
  201. duskdawn->time = timeBuffer;
  202. }
  203. String GetSun(byte dawn)
  204. {
  205. char stime[6];
  206. uint8_t hour[2];
  207. uint8_t minute[2];
  208. DuskTillDawn(&hour[0], &minute[0], &hour[1], &minute[1]);
  209. dawn &= 1;
  210. snprintf_P(stime, sizeof(stime), PSTR("%02d:%02d"), hour[dawn], minute[dawn]);
  211. return String(stime);
  212. }
  213. uint16_t GetSunMinutes(byte dawn)
  214. {
  215. uint8_t hour[2];
  216. uint8_t minute[2];
  217. DuskTillDawn(&hour[0], &minute[0], &hour[1], &minute[1]);
  218. dawn &= 1;
  219. return (hour[dawn] *60) + minute[dawn];
  220. }
  221. #endif // USE_SUNRISE
  222. /*******************************************************************************************/
  223. void TimerSetRandomWindow(byte index)
  224. {
  225. timer_window[index] = 0;
  226. if (Settings.timer[index].window) {
  227. timer_window[index] = (random(0, (Settings.timer[index].window << 1) +1)) - Settings.timer[index].window; // -15 .. 15
  228. }
  229. }
  230. void TimerSetRandomWindows(void)
  231. {
  232. for (byte i = 0; i < MAX_TIMERS; i++) { TimerSetRandomWindow(i); }
  233. }
  234. void TimerEverySecond(void)
  235. {
  236. if (RtcTime.valid) {
  237. if (!RtcTime.hour && !RtcTime.minute && !RtcTime.second) { TimerSetRandomWindows(); } // Midnight
  238. if (Settings.flag3.timers_enable && (uptime > 60) && (RtcTime.minute != timer_last_minute)) { // Execute from one minute after restart every minute only once
  239. timer_last_minute = RtcTime.minute;
  240. int16_t time = (RtcTime.hour *60) + RtcTime.minute;
  241. uint8_t days = 1 << (RtcTime.day_of_week -1);
  242. for (byte i = 0; i < MAX_TIMERS; i++) {
  243. // if (Settings.timer[i].device >= devices_present) Settings.timer[i].data = 0; // Reset timer due to change in devices present
  244. Timer xtimer = Settings.timer[i];
  245. uint16_t set_time = xtimer.time;
  246. #ifdef USE_SUNRISE
  247. if ((1 == xtimer.mode) || (2 == xtimer.mode)) { // Sunrise or Sunset
  248. ApplyTimerOffsets(&xtimer);
  249. set_time = xtimer.time;
  250. }
  251. #endif
  252. if (xtimer.arm) {
  253. set_time += timer_window[i]; // Add random time offset
  254. if (set_time < 0) { set_time = 0; } // Stay today;
  255. if (set_time > 1439) { set_time = 1439; }
  256. if (time == set_time) {
  257. if (xtimer.days & days) {
  258. Settings.timer[i].arm = xtimer.repeat;
  259. #ifdef USE_RULES
  260. if (3 == xtimer.power) { // Blink becomes Rule disregarding device and allowing use of Backlog commands
  261. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"Clock\":{\"Timer\":%d}}"), i +1);
  262. XdrvRulesProcess();
  263. } else
  264. #endif // USE_RULES
  265. if (devices_present) { ExecuteCommandPower(xtimer.device +1, xtimer.power, SRC_TIMER); }
  266. }
  267. }
  268. }
  269. }
  270. }
  271. }
  272. }
  273. void PrepShowTimer(uint8_t index)
  274. {
  275. char days[8] = { 0 };
  276. char sign[2] = { 0 };
  277. char soutput[80];
  278. Timer xtimer = Settings.timer[index -1];
  279. for (byte i = 0; i < 7; i++) {
  280. uint8_t mask = 1 << i;
  281. snprintf(days, sizeof(days), "%s%d", days, ((xtimer.days & mask) > 0));
  282. }
  283. soutput[0] = '\0';
  284. if (devices_present) {
  285. snprintf_P(soutput, sizeof(soutput), PSTR(",\"" D_JSON_TIMER_OUTPUT "\":%d"), xtimer.device +1);
  286. }
  287. #ifdef USE_SUNRISE
  288. int16_t hour = xtimer.time / 60;
  289. if ((1 == xtimer.mode) || (2 == xtimer.mode)) { // Sunrise or Sunset
  290. if (hour > 11) {
  291. hour -= 12;
  292. sign[0] = '-';
  293. }
  294. }
  295. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s\"" D_CMND_TIMER "%d\":{\"" D_JSON_TIMER_ARM "\":%d,\"" D_JSON_TIMER_MODE "\":%d,\"" D_JSON_TIMER_TIME "\":\"%s%02d:%02d\",\"" D_JSON_TIMER_WINDOW "\":%d,\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d%s,\"" D_JSON_TIMER_ACTION "\":%d}"),
  296. mqtt_data, index, xtimer.arm, xtimer.mode, sign, hour, xtimer.time % 60, xtimer.window, days, xtimer.repeat, soutput, xtimer.power);
  297. #else
  298. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s\"" D_CMND_TIMER "%d\":{\"" D_JSON_TIMER_ARM "\":%d,\"" D_JSON_TIMER_TIME "\":\"%02d:%02d\",\"" D_JSON_TIMER_WINDOW "\":%d,\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d%s,\"" D_JSON_TIMER_ACTION "\":%d}"),
  299. mqtt_data, index, xtimer.arm, xtimer.time / 60, xtimer.time % 60, xtimer.window, days, xtimer.repeat, soutput, xtimer.power);
  300. #endif // USE_SUNRISE
  301. }
  302. /*********************************************************************************************\
  303. * Commands
  304. \*********************************************************************************************/
  305. boolean TimerCommand(void)
  306. {
  307. char command[CMDSZ];
  308. char dataBufUc[XdrvMailbox.data_len];
  309. boolean serviced = true;
  310. uint8_t index = XdrvMailbox.index;
  311. UpperCase(dataBufUc, XdrvMailbox.data);
  312. int command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic, kTimerCommands);
  313. if (-1 == command_code) {
  314. serviced = false; // Unknown command
  315. }
  316. else if ((CMND_TIMER == command_code) && (index > 0) && (index <= MAX_TIMERS)) {
  317. uint8_t error = 0;
  318. if (XdrvMailbox.data_len) {
  319. if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= MAX_TIMERS)) {
  320. if (XdrvMailbox.payload == 0) {
  321. Settings.timer[index -1].data = 0; // Clear timer
  322. } else {
  323. Settings.timer[index -1].data = Settings.timer[XdrvMailbox.payload -1].data; // Copy timer
  324. }
  325. } else {
  326. #ifndef USE_RULES
  327. if (devices_present) {
  328. #endif
  329. StaticJsonBuffer<256> jsonBuffer;
  330. JsonObject& root = jsonBuffer.parseObject(dataBufUc);
  331. if (!root.success()) {
  332. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_TIMER "%d\":\"" D_JSON_INVALID_JSON "\"}"), index); // JSON decode failed
  333. error = 1;
  334. }
  335. else {
  336. char parm_uc[10];
  337. index--;
  338. if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_ARM))].success()) {
  339. Settings.timer[index].arm = (root[parm_uc] != 0);
  340. }
  341. #ifdef USE_SUNRISE
  342. if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_MODE))].success()) {
  343. Settings.timer[index].mode = (uint8_t)root[parm_uc] & 0x03;
  344. }
  345. #endif
  346. if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_TIME))].success()) {
  347. uint16_t itime = 0;
  348. int8_t value = 0;
  349. uint8_t sign = 0;
  350. char time_str[10];
  351. snprintf(time_str, sizeof(time_str), root[parm_uc]);
  352. const char *substr = strtok(time_str, ":");
  353. if (substr != NULL) {
  354. if (strchr(substr, '-')) {
  355. sign = 1;
  356. substr++;
  357. }
  358. value = atoi(substr);
  359. if (sign) { value += 12; } // Allow entering timer offset from -11:59 to -00:01 converted to 12:01 to 23:59
  360. if (value > 23) { value = 23; }
  361. itime = value * 60;
  362. substr = strtok(NULL, ":");
  363. if (substr != NULL) {
  364. value = atoi(substr);
  365. if (value < 0) { value = 0; }
  366. if (value > 59) { value = 59; }
  367. itime += value;
  368. }
  369. }
  370. Settings.timer[index].time = itime;
  371. }
  372. if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_WINDOW))].success()) {
  373. Settings.timer[index].window = (uint8_t)root[parm_uc] & 0x0F;
  374. TimerSetRandomWindow(index);
  375. }
  376. if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_DAYS))].success()) {
  377. // SMTWTFS = 1234567 = 0011001 = 00TW00S = --TW--S
  378. Settings.timer[index].days = 0;
  379. const char *tday = root[parm_uc];
  380. uint8_t i = 0;
  381. char ch = *tday++;
  382. while ((ch != '\0') && (i < 7)) {
  383. if (ch == '-') { ch = '0'; }
  384. uint8_t mask = 1 << i++;
  385. Settings.timer[index].days |= (ch == '0') ? 0 : mask;
  386. ch = *tday++;
  387. }
  388. }
  389. if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_REPEAT))].success()) {
  390. Settings.timer[index].repeat = (root[parm_uc] != 0);
  391. }
  392. if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_OUTPUT))].success()) {
  393. uint8_t device = ((uint8_t)root[parm_uc] -1) & 0x0F;
  394. Settings.timer[index].device = (device < devices_present) ? device : 0;
  395. }
  396. if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_ACTION))].success()) {
  397. uint8_t action = (uint8_t)root[parm_uc] & 0x03;
  398. Settings.timer[index].power = (devices_present) ? action : 3; // If no devices than only allow rules
  399. }
  400. index++;
  401. }
  402. #ifndef USE_RULES
  403. } else {
  404. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_TIMER "%d\":\"" D_JSON_TIMER_NO_DEVICE "\"}"), index); // No outputs defined so nothing to control
  405. error = 1;
  406. }
  407. #endif
  408. }
  409. }
  410. if (!error) {
  411. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{"));
  412. PrepShowTimer(index);
  413. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s}"), mqtt_data);
  414. }
  415. }
  416. else if (CMND_TIMERS == command_code) {
  417. if (XdrvMailbox.data_len) {
  418. if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) {
  419. Settings.flag3.timers_enable = XdrvMailbox.payload;
  420. }
  421. if (XdrvMailbox.payload == 2) {
  422. Settings.flag3.timers_enable = !Settings.flag3.timers_enable;
  423. }
  424. }
  425. snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, GetStateText(Settings.flag3.timers_enable));
  426. MqttPublishPrefixTopic_P(RESULT_OR_STAT, command);
  427. byte jsflg = 0;
  428. byte lines = 1;
  429. for (byte i = 0; i < MAX_TIMERS; i++) {
  430. if (!jsflg) {
  431. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_TIMERS "%d\":{"), lines++);
  432. } else {
  433. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,"), mqtt_data);
  434. }
  435. jsflg++;
  436. PrepShowTimer(i +1);
  437. if (jsflg > 3) {
  438. snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s}}"), mqtt_data);
  439. MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_TIMERS));
  440. jsflg = 0;
  441. }
  442. }
  443. mqtt_data[0] = '\0';
  444. }
  445. #ifdef USE_SUNRISE
  446. else if (CMND_LONGITUDE == command_code) {
  447. if (XdrvMailbox.data_len) {
  448. Settings.longitude = (int)(CharToDouble(XdrvMailbox.data) *1000000);
  449. }
  450. char lbuff[33];
  451. dtostrfd(((double)Settings.longitude) /1000000, 6, lbuff);
  452. snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, lbuff);
  453. }
  454. else if (CMND_LATITUDE == command_code) {
  455. if (XdrvMailbox.data_len) {
  456. Settings.latitude = (int)(CharToDouble(XdrvMailbox.data) *1000000);
  457. }
  458. char lbuff[33];
  459. dtostrfd(((double)Settings.latitude) /1000000, 6, lbuff);
  460. snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, lbuff);
  461. }
  462. #endif
  463. else serviced = false; // Unknown command
  464. return serviced;
  465. }
  466. /*********************************************************************************************\
  467. * Presentation
  468. \*********************************************************************************************/
  469. #ifdef USE_WEBSERVER
  470. #ifdef USE_TIMERS_WEB
  471. #define WEB_HANDLE_TIMER "tm"
  472. const char S_CONFIGURE_TIMER[] PROGMEM = D_CONFIGURE_TIMER;
  473. const char HTTP_BTN_MENU_TIMER[] PROGMEM =
  474. "<br/><form action='" WEB_HANDLE_TIMER "' method='get'><button>" D_CONFIGURE_TIMER "</button></form>";
  475. const char HTTP_TIMER_SCRIPT[] PROGMEM =
  476. "var pt=[],ct=99;"
  477. "function qs(s){" // Alias to save code space
  478. "return document.querySelector(s);"
  479. "}"
  480. "function ce(i,q){" // Create select option
  481. "var o=document.createElement('option');"
  482. "o.textContent=i;"
  483. "q.appendChild(o);"
  484. "}"
  485. #ifdef USE_SUNRISE
  486. "function gt(){" // Set hours and minutes according to mode
  487. "var m,p,q;"
  488. "m=qs('input[name=\"rd\"]:checked').value;" // Get mode
  489. "p=pt[ct]&0x7FF;" // Get time
  490. "if(m==0){" // Time is set
  491. "so(0);" // Hide offset span and allow Hour 00..23
  492. "q=Math.floor(p/60);if(q<10){q='0'+q;}qs('#ho').value=q;" // Set hours
  493. "q=p%60;if(q<10){q='0'+q;}qs('#mi').value=q;" // Set minutes
  494. "}"
  495. "if((m==1)||(m==2)){" // Sunrise or sunset is set
  496. "so(1);" // Show offset span and allow Hour 00..11
  497. "q=Math.floor(p/60);" // Parse hours
  498. "if(q>=12){q-=12;qs('#dr').selectedIndex=1;}" // Negative offset
  499. "else{qs('#dr').selectedIndex=0;}"
  500. "if(q<10){q='0'+q;}qs('#ho').value=q;" // Set offset hours
  501. "q=p%60;if(q<10){q='0'+q;}qs('#mi').value=q;" // Set offset minutes
  502. "}"
  503. "}"
  504. "function so(b){" // Hide or show offset items
  505. "o=qs('#ho');"
  506. "e=o.childElementCount;"
  507. "if(b==1){"
  508. "qs('#dr').disabled='';"
  509. "if(e>12){for(i=12;i<=23;i++){o.removeChild(o.lastElementChild);}}" // Create offset hours select options
  510. "}else{"
  511. "qs('#dr').disabled='disabled';"
  512. "if(e<23){for(i=12;i<=23;i++){ce(i,o);}}" // Create hours select options
  513. "}"
  514. "}"
  515. #endif
  516. "function st(){" // Save parameters to hidden area
  517. "var i,l,m,n,p,s;"
  518. "m=0;s=0;"
  519. "n=1<<31;if(eb('a0').checked){s|=n;}" // Get arm
  520. "n=1<<15;if(eb('r0').checked){s|=n;}" // Get repeat
  521. "for(i=0;i<7;i++){n=1<<(16+i);if(eb('w'+i).checked){s|=n;}}" // Get weekdays
  522. #ifdef USE_SUNRISE
  523. "m=qs('input[name=\"rd\"]:checked').value;" // Check mode
  524. "s|=(qs('input[name=\"rd\"]:checked').value<<29);" // Get mode
  525. #endif
  526. "if(}1>0){"
  527. "i=qs('#d1').selectedIndex;if(i>=0){s|=(i<<23);}" // Get output
  528. "s|=(qs('#p1').selectedIndex<<27);" // Get action
  529. "}else{"
  530. "s|=3<<27;" // Get action (rule)
  531. "}"
  532. "l=((qs('#ho').selectedIndex*60)+qs('#mi').selectedIndex)&0x7FF;"
  533. "if(m==0){s|=l;}" // Get time
  534. #ifdef USE_SUNRISE
  535. "if((m==1)||(m==2)){"
  536. "if(qs('#dr').selectedIndex>0){l+=720;}" // If negative offset, add 12h to given offset time
  537. "s|=l&0x7FF;" // Save offset instead of time
  538. "}"
  539. #endif
  540. "s|=((qs('#mw').selectedIndex)&0x0F)<<11;" // Get window minutes
  541. "pt[ct]=s;"
  542. "eb('t0').value=pt.join();" // Save parameters from array to hidden area
  543. "}"
  544. "function ot(t,e){" // Select tab and update elements
  545. "var i,n,o,p,q,s;"
  546. "if(ct<99){st();}" // Save changes
  547. "ct=t;"
  548. "o=document.getElementsByClassName('tl');" // Restore style to all tabs/buttons
  549. "for(i=0;i<o.length;i++){o[i].style.cssText=\"background-color:#ccc;color:#fff;font-weight:normal;\"}"
  550. "e.style.cssText=\"background-color:#fff;color:#000;font-weight:bold;\";" // Change style to tab/button used to open content
  551. "s=pt[ct];" // Get parameters from array
  552. #ifdef USE_SUNRISE
  553. "p=(s>>29)&3;eb('b'+p).checked=1;" // Set mode
  554. "gt();" // Set hours and minutes according to mode
  555. #else
  556. "p=s&0x7FF;" // Get time
  557. "q=Math.floor(p/60);if(q<10){q='0'+q;}qs('#ho').value=q;" // Set hours
  558. "q=p%60;if(q<10){q='0'+q;}qs('#mi').value=q;" // Set minutes
  559. #endif
  560. "q=(s>>11)&0xF;if(q<10){q='0'+q;}qs('#mw').value=q;" // Set window minutes
  561. "for(i=0;i<7;i++){p=(s>>(16+i))&1;eb('w'+i).checked=p;}" // Set weekdays
  562. "if(}1>0){"
  563. "p=(s>>23)&0xF;qs('#d1').value=p+1;" // Set output
  564. "p=(s>>27)&3;qs('#p1').selectedIndex=p;" // Set action
  565. "}"
  566. "p=(s>>15)&1;eb('r0').checked=p;" // Set repeat
  567. "p=(s>>31)&1;eb('a0').checked=p;" // Set arm
  568. "}"
  569. "function it(){" // Initialize elements and select first tab
  570. "var b,i,o,s;"
  571. "pt=eb('t0').value.split(',').map(Number);" // Get parameters from hidden area to array
  572. "s='';for(i=0;i<" STR(MAX_TIMERS) ";i++){b='';if(0==i){b=\" id='dP'\";}s+=\"<button type='button' class='tl' onclick='ot(\"+i+\",this)'\"+b+\">\"+(i+1)+\"</button>\"}"
  573. "eb('bt').innerHTML=s;" // Create tabs
  574. "if(}1>0){" // Create Output and Action drop down boxes
  575. "eb('oa').innerHTML=\"<b>" D_TIMER_OUTPUT "</b>&nbsp;<span><select style='width:60px;' id='d1' name='d1'></select></span>&emsp;<b>" D_TIMER_ACTION "</b>&nbsp;<select style='width:99px;' id='p1' name='p1'></select>\";"
  576. "o=qs('#p1');ce('" D_OFF "',o);ce('" D_ON "',o);ce('" D_TOGGLE "',o);" // Create offset direction select options
  577. #ifdef USE_RULES
  578. "ce('" D_RULE "',o);"
  579. #else
  580. "ce('" D_BLINK "',o);"
  581. #endif
  582. "}else{"
  583. "eb('oa').innerHTML=\"<b>" D_TIMER_ACTION "</b> " D_RULE "\";" // No outputs but rule is allowed
  584. "}"
  585. #ifdef USE_SUNRISE
  586. "o=qs('#dr');ce('+',o);ce('-',o);" // Create offset direction select options
  587. #endif
  588. "o=qs('#ho');for(i=0;i<=23;i++){ce((i<10)?('0'+i):i,o);}" // Create hours select options
  589. "o=qs('#mi');for(i=0;i<=59;i++){ce((i<10)?('0'+i):i,o);}" // Create minutes select options
  590. "o=qs('#mw');for(i=0;i<=15;i++){ce((i<10)?('0'+i):i,o);}" // Create window minutes select options
  591. "o=qs('#d1');for(i=0;i<}1;i++){ce(i+1,o);}" // Create outputs
  592. "var a='" D_DAY3LIST "';"
  593. "s='';for(i=0;i<7;i++){s+=\"<input style='width:5%;' id='w\"+i+\"' name='w\"+i+\"' type='checkbox'><b>\"+a.substring(i*3,(i*3)+3)+\"</b>\"}"
  594. "eb('ds').innerHTML=s;" // Create weekdays
  595. "eb('dP').click();" // Get the element with id='dP' and click on it
  596. "}";
  597. const char HTTP_TIMER_STYLE[] PROGMEM =
  598. ".tl{float:left;border-radius:0;border:1px solid #fff;padding:1px;width:6.25%;}"
  599. #ifdef USE_SUNRISE
  600. "input[type='radio']{width:13px;height:24px;margin-top:-1px;margin-right:8px;vertical-align:middle;}"
  601. #endif
  602. "</style>";
  603. const char HTTP_FORM_TIMER[] PROGMEM =
  604. "<fieldset style='min-width:470px;text-align:center;'>"
  605. "<legend style='text-align:left;'><b>&nbsp;" D_TIMER_PARAMETERS "&nbsp;</b></legend>"
  606. "<form method='post' action='" WEB_HANDLE_TIMER "' onsubmit='return st();'>"
  607. "<br/><input style='width:5%;' id='e0' name='e0' type='checkbox'{e0><b>" D_TIMER_ENABLE "</b><br/><br/><hr/>"
  608. "<input id='t0' name='t0' value='";
  609. const char HTTP_FORM_TIMER1[] PROGMEM =
  610. "' hidden><div id='bt' name='bt'></div><br/><br/><br/>"
  611. "<div id='oa' name='oa'></div><br/>"
  612. "<div>"
  613. "<input style='width:5%;' id='a0' name='a0' type='checkbox'><b>" D_TIMER_ARM "</b>&emsp;"
  614. "<input style='width:5%;' id='r0' name='r0' type='checkbox'><b>" D_TIMER_REPEAT "</b>"
  615. "</div><br/>"
  616. "<div>"
  617. #ifdef USE_SUNRISE
  618. "<fieldset style='width:299px;margin:auto;text-align:left;border:0;'>"
  619. "<input id='b0' name='rd' type='radio' value='0' onclick='gt();'><b>" D_TIMER_TIME "</b><br/>"
  620. "<input id='b1' name='rd' type='radio' value='1' onclick='gt();'><b>" D_SUNRISE "</b> (}8)<br/>"
  621. "<input id='b2' name='rd' type='radio' value='2' onclick='gt();'><b>" D_SUNSET "</b> (}9)<br/>"
  622. "</fieldset>"
  623. "<span><select style='width:46px;' id='dr' name='dr'></select></span>"
  624. "&nbsp;"
  625. #else
  626. "<b>" D_TIMER_TIME "</b>&nbsp;"
  627. #endif // USE_SUNRISE
  628. "<span><select style='width:60px;' id='ho' name='ho'></select></span>"
  629. "&nbsp;" D_HOUR_MINUTE_SEPARATOR "&nbsp;"
  630. "<span><select style='width:60px;' id='mi' name='mi'></select></span>"
  631. "&emsp;<b>+/-</b>&nbsp;"
  632. "<span><select style='width:60px;' id='mw' name='mw'></select></span>"
  633. "</div><br/>"
  634. "<div id='ds' name='ds'></div>";
  635. void HandleTimerConfiguration(void)
  636. {
  637. if (HttpUser()) { return; }
  638. if (!WebAuthenticate()) { return WebServer->requestAuthentication(); }
  639. AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, S_CONFIGURE_TIMER);
  640. if (WebServer->hasArg("save")) {
  641. TimerSaveSettings();
  642. HandleConfiguration();
  643. return;
  644. }
  645. String page = FPSTR(HTTP_HEAD);
  646. page.replace(F("{v}"), FPSTR(S_CONFIGURE_TIMER));
  647. page += FPSTR(HTTP_TIMER_SCRIPT);
  648. page += FPSTR(HTTP_HEAD_STYLE);
  649. page.replace(F("</style>"), FPSTR(HTTP_TIMER_STYLE));
  650. page += FPSTR(HTTP_FORM_TIMER);
  651. page.replace(F("{e0"), (Settings.flag3.timers_enable) ? F(" checked") : F(""));
  652. for (byte i = 0; i < MAX_TIMERS; i++) {
  653. if (i > 0) { page += F(","); }
  654. page += String(Settings.timer[i].data);
  655. }
  656. page += FPSTR(HTTP_FORM_TIMER1);
  657. page.replace(F("}1"), String(devices_present));
  658. #ifdef USE_SUNRISE
  659. page.replace(F("}8"), GetSun(0)); // Add Sunrise
  660. page.replace(F("}9"), GetSun(1)); // Add Sunset
  661. page.replace(F("299"), String(100 + (strlen(D_SUNSET) *12))); // Fix string length to keep radios centered
  662. #endif // USE_SUNRISE
  663. page += FPSTR(HTTP_FORM_END);
  664. page += F("<script>it();</script>"); // Init elements and select first tab/button
  665. page += FPSTR(HTTP_BTN_CONF);
  666. ShowPage(page);
  667. }
  668. void TimerSaveSettings(void)
  669. {
  670. char tmp[MAX_TIMERS *12]; // Need space for MAX_TIMERS x 10 digit numbers separated by a comma
  671. Timer timer;
  672. Settings.flag3.timers_enable = WebServer->hasArg("e0");
  673. WebGetArg("t0", tmp, sizeof(tmp));
  674. char *p = tmp;
  675. snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_MQTT D_CMND_TIMERS " %d"), Settings.flag3.timers_enable);
  676. for (byte i = 0; i < MAX_TIMERS; i++) {
  677. timer.data = strtol(p, &p, 10);
  678. p++; // Skip comma
  679. if (timer.time < 1440) {
  680. bool flag = (timer.window != Settings.timer[i].window);
  681. Settings.timer[i].data = timer.data;
  682. if (flag) TimerSetRandomWindow(i);
  683. }
  684. snprintf_P(log_data, sizeof(log_data), PSTR("%s,0x%08X"), log_data, Settings.timer[i].data);
  685. }
  686. AddLog(LOG_LEVEL_DEBUG);
  687. }
  688. #endif // USE_TIMERS_WEB
  689. #endif // USE_WEBSERVER
  690. /*********************************************************************************************\
  691. * Interface
  692. \*********************************************************************************************/
  693. boolean Xdrv09(byte function)
  694. {
  695. boolean result = false;
  696. switch (function) {
  697. case FUNC_PRE_INIT:
  698. TimerSetRandomWindows();
  699. break;
  700. #ifdef USE_WEBSERVER
  701. #ifdef USE_TIMERS_WEB
  702. case FUNC_WEB_ADD_BUTTON:
  703. #ifdef USE_RULES
  704. strncat_P(mqtt_data, HTTP_BTN_MENU_TIMER, sizeof(mqtt_data) - strlen(mqtt_data) -1);
  705. #else
  706. if (devices_present) { strncat_P(mqtt_data, HTTP_BTN_MENU_TIMER, sizeof(mqtt_data) - strlen(mqtt_data) -1); }
  707. #endif // USE_RULES
  708. break;
  709. case FUNC_WEB_ADD_HANDLER:
  710. WebServer->on("/" WEB_HANDLE_TIMER, HandleTimerConfiguration);
  711. break;
  712. #endif // USE_TIMERS_WEB
  713. #endif // USE_WEBSERVER
  714. case FUNC_EVERY_SECOND:
  715. TimerEverySecond();
  716. break;
  717. case FUNC_COMMAND:
  718. result = TimerCommand();
  719. break;
  720. }
  721. return result;
  722. }
  723. #endif // USE_TIMERS