Мелкие доработки стилей веб-интерфейса
[esp-clock.git] / web.cpp
1 #include "Clock.h"
2 #include <LittleFS.h>
3 #include <ESPAsyncWebServer.h>
4 #include <StreamString.h>
5 #include <Ticker.h>
6 #include "ArduinoJson.h"
7 #include "AsyncJson.h"
8
9 bool isApEnabled = false;
10 bool isWebStarted = false;
11
12 bool pendingWiFi = false;
13 bool pendingAuth = false;
14
15 AsyncWebServer server(80);
16 AsyncEventSource events("/events");
17
18 Ticker tKeepalive;
19
20 char auth_user[32];
21 char auth_pwd[32];
22
23 void reportChange(const __FlashStringHelper* name) {
24   char buf[256];
25   ConfigParameter* param = cfg.getParam(name);
26   if (param) {
27     switch (param->getType()) {
28       case 'B':
29         sprintf(buf,"{\"%s\":%s}", name, param->getBoolValue()?"true":"false");
30         break;
31       case 'I':
32         sprintf(buf,"{\"%s\":%d}", name, param->getIntValue());
33         break;
34       case 'F':
35         sprintf(buf,"{\"%s\":%f}", name, param->getFloatValue());
36         break;
37       case 'S':
38         sprintf(buf,"{\"%s\":\"%s\"}", name, param->getCharValue());
39         break;
40     }
41     events.send(buf,"update",millis());
42   }
43 }
44
45 void reportMessage(const __FlashStringHelper* msg) {
46   char buf[256];
47   strcpy_P(buf, (PGM_P)msg);
48   events.send(buf,"message",millis());
49 }
50
51 void sendInitial(AsyncEventSourceClient *client) {
52   String mac = WiFi.macAddress();
53   char buf[256];
54   sprintf(buf,"{\"_mac\":\"%s\",\"_weather\":\"%s\"}", mac.c_str(), weatherData);
55   client->send(buf,"update",millis());
56   mac = String();
57 }
58
59 void sendWeather() {
60   char buf[256];
61   sprintf(buf,"{\"_weather\":\"%s\"}",weatherData);
62   events.send(buf,"update",millis());
63 }
64
65 void sendKeepalive() {
66   static unsigned long lastMillis = 0;
67   static unsigned long uptimeCorrection = 0;
68   unsigned long currentMillis = millis();
69   unsigned long uptime = millis()/1000;
70   if (currentMillis < lastMillis) {
71     uptimeCorrection += lastMillis/1000;
72   }
73   lastMillis = millis();
74   uptime = uptime + uptimeCorrection;
75   int days = uptime / 86400;
76   uptime = uptime % 86400;
77   int hrs = uptime / 3600;
78   uptime = uptime % 3600;
79   int mins = uptime / 60;
80   uptime = uptime % 60;
81   int heap = ESP.getFreeHeap();
82   int rssi = WiFi.RSSI();
83   struct tm* timeinfo = localtime(&last_sync);
84   bool changed = cfg.getTimestamp() != 0;
85   char sync[16] = "--:--:--";
86   if (last_sync) {
87     sprintf(sync, "%02d:%02d:%02d", timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
88   }
89   char buf[256];
90   if (days) {
91     sprintf(buf,"{\"_uptime\":\"%d д %d ч %d % м %d с\", \"_date\":\"%02d.%2d.%04d\", \"_time\":\"%02d:%02d\",\"_heap\":\"%d б\", \"_rssi\":\"%d\",  \"_last_sync\":\"%s\", \"_changed\":%s}", days, hrs, mins, uptime, dd, mm, yy, hh, mi, heap, rssi, sync, changed?"true":"false");
92   } else if (hrs) {
93     sprintf(buf,"{\"_uptime\":\"%d ч %d % м %d с\", \"_date\":\"%02d.%2d.%04d\", \"_time\":\"%02d:%02d\",\"_heap\":\"%d б\", \"_rssi\":\"%d\",  \"_last_sync\":\"%s\", \"_changed\":%s}", hrs, mins, uptime, dd, mm, yy, hh, mi, heap, rssi, sync, changed?"true":"false");
94   } else {
95     sprintf(buf,"{\"_uptime\":\"%d м %d с\", \"_date\":\"%02d.%2d.%04d\", \"_time\":\"%02d:%02d\",\"_heap\":\"%d б\", \"_rssi\":\"%d\",  \"_last_sync\":\"%s\", \"_changed\":%s}", mins, uptime, dd, mm, yy, hh, mi, heap, rssi, sync, changed?"true":"false");
96   }
97   events.send(buf,"update",millis());
98 }
99
100 void apply(const char* name) {
101   if (strcmp(name,"sta_ssid") == 0 || strcmp(name,"sta_psk") == 0) {
102     if (!isApEnabled) {
103       pendingWiFi = true;
104     }
105   } else if (strcmp(name,"ap_ssid") == 0 || strcmp(name,"ap_psk") == 0) {
106     if (isApEnabled) {
107       pendingWiFi = true;
108     }
109   } else if (strcmp(name,"auth_user") == 0 || strcmp(name,"auth_pwd") == 0) {
110     pendingAuth = true;
111   } else if (strcmp(name,"ntp_server") == 0 || strcmp(name,"tz") == 0) {
112     setupTime();
113   } else if (strcmp(name,"pin_sda") == 0 || strcmp(name,"pin_scl") == 0 ||
114     strcmp(name,"i2c_speed") == 0 || strcmp(name,"enable_rtc") == 0 ||
115     strcmp(name,"enable_button") == 0 || strcmp(name,"button_pin") == 0 || strcmp(name,"button_inversed") == 0 ||
116     strcmp(name,"enable_buzzer") == 0 || strcmp(name,"buzzer_pin") == 0) {
117     setupHardware();
118   } else if (strcmp(name,"pin_din") == 0 || strcmp(name,"pin_clk") == 0 ||
119     strcmp_P(name,"pin_cs") == 0 || strcmp(name,"led_modules") == 0){
120     setupPanel();
121   } else if (strcmp(name,"panel_brightness_day") == 0 || strcmp_P(name,"panel_brightness_night") == 0 ||
122     strcmp_P(name,"day_from") == 0 || strcmp(name,"night_from") == 0){
123     setPanelBrightness();
124   }
125 }
126
127 char* actionScheduled = nullptr;
128 unsigned long millisScheduled = 0;
129
130 void setupWeb() {
131   char buf[256];
132
133   if (isWebStarted) {
134     tKeepalive.detach();
135     server.end();
136   }
137
138   isWebStarted = true;
139
140   strncpy(auth_user,cfg.getCharValue(F("auth_user")),31);
141   strncpy(auth_pwd,cfg.getCharValue(F("auth_pwd")),31);
142
143   server.on("/action", HTTP_GET, [](AsyncWebServerRequest* request) {
144     if (auth_user && auth_pwd && auth_user[0] && auth_pwd[0] && !request->authenticate(auth_user, auth_pwd)) {
145       return request-> requestAuthentication();
146     }
147     if(request->hasParam("name")) {
148       const char* action = request->getParam("name")->value().c_str();
149       if (strcmp(action,"reset") == 0) {
150           millisScheduled = millis();
151           actionScheduled = "reset";
152           request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Сбрасываю настройки и перезагружаюсь\"}");
153       } else if (strcmp(action,"restart") == 0) {
154         if (pendingWiFi) {
155           request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки WiFi\", \"page\":\"wifi\"}");
156         } else if (pendingAuth) {
157           request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки авторизации\", \"page\":\"system\"}");
158         } else {
159           request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Перезагружаюсь\"}");
160           millisScheduled = millis();
161           actionScheduled = "restart";
162         }
163       } else if (strcmp(action,"wifi") == 0) {
164         if (pendingAuth) {
165           request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки авторизации\", \"page\":\"system\"}");
166         } else {
167           request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Применяю настройки\"}");
168           millisScheduled = millis();
169           actionScheduled = "wifi";
170         }
171       } else if (strcmp(action,"auth") == 0) {
172         request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Применяю настройки\"}");
173         millisScheduled = millis();
174         actionScheduled = "auth";
175       } else if (strcmp(action,"save") == 0) {
176         if (pendingWiFi) {
177           request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки WiFi\", \"page\":\"wifi\"}");
178         } else if (pendingAuth) {
179           request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки авторизации\", \"page\":\"system\"}");
180         } else {
181           request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Сохраняю настройки\"}");
182           millisScheduled = millis();
183           actionScheduled = "save";
184         }
185       } else if (strcmp(action,"time") == 0) {
186         if(request->hasParam("timestamp")) {
187           unsigned long timestamp = atoi(request->getParam("timestamp")->value().c_str());
188           if (timestamp) {
189             timeval tv = { timestamp, 0 };
190             settimeofday(&tv, nullptr);
191             if (isRTCEnabled) {
192               Serial.println(F("Время установлено вручную"));
193               RTC.adjust(DateTime(timestamp));
194             }
195           }
196           request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Устанавливаю время\"}");
197         } else {
198           request->send(500, "text/plain", "{\"result\":\"FAILED\",\"message\":\"Not all parameters set\"}");
199         }
200       } else {
201         request->send(500, "text/plain", "{\"result\":\"FAILED\",\"message\":\"Unsupported action\"}");
202       }
203     } else {
204       request->send(500, "text/plain", "{\"result\":\"FAILED\",\"message\":\"Not all parameters set\"}");
205     }
206   });
207
208   server.on("/wifi/scan", HTTP_GET, [](AsyncWebServerRequest* request) {
209     if (auth_user && auth_pwd && auth_user[0] && auth_pwd[0] && !request->authenticate(auth_user, auth_pwd)) {
210       return request-> requestAuthentication();
211     }
212     String json = "[";
213     int n = WiFi.scanComplete();
214     if (n == -2) {
215       WiFi.scanNetworks(true);
216     } else if (n) {
217       for (int i = 0; i < n; ++i) {
218         if (i) json += ",";
219         json += "{";
220         json += "\"rssi\":" + String(WiFi.RSSI(i));
221         json += ",\"ssid\":\"" + WiFi.SSID(i) + "\"";
222         json += ",\"bssid\":\"" + WiFi.BSSIDstr(i) + "\"";
223         json += ",\"channel\":" + String(WiFi.channel(i));
224         json += ",\"secure\":" + String(WiFi.encryptionType(i));
225         json += ",\"hidden\":" + String(WiFi.isHidden(i) ? "true" : "false");
226         json += "}";
227       }
228       WiFi.scanDelete();
229       if (WiFi.scanComplete() == -2) {
230         WiFi.scanNetworks(true);
231       }
232     }
233     json += "]";
234     request->send(200, "application/json", json);
235     json = String();
236   });
237
238   server.on("/config/get", HTTP_GET, [](AsyncWebServerRequest* request) {
239     if (auth_user && auth_pwd && auth_user[0] && auth_pwd[0] && !request->authenticate(auth_user, auth_pwd)) {
240       return request-> requestAuthentication();
241     }
242     AsyncResponseStream* s = request->beginResponseStream("application/json");
243     s->print("{");
244     for (int i = 0; i < cfg.getParametersCount(); i++) {
245       ConfigParameter* param = cfg.getParameter(i);
246       if (i) s->print(",");
247       s->print("\"");
248       s->print(param->getID());
249       s->print("\":");
250       switch (param->getType()) {
251         case 'B':
252           s->print(param->getBoolValue() ? "true" : "false");
253           break;
254         case 'I':
255           s->print(param->getIntValue());
256           break;
257         case 'F':
258           s->print(param->getFloatValue());
259           break;
260         case 'S':
261           s->print("\"");
262           s->print(param->getCharValue());
263           s->print("\"");
264           break;
265       }
266     }
267     s->print("}");
268     request->send(s);
269   });
270
271   server.on("/config/set", HTTP_GET, [](AsyncWebServerRequest* request) {
272     if (auth_user && auth_pwd && auth_user[0] && auth_pwd[0] && !request->authenticate(auth_user, auth_pwd)) {
273       return request-> requestAuthentication();
274     }
275     if(request->hasParam("name") && (request->hasParam("value"))) {
276       const char* name = request->getParam("name")->value().c_str();
277       const char* value = request->getParam("value")->value().c_str();
278       Serial.print(name); Serial.print(" = "); Serial.println(value);
279       char buf[256];
280       ConfigParameter* param = cfg.getParam(name);
281       if (param) {
282         switch (param->getType()) {
283           case 'B':
284             cfg.setValue(name, strcmp(value, "true")==0);
285             sprintf(buf,"{\"%s\":%s}", name, param->getBoolValue()?"true":"false");
286             break;
287           case 'I':
288             cfg.setValue(name, atoi(value));
289             sprintf(buf,"{\"%s\":%d}", name, param->getIntValue());
290             break;
291           case 'F':
292             cfg.setValue(name, atof(value));
293             sprintf(buf,"{\"%s\":%f}", name, param->getFloatValue());
294             break;
295           case 'S':
296             cfg.setValue(name, value);
297             sprintf(buf,"{\"%s\":\"%s\"}", name, param->getCharValue());
298             break;
299         }
300         apply(name);
301       } else {
302           request->send(500, "text/plain", "Unknown parameter name");
303           return;
304       }
305       request->send(200,"application/json",buf);
306      } else {
307       request->send(500, "text/plain", "Not all parameters set");
308     }
309   });
310
311   AsyncCallbackJsonWebHandler* configUploadHandler = new AsyncCallbackJsonWebHandler("/config/put", [](AsyncWebServerRequest *request, JsonVariant &json) {
312     cfg.clear();
313     // first - set values
314     for( JsonPair kv : json.as<JsonObject>() ) {
315       const char* name = kv.key().c_str();
316       if (kv.value().is<bool>()) {
317         cfg.setValue(name, kv.value().as<bool>());
318       } else if (kv.value().is<int>()) {
319         cfg.setValue(name, kv.value().as<int>());
320       } else if (kv.value().is<double>()) {
321         cfg.setValue(name, kv.value().as<double>());
322       } else if (kv.value().is<char*>()) {
323         cfg.setValue(name, kv.value().as<char*>());
324       } else {
325         Serial.print(F("Неопознанный тип значения параметра ")); Serial.print(name); Serial.print(": "); Serial.println(kv.value().as<String>().c_str());
326         cfg.clear();
327         setupConfig();
328         request->send(500, "text/plain", "Unknown parameter type");
329       }
330     }
331     // second - handle all changes
332     for( JsonPair kv : json.as<JsonObject>() ) {
333       apply(kv.key().c_str());
334     }
335     pendingWiFi = false;
336     pendingAuth = false;
337     saveConfig();
338     message(F("Применены сохраненные настройки"),5);
339     reportMessage(F("Применены сохраненные настройки"));
340     millisScheduled = millis() + 10000;
341     actionScheduled = "restart";
342     request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Настройки восстановлены из резервной копии\"}");
343   });
344
345   server.addHandler(configUploadHandler).setAuthentication(auth_user,auth_pwd);
346
347   server.serveStatic("ui", LittleFS, "/ui.json").setAuthentication(auth_user,auth_pwd);
348
349   server.serveStatic("/", LittleFS, "/web/").setDefaultFile("index.html").setAuthentication(auth_user,auth_pwd);
350
351   server.onNotFound([](AsyncWebServerRequest *request){
352     request->send(404,"text/plain","Not found");
353   });
354
355   events.onConnect([](AsyncEventSourceClient *client){
356     sendInitial(client);
357   });
358
359   events.setAuthentication(auth_user,auth_pwd);
360
361   server.addHandler(&events);
362
363   server.begin();
364
365   tKeepalive.attach(2, sendKeepalive);
366
367 }
368
369 #define CFG_AUTOSAVE 15
370
371 void tickWeb() {
372   static char storedSSID[64];
373   static char storedPSK[64];
374   static bool connectInProgress = false;
375   static unsigned long connectMillis = 0;
376   if (actionScheduled && millis()>millisScheduled+300) {
377     Serial.print(F("Запланированная операция ")); Serial.println(actionScheduled);
378     //
379     if (strcmp(actionScheduled,"reset") == 0) {
380       server.end();
381       reset();
382     } else if (strcmp(actionScheduled,"restart") == 0) {
383       server.end();
384       reboot();
385     } else if (strcmp(actionScheduled,"auth") == 0) {
386       Serial.println("Логин/пароль изменены");
387       strncpy(auth_user,cfg.getCharValue(F("auth_user")),31);
388       strncpy(auth_pwd,cfg.getCharValue(F("auth_pwd")),31);
389       pendingAuth = false;
390     } else if (strcmp(actionScheduled,"wifi") == 0) {
391       Serial.println("Применяю настройки сети");
392       strcpy(storedSSID,WiFi.SSID().c_str());
393       strcpy(storedPSK,WiFi.psk().c_str());
394       WiFi.mode(WIFI_STA);
395       WiFi.begin(cfg.getCharValue("sta_ssid"),cfg.getCharValue("sta_psk"));
396       connectInProgress = true;
397       connectMillis = millis();
398     } else if (strcmp(actionScheduled,"save") == 0 && !pendingWiFi && !pendingAuth) {
399       saveConfig();
400     }
401     actionScheduled = nullptr;
402   }
403   if (connectInProgress && (millis() > connectMillis + 1000)) {
404     if (WiFi.status() == WL_CONNECTED) {
405       char buf[64];
406       sprintf(buf,"Подключен к %s, IP=%s", WiFi.SSID(), WiFi.localIP().toString().c_str());
407       Serial.println(buf);
408       message(buf,1);
409       pendingWiFi = false;
410       connectInProgress = false;
411     } else if (WiFi.status() == WL_CONNECT_FAILED || WiFi.status() == WL_NO_SSID_AVAIL || ((WiFi.status() == WL_CONNECTION_LOST || WiFi.status() == WL_DISCONNECTED) && (millis()>connectMillis+1000*cfg.getIntValue("sta_wait")))) {
412       Serial.println(F("Подключение не удалось, возвращаю прежние настройки"));
413       message(F("Подключение не удалось, возвращаю прежние настройки"),1);
414       WiFi.begin(storedSSID, storedPSK);
415       connectInProgress = false;
416     }
417   }
418
419   if (!pendingWiFi && !pendingAuth && cfg.getTimestamp() && cfg.getTimestamp() < now - CFG_AUTOSAVE) {
420     saveConfig();
421     reportMessage(F("Настройки сохранены"));
422     Serial.println(F("Настройки сохранены"));
423   }
424 }