31 "/intervals", HTTP_GET,
45 html +=
"<form action='/save_intervals' method='post'>";
47 html +=
"<div class='section'><h2>📊 Интервалы опроса и публикации</h2>";
48 html +=
"<div class='form-group'><label for='sensor_interval'>Интервал опроса датчика (сек):</label>";
49 html +=
"<input type='number' id='sensor_interval' name='sensor_interval' min='1' max='300' value='" +
50 String(
config.sensorReadInterval / 1000) +
"' required>";
51 html +=
"<div class='help'>1-300 сек. Текущее: " + String(
config.sensorReadInterval / 1000) +
52 " сек (по умолчанию: 1 сек)</div></div>";
54 html +=
"<div class='form-group'><label for='mqtt_interval'>Интервал MQTT публикации (мин):</label>";
55 html +=
"<input type='number' id='mqtt_interval' name='mqtt_interval' min='1' max='60' value='" +
56 String(
config.mqttPublishInterval / 60000) +
"' required>";
57 html +=
"<div class='help'>1-60 мин. Текущее: " + String(
config.mqttPublishInterval / 60000) +
60 html +=
"<div class='form-group'><label for='ts_interval'>Интервал ThingSpeak (мин):</label>";
61 html +=
"<input type='number' id='ts_interval' name='ts_interval' min='5' max='120' value='" +
62 String(
config.thingSpeakInterval / 60000) +
"' required>";
63 html +=
"<div class='help'>5-120 мин. Текущее: " + String(
config.thingSpeakInterval / 60000) +
67 "<div class='form-group'><label for='web_interval'>Интервал обновления веб-интерфейса (сек):</label>";
68 html +=
"<input type='number' id='web_interval' name='web_interval' min='5' max='60' value='" +
69 String(
config.webUpdateInterval / 1000) +
"' required>";
70 html +=
"<div class='help'>5-60 сек. Текущее: " + String(
config.webUpdateInterval / 1000) +
71 " сек</div></div></div>";
73 html +=
"<div class='section'><h2>🎯 Пороги дельта-фильтра</h2>";
74 html +=
"<div class='form-group'><label for='delta_temp'>Порог температуры (°C):</label>";
75 html +=
"<input type='number' id='delta_temp' name='delta_temp' min='0.1' max='5.0' step='0.1' value='" +
76 String(
config.deltaTemperature) +
"' required>";
77 html +=
"<div class='help'>0.1-5.0°C. Публикация при изменении более чем на это значение</div></div>";
79 html +=
"<div class='form-group'><label for='delta_hum'>Порог влажности (%):</label>";
80 html +=
"<input type='number' id='delta_hum' name='delta_hum' min='0.5' max='10.0' step='0.5' value='" +
81 String(
config.deltaHumidity) +
"' required>";
82 html +=
"<div class='help'>0.5-10.0%. Публикация при изменении более чем на это значение</div></div>";
84 html +=
"<div class='form-group'><label for='delta_ph'>Порог pH:</label>";
85 html +=
"<input type='number' id='delta_ph' name='delta_ph' min='0.01' max='1.0' step='0.01' value='" +
86 String(
config.deltaPh) +
"' required>";
87 html +=
"<div class='help'>0.01-1.0. Публикация при изменении более чем на это значение</div></div>";
89 html +=
"<div class='form-group'><label for='delta_ec'>Порог EC (µS/cm):</label>";
90 html +=
"<input type='number' id='delta_ec' name='delta_ec' min='10' max='500' value='" +
91 String((
int)
config.deltaEc) +
"' required>";
92 html +=
"<div class='help'>10-500 µS/cm. Публикация при изменении более чем на это значение</div></div>";
94 html +=
"<div class='form-group'><label for='delta_npk'>Порог NPK (mg/kg):</label>";
95 html +=
"<input type='number' id='delta_npk' name='delta_npk' min='1' max='50' value='" +
96 String((
int)
config.deltaNpk) +
"' required>";
98 "<div class='help'>1-50 mg/kg. Публикация при изменении более чем на это значение</div></div></div>";
100 html +=
"<div class='section'><h2>📈 Скользящее среднее</h2>";
101 html +=
"<div class='form-group'><label for='avg_window'>Размер окна усреднения:</label>";
102 html +=
"<input type='number' id='avg_window' name='avg_window' min='5' max='15' value='" +
103 String(
config.movingAverageWindow) +
"' required>";
104 html +=
"<div class='help'>5-15 измерений. Больше = плавнее, но медленнее реакция</div></div>";
106 html +=
"<div class='form-group'><label for='force_cycles'>Принудительная публикация (циклов):</label>";
107 html +=
"<input type='number' id='force_cycles' name='force_cycles' min='5' max='50' value='" +
108 String(
config.forcePublishCycles) +
"' required>";
109 html +=
"<div class='help'>5-50 циклов. Публикация каждые N циклов даже без изменений</div></div>";
112 html +=
"<div class='form-group'><label for='filter_algo'>Алгоритм обработки данных:</label>";
113 html +=
"<select id='filter_algo' name='filter_algo' required>";
114 html +=
"<option value='0'" + String(
config.filterAlgorithm == 0 ?
" selected" :
"") +
115 ">Среднее арифметическое</option>";
116 html +=
"<option value='1'" + String(
config.filterAlgorithm == 1 ?
" selected" :
"") +
117 ">Медианное значение</option>";
119 html +=
"<div class='help'>Среднее - быстрее, медиана - устойчивее к выбросам</div></div>";
121 html +=
"<div class='form-group'><label for='outlier_filter'>Фильтр выбросов >2σ:</label>";
122 html +=
"<select id='outlier_filter' name='outlier_filter' required>";
123 html +=
"<option value='0'" + String(
config.outlierFilterEnabled == 0 ?
" selected" :
"") +
124 ">Отключен</option>";
126 "<option value='1'" + String(
config.outlierFilterEnabled == 1 ?
" selected" :
"") +
">Включен</option>";
129 "<div class='help'>Автоматически отбрасывает измерения, отклоняющиеся более чем на 2 "
130 "сигма</div></div></div>";
135 "location.href='/reset_intervals'");
139 webServer.send(200,
"text/html; charset=utf-8", html);
143 webServer.on(
"/save_intervals", HTTP_POST,
150 webServer.send(403,
"text/plain",
"Недоступно в режиме точки доступа");
155 config.sensorReadInterval =
webServer.arg(
"sensor_interval").toInt() * 1000;
156 config.mqttPublishInterval =
webServer.arg(
"mqtt_interval").toInt() * 60000;
157 config.thingSpeakInterval =
webServer.arg(
"ts_interval").toInt() * 60000;
158 config.webUpdateInterval =
webServer.arg(
"web_interval").toInt() * 1000;
179 "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta http-equiv='refresh' "
180 "content='3;url=/intervals'>";
182 html +=
"<style>" + String(
getUnifiedCSS()) +
"</style></head><body><div class='container'>";
183 html +=
"<h1>" UI_ICON_SUCCESS " Настройки интервалов сохранены!</h1>";
184 html +=
"<div class='msg msg-success'>" UI_ICON_SUCCESS " Новые настройки вступили в силу</div>";
185 html +=
"<p><strong>Текущие интервалы:</strong><br>";
186 html +=
"📊 Датчик: " + String(
config.sensorReadInterval / 1000) +
" сек<br>";
187 html +=
"📡 MQTT: " + String(
config.mqttPublishInterval / 60000) +
" мин<br>";
188 html +=
"📈 ThingSpeak: " + String(
config.thingSpeakInterval / 60000) +
" мин</p>";
189 html +=
"<p><em>Возврат к настройкам через 3 секунды...</em></p>";
190 html +=
"</div>" + String(
getToastHTML()) +
"</body></html>";
191 webServer.send(200,
"text/html; charset=utf-8", html);
195 webServer.on(
"/reset_intervals", HTTP_GET,
202 webServer.send(403,
"text/plain",
"Недоступно в режиме точки доступа");
216 config.movingAverageWindow = 5;
218 config.filterAlgorithm = 0;
219 config.outlierFilterEnabled = 0;
224 "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta http-equiv='refresh' "
225 "content='2;url=/intervals'>";
227 html +=
"<style>" + String(
getUnifiedCSS()) +
"</style></head><body><div class='container'>";
230 " Настройки интервалов возвращены к значениям по умолчанию</div>";
231 html +=
"<p><em>Возврат к настройкам через 2 секунды...</em></p>";
232 html +=
"</div>" + String(
getToastHTML()) +
"</body></html>";
233 webServer.send(200,
"text/html; charset=utf-8", html);
237 webServer.on(
"/config_manager", HTTP_GET,
244 String html =
"<!DOCTYPE html><html><head><meta charset='UTF-8'><title>" UI_ICON_FOLDER
245 " Конфигурация</title>";
246 html +=
"<style>" + String(
getUnifiedCSS()) +
"</style></head><body><div class='container'>";
249 " Недоступно в режиме точки доступа</div></div></body></html>";
250 webServer.send(200,
"text/html; charset=utf-8", html);
255 "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' "
256 "content='width=device-width, initial-scale=1.0'>";
257 html +=
"<title>" UI_ICON_FOLDER " Управление конфигурацией JXCT</title>";
258 html +=
"<style>" + String(
getUnifiedCSS()) +
"</style></head><body><div class='container'>";
263 html +=
"<div class='msg msg-success'>✅ Конфигурация успешно импортирована и сохранена</div>";
266 html +=
"<div class='section'>";
267 html +=
"<h2>📤 Экспорт конфигурации</h2>";
268 html +=
"<p>Скачайте текущую конфигурацию в формате JSON (пароли заменены на заглушки):</p>";
270 "location.href='/api/v1/config/export'");
273 html +=
"<div class='section'>";
274 html +=
"<h2>📥 Импорт конфигурации</h2>";
275 html +=
"<p>Загрузите файл конфигурации для восстановления настроек:</p>";
276 html +=
"<form action='/api/config/import' method='post' enctype='multipart/form-data'>";
277 html +=
"<input type='file' name='config' accept='.json' required>";
282 html +=
"</div>" + String(
getToastHTML()) +
"</body></html>";
283 webServer.send(200,
"text/html; charset=utf-8", html);
290 static String importedJson;
292 "/api/config/import", HTTP_POST,
300 webServer.send(403,
"application/json",
"{\"error\":\"Недоступно в режиме AP\"}");
306 StaticJsonDocument<2048> doc;
307 DeserializationError err = deserializeJson(doc, importedJson);
310 String resp = String(
"{\"error\":\"Ошибка JSON: ") + err.c_str() +
"\"}";
311 webServer.send(400,
"application/json", resp);
317 if (doc.containsKey(
"wifi"))
319 JsonObject wifi = doc[
"wifi"];
320 strlcpy(
config.ssid, wifi[
"ssid"].as<
const char*>(),
sizeof(
config.ssid));
321 strlcpy(
config.password, wifi[
"password"].as<
const char*>(),
sizeof(
config.password));
324 if (doc.containsKey(
"mqtt"))
326 JsonObject mqtt = doc[
"mqtt"];
327 config.flags.mqttEnabled = mqtt[
"enabled"].as<
bool>();
328 strlcpy(
config.mqttServer, mqtt[
"server"].as<
const char*>(),
sizeof(
config.mqttServer));
329 config.mqttPort = mqtt[
"port"].as<
int>();
330 strlcpy(
config.mqttUser, mqtt[
"user"].as<
const char*>(),
sizeof(
config.mqttUser));
331 strlcpy(
config.mqttPassword, mqtt[
"password"].as<
const char*>(),
sizeof(
config.mqttPassword));
340 webServer.sendHeader(
"Location",
"/config_manager?import_ok=1",
true);
341 webServer.send(303,
"text/plain",
"Redirect");
347 if (up.status == UPLOAD_FILE_START)
351 else if (up.status == UPLOAD_FILE_WRITE)
353 importedJson += String((
const char*)up.buf, up.currentSize);
355 else if (up.status == UPLOAD_FILE_END)
361 logDebug(
"Маршруты конфигурации настроены: /intervals, /config_manager, /api/v1/config/export");
373 webServer.send(403,
"application/json",
"{\"error\":\"Недоступно в режиме точки доступа\"}");
377 StaticJsonDocument<1024> root;
380 JsonObject mqtt = root.createNestedObject(
"mqtt");
381 mqtt[
"enabled"] = (bool)
config.flags.mqttEnabled;
382 mqtt[
"server"] =
"YOUR_MQTT_SERVER_HERE";
383 mqtt[
"port"] =
config.mqttPort;
384 mqtt[
"user"] =
"YOUR_MQTT_USER_HERE";
385 mqtt[
"password"] =
"YOUR_MQTT_PASSWORD_HERE";
388 JsonObject ts = root.createNestedObject(
"thingspeak");
389 ts[
"enabled"] = (bool)
config.flags.thingSpeakEnabled;
390 ts[
"channel_id"] =
"YOUR_CHANNEL_ID_HERE";
391 ts[
"api_key"] =
"YOUR_API_KEY_HERE";
394 JsonObject intervals = root.createNestedObject(
"intervals");
395 intervals[
"sensor_read"] =
config.sensorReadInterval;
396 intervals[
"mqtt_publish"] =
config.mqttPublishInterval;
397 intervals[
"thingspeak"] =
config.thingSpeakInterval;
398 intervals[
"web_update"] =
config.webUpdateInterval;
401 JsonObject filters = root.createNestedObject(
"filters");
402 filters[
"delta_temperature"] =
config.deltaTemperature;
403 filters[
"delta_humidity"] =
config.deltaHumidity;
404 filters[
"delta_ph"] =
config.deltaPh;
405 filters[
"delta_ec"] =
config.deltaEc;
406 filters[
"delta_npk"] =
config.deltaNpk;
407 filters[
"moving_average_window"] =
config.movingAverageWindow;
408 filters[
"force_publish_cycles"] =
config.forcePublishCycles;
409 filters[
"filter_algorithm"] =
config.filterAlgorithm;
410 filters[
"outlier_filter_enabled"] =
config.outlierFilterEnabled;
413 JsonObject device = root.createNestedObject(
"device");
414 device[
"use_real_sensor"] = (bool)
config.flags.useRealSensor;
415 device[
"hass_enabled"] = (bool)
config.flags.hassEnabled;
417 root[
"export_timestamp"] = millis();
420 serializeJson(root, json);
422 webServer.sendHeader(
"Content-Disposition",
"attachment; filename=\"jxct_config_" + String(millis()) +
".json\"");
423 webServer.send(200,
"application/json", json);