JXCT Soil Sensor 7-in-1 v3.4.9 (June 2025)
Professional IoT soil monitoring system with ESP32, Modbus RTU, MQTT, and advanced compensation algorithms
Загрузка...
Поиск...
Не найдено
mqtt_client.cpp
См. документацию.
1
6#include <Arduino.h>
7#include <PubSubClient.h>
8#include <ArduinoJson.h>
9#include "modbus_sensor.h"
10#include "wifi_manager.h"
11#include <WiFiClient.h>
12#include "jxct_device_info.h"
13#include "jxct_config_vars.h"
14#include "jxct_format_utils.h"
15#include "logger.h"
16#include "debug.h" // ✅ Добавляем систему условной компиляции
17#include "jxct_constants.h" // ✅ Централизованные константы
18#include <NTPClient.h>
19#include "ota_manager.h"
20extern NTPClient* timeClient;
21
22WiFiClient espClient;
23PubSubClient mqttClient(espClient);
24bool mqttConnected = false;
25
26// ✅ Заменяем String на статический буфер
27static char mqttLastErrorBuffer[128] = "";
28const char* getMqttLastError()
29{
31}
32
33// ✅ Статические буферы для топиков и ID
34static char clientIdBuffer[32] = "";
35static char statusTopicBuffer[128] = "";
36static char commandTopicBuffer[128] = "";
37static char otaStatusTopicBuffer[128] = "";
38static char otaCommandTopicBuffer[128] = "";
39
40// ✅ НОВОЕ: Кэш Home Assistant конфигураций
41// ИСПРАВЛЕНО: Перенос больших структур в статическую память для предотвращения переполнения стека
43{
44 char tempConfig[512];
45 char humConfig[512];
46 char ecConfig[512];
47 char phConfig[512];
48 char nitrogenConfig[512];
50 char potassiumConfig[512];
51 bool isValid;
54} haConfigCache = {"", "", "", "", "", "", "", false, "", ""};
55
56// ✅ НОВОЕ: Кэш топиков публикации (перенесен в статическую память)
57static char pubTopicCache[7][128] = {"", "", "", "", "", "", ""};
58static bool pubTopicCacheValid = false;
59
60// ✅ НОВОЕ: Кэш данных датчика JSON (перенесен в статическую память)
61static char cachedSensorJson[256] = "";
62static unsigned long lastCachedSensorTime = 0;
63static bool sensorJsonCacheValid = false;
64
65// ✅ ОПТИМИЗАЦИЯ 3.3: DNS кэширование для избежания повторных запросов
67{
69 IPAddress cachedIP;
70 unsigned long cacheTime;
71 bool isValid;
72} dnsCacheMqtt = {"", IPAddress(0, 0, 0, 0), 0, false};
73
74// Forward declarations
75void mqttCallback(char* topic, byte* payload, unsigned int length);
77void invalidateHAConfigCache(); // ✅ НОВОЕ: Инвалидация кэша
78IPAddress getCachedIP(const char* hostname); // ✅ НОВОЕ: Forward declaration для DNS кэша
79
80// ✅ Оптимизированная функция getClientId с буфером
81const char* getClientId()
82{
83 if (clientIdBuffer[0] == '\0')
84 { // Кэшируем результат
85 uint8_t mac[6];
86 WiFi.macAddress(mac);
87 snprintf(clientIdBuffer, sizeof(clientIdBuffer), "JXCT_%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2],
88 mac[3], mac[4], mac[5]);
89 }
90 return clientIdBuffer;
91}
92
93// ✅ Оптимизированная функция getMqttClientName
94const char* getMqttClientName()
95{
96 if (strlen(config.mqttDeviceName) > 0)
97 {
98 return config.mqttDeviceName;
99 }
100 else
101 {
102 return getClientId();
103 }
104}
105
106// ✅ Оптимизированная функция getStatusTopic с буфером
107const char* getStatusTopic()
108{
109 if (statusTopicBuffer[0] == '\0')
110 { // Кэшируем результат
111 snprintf(statusTopicBuffer, sizeof(statusTopicBuffer), "%s/status", config.mqttTopicPrefix);
112 }
113 return statusTopicBuffer;
114}
115
116// ✅ Оптимизированная функция getCommandTopic с буфером
117const char* getCommandTopic()
118{
119 if (commandTopicBuffer[0] == '\0')
120 { // Кэшируем результат
121 snprintf(commandTopicBuffer, sizeof(commandTopicBuffer), "%s/command", config.mqttTopicPrefix);
122 }
123 return commandTopicBuffer;
124}
125
126// OTA статус topic
127static const char* getOtaStatusTopic()
128{
129 if (otaStatusTopicBuffer[0] == '\0')
130 snprintf(otaStatusTopicBuffer, sizeof(otaStatusTopicBuffer), "%s/ota/status", config.mqttTopicPrefix);
132}
133
134static const char* getOtaCommandTopic()
135{
136 if (otaCommandTopicBuffer[0] == '\0')
137 snprintf(otaCommandTopicBuffer, sizeof(otaCommandTopicBuffer), "%s/ota/command", config.mqttTopicPrefix);
139}
140
141void publishAvailability(bool online)
142{
143 const char* topic = getStatusTopic();
144 const char* payload = online ? "online" : "offline";
145 DEBUG_PRINTF("[publishAvailability] Публикация статуса: %s в топик %s\n", payload, topic);
146 mqttClient.publish(topic, payload, true);
147}
148
150{
151 DEBUG_PRINTLN("[КРИТИЧЕСКАЯ ОТЛАДКА] Инициализация MQTT");
152
153 // Расширенная диагностика WiFi
154 DEBUG_PRINTLN("[WiFi Debug] Статус подключения:");
155 DEBUG_PRINTF("WiFi статус: %d\n", WiFi.status());
156 DEBUG_PRINTF("IP-адрес: %s\n", WiFi.localIP().toString().c_str());
157 DEBUG_PRINTF("Маска подсети: %s\n", WiFi.subnetMask().toString().c_str());
158 DEBUG_PRINTF("Шлюз: %s\n", WiFi.gatewayIP().toString().c_str());
159
160 DEBUG_PRINTLN("[MQTT Debug] Параметры:");
161 DEBUG_PRINTF("MQTT включен: %d\n", config.flags.mqttEnabled);
162 DEBUG_PRINTF("Сервер: %s\n", config.mqttServer);
163 DEBUG_PRINTF("Порт: %d\n", config.mqttPort);
164 DEBUG_PRINTF("Префикс топика: %s\n", config.mqttTopicPrefix);
165 DEBUG_PRINTF("Пользователь: %s\n", config.mqttUser);
166
167 if (!config.flags.mqttEnabled || strlen(config.mqttServer) == 0)
168 {
169 ERROR_PRINTLN("[ОШИБКА] MQTT не может быть инициализирован");
170 return;
171 }
172
173 // ✅ ОПТИМИЗАЦИЯ 3.3: Используем кэшированный DNS резолвинг
174 IPAddress mqttServerIP = getCachedIP(config.mqttServer);
175 if (mqttServerIP == IPAddress(0, 0, 0, 0))
176 {
177 ERROR_PRINTF("[DNS] Не удалось разрешить DNS для %s\n", config.mqttServer);
178 strlcpy(mqttLastErrorBuffer, "Ошибка DNS резолвинга", sizeof(mqttLastErrorBuffer));
179 return;
180 }
181
182 DEBUG_PRINTF("[DNS] MQTT сервер %s -> %s\n", config.mqttServer, mqttServerIP.toString().c_str());
183 mqttClient.setServer(mqttServerIP, config.mqttPort); // Используем IP вместо hostname
184 mqttClient.setCallback(mqttCallback);
185 mqttClient.setKeepAlive(30);
186 mqttClient.setSocketTimeout(30);
187
188 INFO_PRINTLN("[MQTT] Инициализация завершена с DNS кэшированием");
189}
190
192{
193 DEBUG_PRINTLN("[КРИТИЧЕСКАЯ ОТЛАДКА] Попытка подключения к MQTT");
194
195 // Проверка WiFi
196 if (WiFi.status() != WL_CONNECTED)
197 {
198 ERROR_PRINTLN("[ОШИБКА] WiFi не подключен!");
199 return false;
200 }
201
202 // Расширенная проверка параметров
203 if (strlen(config.mqttServer) == 0)
204 {
205 ERROR_PRINTLN("[ОШИБКА] Не указан MQTT-сервер");
206 return false;
207 }
208
209 // Попытка подключения с максимальной детализацией
210 const char* clientId = getMqttClientName();
211 DEBUG_PRINTF("[MQTT] Сервер: %s\n", config.mqttServer);
212 DEBUG_PRINTF("[MQTT] Порт: %d\n", config.mqttPort);
213 DEBUG_PRINTF("[MQTT] ID клиента: %s\n", clientId);
214 DEBUG_PRINTF("[MQTT] Пользователь: %s\n", config.mqttUser);
215 DEBUG_PRINTF("[MQTT] Пароль: %s\n", config.mqttPassword);
216
217 mqttClient.setServer(config.mqttServer, config.mqttPort);
218
219 // Попытка подключения с максимально подробной информацией
220 bool result = mqttClient.connect(clientId,
221 config.mqttUser, // может быть пустым
222 config.mqttPassword, // может быть пустым
224 1, // QoS
225 true, // retain
226 "offline" // will message
227 );
228
229 DEBUG_PRINTF("[MQTT] Результат подключения: %d\n", result);
230
231 // Расшифровка кодов состояния
232 int state = mqttClient.state();
233 DEBUG_PRINTF("[MQTT] Состояние клиента: %d - ", state);
234
235 // Сохраняем ошибку в буфер для доступа извне
236 switch (state)
237 {
238 case -4:
239 DEBUG_PRINTLN("Тайм-аут подключения");
240 strncpy(mqttLastErrorBuffer, "Тайм-аут подключения", sizeof(mqttLastErrorBuffer) - 1);
241 break;
242 case -3:
243 DEBUG_PRINTLN("Соединение потеряно");
244 strncpy(mqttLastErrorBuffer, "Соединение потеряно", sizeof(mqttLastErrorBuffer) - 1);
245 break;
246 case -2:
247 DEBUG_PRINTLN("Ошибка подключения");
248 strncpy(mqttLastErrorBuffer, "Ошибка подключения", sizeof(mqttLastErrorBuffer) - 1);
249 break;
250 case -1:
251 DEBUG_PRINTLN("Отключено");
252 strncpy(mqttLastErrorBuffer, "Отключено", sizeof(mqttLastErrorBuffer) - 1);
253 break;
254 case 0:
255 DEBUG_PRINTLN("Подключено");
256 strncpy(mqttLastErrorBuffer, "Подключено", sizeof(mqttLastErrorBuffer) - 1);
257 break;
258 case 1:
259 DEBUG_PRINTLN("Неверный протокол");
260 strncpy(mqttLastErrorBuffer, "Неверный протокол", sizeof(mqttLastErrorBuffer) - 1);
261 break;
262 case 2:
263 DEBUG_PRINTLN("Неверный ID клиента");
264 strncpy(mqttLastErrorBuffer, "Неверный ID клиента", sizeof(mqttLastErrorBuffer) - 1);
265 break;
266 case 3:
267 DEBUG_PRINTLN("Сервер недоступен");
268 strncpy(mqttLastErrorBuffer, "Сервер недоступен", sizeof(mqttLastErrorBuffer) - 1);
269 break;
270 case 4:
271 DEBUG_PRINTLN("Неверные учетные данные");
272 strncpy(mqttLastErrorBuffer, "Неверные учетные данные", sizeof(mqttLastErrorBuffer) - 1);
273 break;
274 case 5:
275 DEBUG_PRINTLN("Не авторизован");
276 strncpy(mqttLastErrorBuffer, "Не авторизован", sizeof(mqttLastErrorBuffer) - 1);
277 break;
278 default:
279 DEBUG_PRINTLN("Неизвестная ошибка");
280 strncpy(mqttLastErrorBuffer, "Неизвестная ошибка", sizeof(mqttLastErrorBuffer) - 1);
281 break;
282 }
283
284 // Если подключились успешно
285 if (result)
286 {
287 INFO_PRINTLN("[MQTT] Подключение успешно!");
288
289 // Подписываемся на топик команд
290 const char* commandTopic = getCommandTopic();
291 mqttClient.subscribe(commandTopic);
292 DEBUG_PRINTF("[MQTT] Подписались на топик команд: %s\n", commandTopic);
293
294 const char* otaCmdTopic = getOtaCommandTopic();
295 mqttClient.subscribe(otaCmdTopic);
296 DEBUG_PRINTF("[MQTT] Подписались на OTA команды: %s\n", otaCmdTopic);
297
298 // Публикуем статус availability
300
301 // Публикуем конфигурацию Home Assistant discovery если включено
302 if (config.flags.hassEnabled)
303 {
305 }
306 }
307
308 return result;
309}
310
312{
313 if (!config.flags.mqttEnabled)
314 {
315 return;
316 }
317
318 static bool wasConnected = false;
319 bool isConnected = mqttClient.connected();
320
321 // Логирование изменений состояния подключения
322 if (wasConnected && !isConnected)
323 {
324 logWarn("MQTT подключение потеряно!");
325 }
326 else if (!wasConnected && isConnected)
327 {
328 logSuccess("MQTT переподключение успешно");
329 }
330 wasConnected = isConnected;
331
332 if (!isConnected)
333 {
334 static unsigned long lastReconnectAttempt = 0;
335 if (millis() - lastReconnectAttempt > 5000)
336 {
337 lastReconnectAttempt = millis();
338 logMQTT("Попытка переподключения...");
339 connectMQTT();
340 }
341 }
342 else
343 {
344 mqttClient.loop();
345
346 // Публикуем статус OTA, если изменился (не чаще 5 сек)
347 static char lastOtaStatus[64] = "";
348 static unsigned long lastOtaPublish = 0;
349 const unsigned long OTA_STATUS_INTERVAL = 5000;
350 if (millis() - lastOtaPublish > OTA_STATUS_INTERVAL)
351 {
352 const char* cur = getOtaStatus();
353 if (strcmp(cur, lastOtaStatus) != 0)
354 {
355 mqttClient.publish(getOtaStatusTopic(), cur, true);
356 strlcpy(lastOtaStatus, cur, sizeof(lastOtaStatus));
357 }
358 lastOtaPublish = millis();
359 }
360 }
361}
362
363// ДЕЛЬТА-ФИЛЬТР v2.2.1: Проверка необходимости публикации
365{
366 static int skipCounter = 0;
367
368 // Первая публикация - всегда публикуем
369 if (sensorData.last_mqtt_publish == 0)
370 {
371 return true;
372 }
373
374 // Принудительная публикация каждые N циклов (настраиваемо v2.3.0)
375 if (++skipCounter >= config.forcePublishCycles)
376 {
377 skipCounter = 0;
378 DEBUG_PRINTLN("[DELTA] Принудительная публикация (цикл)");
379 return true;
380 }
381
382 // Проверяем дельта изменения
383 bool hasSignificantChange = false;
384
385 if (abs(sensorData.temperature - sensorData.prev_temperature) >= config.deltaTemperature)
386 {
387 DEBUG_PRINTF("[DELTA] Температура изменилась: %.1f -> %.1f\n", sensorData.prev_temperature,
388 sensorData.temperature);
389 hasSignificantChange = true;
390 }
391
392 if (abs(sensorData.humidity - sensorData.prev_humidity) >= config.deltaHumidity)
393 {
394 DEBUG_PRINTF("[DELTA] Влажность изменилась: %.1f -> %.1f\n", sensorData.prev_humidity, sensorData.humidity);
395 hasSignificantChange = true;
396 }
397
398 if (abs(sensorData.ph - sensorData.prev_ph) >= config.deltaPh)
399 {
400 DEBUG_PRINTF("[DELTA] pH изменился: %.1f -> %.1f\n", sensorData.prev_ph, sensorData.ph);
401 hasSignificantChange = true;
402 }
403
404 if (abs(sensorData.ec - sensorData.prev_ec) >= config.deltaEc)
405 {
406 DEBUG_PRINTF("[DELTA] EC изменилась: %.0f -> %.0f\n", sensorData.prev_ec, sensorData.ec);
407 hasSignificantChange = true;
408 }
409
410 if (abs(sensorData.nitrogen - sensorData.prev_nitrogen) >= config.deltaNpk)
411 {
412 DEBUG_PRINTF("[DELTA] Азот изменился: %.0f -> %.0f\n", sensorData.prev_nitrogen, sensorData.nitrogen);
413 hasSignificantChange = true;
414 }
415
416 if (abs(sensorData.phosphorus - sensorData.prev_phosphorus) >= config.deltaNpk)
417 {
418 DEBUG_PRINTF("[DELTA] Фосфор изменился: %.0f -> %.0f\n", sensorData.prev_phosphorus, sensorData.phosphorus);
419 hasSignificantChange = true;
420 }
421
422 if (abs(sensorData.potassium - sensorData.prev_potassium) >= config.deltaNpk)
423 {
424 DEBUG_PRINTF("[DELTA] Калий изменился: %.0f -> %.0f\n", sensorData.prev_potassium, sensorData.potassium);
425 hasSignificantChange = true;
426 }
427
428 if (hasSignificantChange)
429 {
430 skipCounter = 0; // Сбрасываем счетчик при значимом изменении
431 }
432 else
433 {
434 DEBUG_PRINTLN("[DELTA] Изменения незначительные, пропускаем публикацию");
435 }
436
437 return hasSignificantChange;
438}
439
441{
442 if (!config.flags.mqttEnabled || !mqttClient.connected() || !sensorData.valid)
443 {
444 return;
445 }
446
447 // ДЕЛЬТА-ФИЛЬТР v2.2.1: Проверяем необходимость публикации
448 if (!shouldPublishMqtt())
449 {
450 return;
451 }
452
453 // ✅ ОПТИМИЗАЦИЯ: Кэшируем JSON данных датчика
454 unsigned long currentTime = millis();
455 bool needToRebuildJson = false;
456
457 // Проверяем, нужно ли пересоздать JSON (данные обновились или кэш устарел)
458 if (!sensorJsonCacheValid || (currentTime - lastCachedSensorTime > 1000) || // Кэш на 1 секунду
459 (currentTime - sensorData.last_update < 100)) // Свежие данные
460 {
461 needToRebuildJson = true;
462 }
463
464 if (needToRebuildJson)
465 {
466 // Пересоздаем JSON только при необходимости
467 StaticJsonDocument<256> doc; // ✅ Уменьшен размер с 512 до 256
468
469 // ✅ ОПТИМИЗАЦИЯ 3.1: Сокращенные ключи для экономии трафика
470 doc["t"] = round(sensorData.temperature * 10) / 10.0; // temperature → t (-10 байт)
471 doc["h"] = round(sensorData.humidity * 10) / 10.0; // humidity → h (-7 байт)
472 doc["e"] = (int)round(sensorData.ec); // ec → e (стабильно)
473 doc["p"] = round(sensorData.ph * 10) / 10.0; // ph → p (стабильно)
474 doc["n"] = (int)round(sensorData.nitrogen); // nitrogen → n (-7 байт)
475 doc["r"] = (int)round(sensorData.phosphorus); // phosphorus → r (-9 байт)
476 doc["k"] = (int)round(sensorData.potassium); // potassium → k (-8 байт)
477 doc["ts"] = (long)(timeClient ? timeClient->getEpochTime() : 0); // timestamp → ts (-7 байт)
478
479 // ✅ Кэшируем результат
480 serializeJson(doc, cachedSensorJson, sizeof(cachedSensorJson));
481 lastCachedSensorTime = currentTime;
483
484 DEBUG_PRINTLN("[MQTT] Компактный JSON датчика пересоздан и закэширован");
485 }
486
487 // ✅ Кэшируем топик публикации
488 static char stateTopicBuffer[128] = "";
489 static bool stateTopicCached = false;
490 if (!stateTopicCached)
491 {
492 snprintf(stateTopicBuffer, sizeof(stateTopicBuffer), "%s/state", config.mqttTopicPrefix);
493 stateTopicCached = true;
494 }
495
496 // Публикуем кэшированный JSON
497 bool res = mqttClient.publish(stateTopicBuffer, cachedSensorJson, true);
498
499 if (res)
500 {
501 strcpy(mqttLastErrorBuffer, "");
502
503 // ДЕЛЬТА-ФИЛЬТР v2.2.1: Сохраняем текущие значения как предыдущие
504 sensorData.prev_temperature = sensorData.temperature;
505 sensorData.prev_humidity = sensorData.humidity;
506 sensorData.prev_ec = sensorData.ec;
507 sensorData.prev_ph = sensorData.ph;
508 sensorData.prev_nitrogen = sensorData.nitrogen;
509 sensorData.prev_phosphorus = sensorData.phosphorus;
510 sensorData.prev_potassium = sensorData.potassium;
511 sensorData.last_mqtt_publish = millis();
512
513 DEBUG_PRINTLN("[MQTT] Данные опубликованы, предыдущие значения обновлены");
514 }
515 else
516 {
517 strcpy(mqttLastErrorBuffer, "Ошибка публикации MQTT");
518 }
519}
520
522{
523 DEBUG_PRINTLN("[publishHomeAssistantConfig] Публикация discovery-конфигов Home Assistant...");
524 if (!config.flags.mqttEnabled || !mqttClient.connected() || !config.flags.hassEnabled)
525 {
526 DEBUG_PRINTLN("[publishHomeAssistantConfig] Условия не выполнены, публикация отменена");
527 return;
528 }
529
530 String deviceIdStr = getDeviceId();
531 const char* deviceId = deviceIdStr.c_str();
532
533 // ✅ ОПТИМИЗАЦИЯ: Проверяем кэш конфигураций
534 bool needToRebuildConfigs = false;
535 if (!haConfigCache.isValid || strcmp(haConfigCache.cachedDeviceId, deviceId) != 0 ||
536 strcmp(haConfigCache.cachedTopicPrefix, config.mqttTopicPrefix) != 0)
537 {
538 needToRebuildConfigs = true;
539 DEBUG_PRINTLN("[HA] Кэш конфигураций устарел, пересоздаем...");
540 }
541
542 if (needToRebuildConfigs)
543 {
544 // Обновляем кэшированные значения
545 strlcpy(haConfigCache.cachedDeviceId, deviceId, sizeof(haConfigCache.cachedDeviceId));
546 strlcpy(haConfigCache.cachedTopicPrefix, config.mqttTopicPrefix, sizeof(haConfigCache.cachedTopicPrefix));
547
548 // ✅ Создаем JSON конфигурации один раз и кэшируем их
549 StaticJsonDocument<256> deviceInfo;
550 deviceInfo["identifiers"] = deviceId;
551 deviceInfo["manufacturer"] = DEVICE_MANUFACTURER;
552 deviceInfo["model"] = DEVICE_MODEL;
553 deviceInfo["sw_version"] = DEVICE_SW_VERSION;
554 deviceInfo["name"] = deviceId;
555
556 // Создаем все конфигурации и сразу сериализуем в кэш
557 StaticJsonDocument<512> tempConfig;
558 tempConfig["name"] = "JXCT Temperature";
559 tempConfig["device_class"] = "temperature";
560 tempConfig["state_topic"] = String(config.mqttTopicPrefix) + "/state";
561 tempConfig["unit_of_measurement"] = "°C";
562 tempConfig["value_template"] = "{{ value_json.t }}"; // ✅ temperature → t
563 tempConfig["unique_id"] = String(deviceId) + "_temp";
564 tempConfig["availability_topic"] = String(config.mqttTopicPrefix) + "/status";
565 tempConfig["device"] = deviceInfo;
566 serializeJson(tempConfig, haConfigCache.tempConfig, sizeof(haConfigCache.tempConfig));
567
568 StaticJsonDocument<512> humConfig;
569 humConfig["name"] = "JXCT Humidity";
570 humConfig["device_class"] = "humidity";
571 humConfig["state_topic"] = String(config.mqttTopicPrefix) + "/state";
572 humConfig["unit_of_measurement"] = "%";
573 humConfig["value_template"] = "{{ value_json.h }}"; // ✅ humidity → h
574 humConfig["unique_id"] = String(deviceId) + "_hum";
575 humConfig["availability_topic"] = String(config.mqttTopicPrefix) + "/status";
576 humConfig["device"] = deviceInfo;
577 serializeJson(humConfig, haConfigCache.humConfig, sizeof(haConfigCache.humConfig));
578
579 StaticJsonDocument<512> ecConfig;
580 ecConfig["name"] = "JXCT EC";
581 ecConfig["device_class"] = "conductivity";
582 ecConfig["state_topic"] = String(config.mqttTopicPrefix) + "/state";
583 ecConfig["unit_of_measurement"] = "µS/cm";
584 ecConfig["value_template"] = "{{ value_json.e }}"; // ✅ ec → e
585 ecConfig["unique_id"] = String(deviceId) + "_ec";
586 ecConfig["availability_topic"] = String(config.mqttTopicPrefix) + "/status";
587 ecConfig["device"] = deviceInfo;
588 serializeJson(ecConfig, haConfigCache.ecConfig, sizeof(haConfigCache.ecConfig));
589
590 StaticJsonDocument<512> phConfig;
591 phConfig["name"] = "JXCT pH";
592 phConfig["device_class"] = "ph";
593 phConfig["state_topic"] = String(config.mqttTopicPrefix) + "/state";
594 phConfig["unit_of_measurement"] = "pH";
595 phConfig["value_template"] = "{{ value_json.p }}"; // ✅ ph → p
596 phConfig["unique_id"] = String(deviceId) + "_ph";
597 phConfig["availability_topic"] = String(config.mqttTopicPrefix) + "/status";
598 phConfig["device"] = deviceInfo;
599 serializeJson(phConfig, haConfigCache.phConfig, sizeof(haConfigCache.phConfig));
600
601 StaticJsonDocument<512> nitrogenConfig;
602 nitrogenConfig["name"] = "JXCT Nitrogen";
603 nitrogenConfig["state_topic"] = String(config.mqttTopicPrefix) + "/state";
604 nitrogenConfig["unit_of_measurement"] = "mg/kg";
605 nitrogenConfig["value_template"] = "{{ value_json.n }}"; // ✅ nitrogen → n
606 nitrogenConfig["unique_id"] = String(deviceId) + "_nitrogen";
607 nitrogenConfig["availability_topic"] = String(config.mqttTopicPrefix) + "/status";
608 nitrogenConfig["device"] = deviceInfo;
609 serializeJson(nitrogenConfig, haConfigCache.nitrogenConfig, sizeof(haConfigCache.nitrogenConfig));
610
611 StaticJsonDocument<512> phosphorusConfig;
612 phosphorusConfig["name"] = "JXCT Phosphorus";
613 phosphorusConfig["state_topic"] = String(config.mqttTopicPrefix) + "/state";
614 phosphorusConfig["unit_of_measurement"] = "mg/kg";
615 phosphorusConfig["value_template"] = "{{ value_json.r }}"; // ✅ phosphorus → r
616 phosphorusConfig["unique_id"] = String(deviceId) + "_phosphorus";
617 phosphorusConfig["availability_topic"] = String(config.mqttTopicPrefix) + "/status";
618 phosphorusConfig["device"] = deviceInfo;
619 serializeJson(phosphorusConfig, haConfigCache.phosphorusConfig, sizeof(haConfigCache.phosphorusConfig));
620
621 StaticJsonDocument<512> potassiumConfig;
622 potassiumConfig["name"] = "JXCT Potassium";
623 potassiumConfig["state_topic"] = String(config.mqttTopicPrefix) + "/state";
624 potassiumConfig["unit_of_measurement"] = "mg/kg";
625 potassiumConfig["value_template"] = "{{ value_json.k }}"; // ✅ potassium → k
626 potassiumConfig["unique_id"] = String(deviceId) + "_potassium";
627 potassiumConfig["availability_topic"] = String(config.mqttTopicPrefix) + "/status";
628 potassiumConfig["device"] = deviceInfo;
629 serializeJson(potassiumConfig, haConfigCache.potassiumConfig, sizeof(haConfigCache.potassiumConfig));
630
631 // ✅ Кэшируем топики публикации
632 snprintf(pubTopicCache[0], sizeof(pubTopicCache[0]), "homeassistant/sensor/%s_temperature/config", deviceId);
633 snprintf(pubTopicCache[1], sizeof(pubTopicCache[1]), "homeassistant/sensor/%s_humidity/config", deviceId);
634 snprintf(pubTopicCache[2], sizeof(pubTopicCache[2]), "homeassistant/sensor/%s_ec/config", deviceId);
635 snprintf(pubTopicCache[3], sizeof(pubTopicCache[3]), "homeassistant/sensor/%s_ph/config", deviceId);
636 snprintf(pubTopicCache[4], sizeof(pubTopicCache[4]), "homeassistant/sensor/%s_nitrogen/config", deviceId);
637 snprintf(pubTopicCache[5], sizeof(pubTopicCache[5]), "homeassistant/sensor/%s_phosphorus/config", deviceId);
638 snprintf(pubTopicCache[6], sizeof(pubTopicCache[6]), "homeassistant/sensor/%s_potassium/config", deviceId);
639 pubTopicCacheValid = true;
640
641 haConfigCache.isValid = true;
642 INFO_PRINTLN("[HA] Конфигурации созданы и закэшированы");
643 }
644
645 // ✅ Публикуем из кэша (супер быстро!)
646 mqttClient.publish(pubTopicCache[0], haConfigCache.tempConfig, true);
647 mqttClient.publish(pubTopicCache[1], haConfigCache.humConfig, true);
648 mqttClient.publish(pubTopicCache[2], haConfigCache.ecConfig, true);
649 mqttClient.publish(pubTopicCache[3], haConfigCache.phConfig, true);
650 mqttClient.publish(pubTopicCache[4], haConfigCache.nitrogenConfig, true);
651 mqttClient.publish(pubTopicCache[5], haConfigCache.phosphorusConfig, true);
652 mqttClient.publish(pubTopicCache[6], haConfigCache.potassiumConfig, true);
653
654 INFO_PRINTLN("[HA] Конфигурация Home Assistant опубликована из кэша");
655 strcpy(mqttLastErrorBuffer, "");
656}
657
659{
660 String deviceIdStr = getDeviceId();
661 const char* deviceId = deviceIdStr.c_str();
662 // Публикуем пустой payload с retain для удаления сенсоров из HA
663 mqttClient.publish(("homeassistant/sensor/" + String(deviceId) + "_temperature/config").c_str(), "", true);
664 mqttClient.publish(("homeassistant/sensor/" + String(deviceId) + "_humidity/config").c_str(), "", true);
665 mqttClient.publish(("homeassistant/sensor/" + String(deviceId) + "_ec/config").c_str(), "", true);
666 mqttClient.publish(("homeassistant/sensor/" + String(deviceId) + "_ph/config").c_str(), "", true);
667 mqttClient.publish(("homeassistant/sensor/" + String(deviceId) + "_nitrogen/config").c_str(), "", true);
668 mqttClient.publish(("homeassistant/sensor/" + String(deviceId) + "_phosphorus/config").c_str(), "", true);
669 mqttClient.publish(("homeassistant/sensor/" + String(deviceId) + "_potassium/config").c_str(), "", true);
670 INFO_PRINTLN("[MQTT] Discovery-конфиги Home Assistant удалены");
671 strcpy(mqttLastErrorBuffer, "");
672}
673
674void handleMqttCommand(const String& cmd)
675{
676 DEBUG_PRINT("[MQTT] Получена команда: ");
677 DEBUG_PRINTLN(cmd);
678 if (cmd == "reboot")
679 {
680 ESP.restart();
681 }
682 else if (cmd == "reset")
683 {
684 resetConfig();
685 ESP.restart();
686 }
687 else if (cmd == "publish_test")
688 {
690 }
691 else if (cmd == "publish_discovery")
692 {
694 }
695 else if (cmd == "remove_discovery")
696 {
698 }
699 else if (cmd == "ota_check")
700 {
702 handleOTA();
703 }
704 else if (cmd == "ota_auto_on")
705 {
706 config.flags.autoOtaEnabled = 1;
707 saveConfig();
709 }
710 else if (cmd == "ota_auto_off")
711 {
712 config.flags.autoOtaEnabled = 0;
713 saveConfig();
715 }
716 else
717 {
718 DEBUG_PRINTLN("[MQTT] Неизвестная команда");
719 }
720}
721
722void mqttCallback(char* topic, byte* payload, unsigned int length)
723{
724 String t = String(topic);
725 String message;
726 for (unsigned int i = 0; i < length; i++) message += (char)payload[i];
727 DEBUG_PRINTF("[mqttCallback] Получено сообщение: %s = %s\n", t.c_str(), message.c_str());
728 if (t == getCommandTopic() || t == getOtaCommandTopic())
729 {
730 handleMqttCommand(message);
731 }
732}
733
735{
736 // Реализация инвалидации кэша Home Assistant конфигураций
737 haConfigCache.isValid = false;
738 strcpy(haConfigCache.cachedDeviceId, "");
739 strcpy(haConfigCache.cachedTopicPrefix, "");
740 strcpy(haConfigCache.tempConfig, "");
741 strcpy(haConfigCache.humConfig, "");
742 strcpy(haConfigCache.ecConfig, "");
743 strcpy(haConfigCache.phConfig, "");
744 strcpy(haConfigCache.nitrogenConfig, "");
745 strcpy(haConfigCache.phosphorusConfig, "");
746 strcpy(haConfigCache.potassiumConfig, "");
747 INFO_PRINTLN("[MQTT] Кэш Home Assistant конфигураций инвалидирован");
748 strcpy(mqttLastErrorBuffer, "");
749}
750
751// Функция получения IP с кэшированием
752IPAddress getCachedIP(const char* hostname)
753{
754 unsigned long currentTime = millis();
755
756 // Проверяем кэш
757 if (dnsCacheMqtt.isValid && strcmp(dnsCacheMqtt.hostname, hostname) == 0 &&
758 (currentTime - dnsCacheMqtt.cacheTime < DNS_CACHE_TTL))
759 {
760 DEBUG_PRINTF("[DNS] Используем кэшированный IP %s для %s\n", dnsCacheMqtt.cachedIP.toString().c_str(),
761 hostname);
762 return dnsCacheMqtt.cachedIP;
763 }
764
765 // DNS запрос
766 IPAddress resolvedIP;
767 if (WiFi.hostByName(hostname, resolvedIP))
768 {
769 // Кэшируем результат
770 strlcpy(dnsCacheMqtt.hostname, hostname, sizeof(dnsCacheMqtt.hostname));
771 dnsCacheMqtt.cachedIP = resolvedIP;
772 dnsCacheMqtt.cacheTime = currentTime;
773 dnsCacheMqtt.isValid = true;
774 DEBUG_PRINTF("[DNS] Новый IP %s для %s кэширован\n", resolvedIP.toString().c_str(), hostname);
775 return resolvedIP;
776 }
777
778 return IPAddress(0, 0, 0, 0); // Ошибка резолвинга
779}
Config config
Определения config.cpp:34
void saveConfig()
Определения config.cpp:128
void resetConfig()
Определения config.cpp:212
String getDeviceId()
Определения config.cpp:16
#define INFO_PRINTLN(x)
Определения debug.h:37
#define DEBUG_PRINTLN(x)
Определения debug.h:18
#define ERROR_PRINTF(fmt,...)
Определения debug.h:28
#define DEBUG_PRINTF(fmt,...)
Определения debug.h:19
#define ERROR_PRINTLN(x)
Определения debug.h:27
#define DEBUG_PRINT(x)
Определения debug.h:17
Централизованные константы системы JXCT.
constexpr unsigned long DNS_CACHE_TTL
Определения jxct_constants.h:22
constexpr size_t HOSTNAME_BUFFER_SIZE
Определения jxct_constants.h:51
void logWarn(const char *format,...)
Определения logger.cpp:78
void logSuccess(const char *format,...)
Определения logger.cpp:129
void logMQTT(const char *format,...)
Определения logger.cpp:179
Система логгирования с красивым форматированием
NTPClient * timeClient
Определения main.cpp:43
SensorData sensorData
Определения modbus_sensor.cpp:18
static const char * getOtaCommandTopic()
Определения mqtt_client.cpp:134
bool mqttConnected
Определения mqtt_client.cpp:24
static char clientIdBuffer[32]
Определения mqtt_client.cpp:34
static char otaCommandTopicBuffer[128]
Определения mqtt_client.cpp:38
static struct HomeAssistantConfigCache haConfigCache
void setupMQTT()
Определения mqtt_client.cpp:149
static char mqttLastErrorBuffer[128]
Определения mqtt_client.cpp:27
const char * getMqttClientName()
Определения mqtt_client.cpp:94
static bool pubTopicCacheValid
Определения mqtt_client.cpp:58
void handleMqttCommand(const String &cmd)
Определения mqtt_client.cpp:674
void mqttCallback(char *topic, byte *payload, unsigned int length)
Определения mqtt_client.cpp:722
static char pubTopicCache[7][128]
Определения mqtt_client.cpp:57
void invalidateHAConfigCache()
Определения mqtt_client.cpp:734
void removeHomeAssistantConfig()
Определения mqtt_client.cpp:658
static unsigned long lastCachedSensorTime
Определения mqtt_client.cpp:62
static char commandTopicBuffer[128]
Определения mqtt_client.cpp:36
void handleMQTT()
Определения mqtt_client.cpp:311
static char cachedSensorJson[256]
Определения mqtt_client.cpp:61
static char otaStatusTopicBuffer[128]
Определения mqtt_client.cpp:37
static const char * getOtaStatusTopic()
Определения mqtt_client.cpp:127
void publishSensorData()
Определения mqtt_client.cpp:440
const char * getCommandTopic()
Определения mqtt_client.cpp:117
void publishAvailability(bool online)
Определения mqtt_client.cpp:141
bool connectMQTT()
Определения mqtt_client.cpp:191
static char statusTopicBuffer[128]
Определения mqtt_client.cpp:35
WiFiClient espClient
Определения mqtt_client.cpp:22
IPAddress getCachedIP(const char *hostname)
Определения mqtt_client.cpp:752
void publishHomeAssistantConfig()
Определения mqtt_client.cpp:521
struct DNSCache dnsCacheMqtt
const char * getClientId()
Определения mqtt_client.cpp:81
const char * getStatusTopic()
Определения mqtt_client.cpp:107
bool shouldPublishMqtt()
Определения mqtt_client.cpp:364
const char * getMqttLastError()
Определения mqtt_client.cpp:28
static bool sensorJsonCacheValid
Определения mqtt_client.cpp:63
PubSubClient mqttClient
void handleOTA()
Определения ota_manager.cpp:455
const char * getOtaStatus()
Определения ota_manager.cpp:45
void triggerOtaCheck()
Определения ota_manager.cpp:407
Определения mqtt_client.cpp:67
bool isValid
Определения mqtt_client.cpp:71
char hostname[HOSTNAME_BUFFER_SIZE]
Определения mqtt_client.cpp:68
unsigned long cacheTime
Определения mqtt_client.cpp:70
IPAddress cachedIP
Определения mqtt_client.cpp:69
bool isValid
Определения mqtt_client.cpp:51
char phosphorusConfig[512]
Определения mqtt_client.cpp:49
char humConfig[512]
Определения mqtt_client.cpp:45
char nitrogenConfig[512]
Определения mqtt_client.cpp:48
char tempConfig[512]
Определения mqtt_client.cpp:44
char phConfig[512]
Определения mqtt_client.cpp:47
char cachedDeviceId[32]
Определения mqtt_client.cpp:52
char ecConfig[512]
Определения mqtt_client.cpp:46
char cachedTopicPrefix[64]
Определения mqtt_client.cpp:53
char potassiumConfig[512]
Определения mqtt_client.cpp:50
static const char DEVICE_MANUFACTURER[]
Определения version.h:14
static const char DEVICE_SW_VERSION[]
Определения version.h:16
static const char DEVICE_MODEL[]
Определения version.h:15