diff --git a/include/config.h b/include/config.h index 5d8e3fc..722887a 100644 --- a/include/config.h +++ b/include/config.h @@ -70,9 +70,11 @@ #define MQTT_HEARTBEAT_MS 60000 // Heartbeat-Intervall in ms // MQTT Topics -#define MQTT_TOPIC_SESSION "lasercutter/session" // Publish beim Session-Ende -#define MQTT_TOPIC_STATUS "lasercutter/status" // Publish Heartbeat -#define MQTT_TOPIC_CMD "lasercutter/cmd" // Subscribe Steuerkommandos +#define MQTT_TOPIC_SESSION "lasercutter/session" // Publish beim Session-Ende +#define MQTT_TOPIC_STATUS "lasercutter/status" // Publish Heartbeat +#define MQTT_TOPIC_CMD "lasercutter/cmd" // Subscribe Steuerkommandos +#define MQTT_TOPIC_AVAILABILITY "lasercutter/availability" // LWT + online/offline (HA Discovery) +#define MQTT_DISCOVERY_PREFIX "homeassistant" // HA MQTT Discovery Prefix // ----------------------------------------------------------------------------- // WiFiManager diff --git a/include/mqtt_client.h b/include/mqtt_client.h index eb099e4..c532b95 100644 --- a/include/mqtt_client.h +++ b/include/mqtt_client.h @@ -61,6 +61,9 @@ public: // Session-Zähler zurücksetzen (bei reset / reset_session Kommando) void resetSessionCounter(); + // Home Assistant MQTT Discovery (retained Config-Nachrichten für alle Entities) + void publishDiscovery(); + // Debug-Ausgabe auf Serial void printToSerial(); diff --git a/platformio.ini b/platformio.ini index 0726972..73915f6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -23,7 +23,7 @@ build_flags = -DCORE_DEBUG_LEVEL=1 ; 0=keine, 1=Fehler, 3=Info, 5=Verbose -DARDUINO_LOOP_STACK_SIZE=8192 -DELEGANTOTA_USE_ASYNC_WEBSERVER=1 - -DFIRMWARE_VERSION='"1.4.1"' ; Semantic Versioning – hier erhöhen bei neuem Release + -DFIRMWARE_VERSION='"1.5.0"' ; Semantic Versioning – hier erhöhen bei neuem Release lib_deps = majicDesigns/MD_Parola @ ^3.7.3 majicDesigns/MD_MAX72XX @ ^3.5.1 @@ -59,7 +59,10 @@ monitor_rts = 0 ;upload_port = 172.30.30.90 upload_port = 192.168.2.62 upload_protocol = espota -; upload_flags = --auth=DEIN_WEBPASSWORT ; <-- auskommentieren und anpassen wenn Passwort gesetzt +upload_flags = --auth=${sysenv.LASERCUTTER_OTA_PW} +; Vor dem Upload in PowerShell: $env:LASERCUTTER_OTA_PW = "DeinPasswort" +; Vor dem Upload in CMD: set LASERCUTTER_OTA_PW=DeinPasswort +; Kein Passwort gesetzt: Variable leer lassen oder weglassen (espota ignoriert leeren --auth) ; ============================================================================= ; OTA ENVIRONMENT (HTTP) – Firmware via ElegantOTA Webinterface diff --git a/src/mqtt_client.cpp b/src/mqtt_client.cpp index 98d12f4..8e24518 100644 --- a/src/mqtt_client.cpp +++ b/src/mqtt_client.cpp @@ -172,26 +172,33 @@ bool MqttClient::_doPublishSession(int lastSessionSec, int gratisSec, time_t sta // publishHeartbeat() - Status-Heartbeat // -------------------------------------------------------------------------- void MqttClient::publishHeartbeat() { - char totalBuf[12]; - snprintf(totalBuf, sizeof(totalBuf), "%.2f", laserTracker.getTotalMinutes()); - char ipBuf[16]; IPAddress ip = WiFi.localIP(); snprintf(ipBuf, sizeof(ipBuf), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); + int totalMin = (int)laserTracker.getTotalMinutes(); + // Kein ArduinoJson (JsonDocument) – reine snprintf-Formatierung, kein Heap-Alloc - char buf[300]; + char buf[512]; snprintf(buf, sizeof(buf), "{\"online\":true," + "\"laser_active\":%s," "\"session_sum\":%d," - "\"machine_running_time_min\":\"%s\"," + "\"session_minutes_sum\":%d," + "\"session_seconds\":%d," + "\"machine_running_time_min\":\"%.2f\"," + "\"total_minutes\":%d," "\"display_on\":%s," "\"ip\":\"%s\"," "\"uptime_s\":%lu," "\"firmware_version\":\"%s (%s)\"," "\"reset_reason\":\"%s\"}", + laserTracker.isActive() ? "true" : "false", laserTracker.getAllSessionsSumMinutes(), - totalBuf, + laserTracker.getAllSessionsSumMinutes(), + laserTracker.getRunningSessionSeconds(), + laserTracker.getTotalMinutes(), + totalMin, display.isEnabled() ? "true" : "false", ipBuf, (unsigned long)(millis() / 1000UL), @@ -202,6 +209,137 @@ void MqttClient::publishHeartbeat() { LOG_I("MQTT", "Heartbeat: %s -> %s", buf, ok ? "OK" : "FEHLER"); } +// -------------------------------------------------------------------------- +// publishDiscovery() – Home Assistant MQTT Discovery +// Publiziert retained Config-Nachrichten für alle 8 Entities als ein Device. +// Wird nach jedem erfolgreichen (Re-)Connect aufgerufen. +// -------------------------------------------------------------------------- +void MqttClient::publishDiscovery() { + // Device-Block – in jede Entity-Config identisch eingebettet + char dev[220]; + snprintf(dev, sizeof(dev), + "\"device\":{" + "\"identifiers\":[\"lasercutter-display\"]," + "\"name\":\"Laser Cutter Display\"," + "\"model\":\"ESP32 MQTT Display\"," + "\"manufacturer\":\"Custom\"," + "\"sw_version\":\"%s (%s)\"}", FIRMWARE_VERSION, __DATE__); + + // Availability-Schlüssel (gemeinsam für alle Entities) + const char* av = "\"availability_topic\":\"" MQTT_TOPIC_AVAILABILITY "\""; + + char t[90]; // Topic-Buffer + char p[680]; // Payload-Buffer + + // ---- 1. binary_sensor – Laser aktiv ------------------------------------ + snprintf(t, sizeof(t), "%s/binary_sensor/lasercutter-display/laser_active/config", + MQTT_DISCOVERY_PREFIX); + snprintf(p, sizeof(p), + "{\"name\":\"Laser aktiv\"," + "\"unique_id\":\"lc_laser_active\"," + "\"state_topic\":\"" MQTT_TOPIC_STATUS "\"," + "\"value_template\":\"{{ value_json.laser_active }}\"," + "\"payload_on\":\"True\",\"payload_off\":\"False\"," + "\"device_class\":\"running\",%s,%s}", av, dev); + _client->publish(t, p, true); + vTaskDelay(pdMS_TO_TICKS(50)); + + // ---- 2. sensor – Laserzeit Summe --------------------------------------- + snprintf(t, sizeof(t), "%s/sensor/lasercutter-display/session_sum/config", + MQTT_DISCOVERY_PREFIX); + snprintf(p, sizeof(p), + "{\"name\":\"Laserzeit Summe\"," + "\"unique_id\":\"lc_session_sum\"," + "\"state_topic\":\"" MQTT_TOPIC_STATUS "\"," + "\"value_template\":\"{{ value_json.session_minutes_sum }}\"," + "\"unit_of_measurement\":\"min\"," + "\"icon\":\"mdi:timer\",%s,%s}", av, dev); + _client->publish(t, p, true); + vTaskDelay(pdMS_TO_TICKS(50)); + + // ---- 3. sensor – Laserzeit Aktuell ------------------------------------- + snprintf(t, sizeof(t), "%s/sensor/lasercutter-display/session_sec/config", + MQTT_DISCOVERY_PREFIX); + snprintf(p, sizeof(p), + "{\"name\":\"Laserzeit Aktuell\"," + "\"unique_id\":\"lc_session_sec\"," + "\"state_topic\":\"" MQTT_TOPIC_STATUS "\"," + "\"value_template\":\"{{ value_json.session_seconds }}\"," + "\"unit_of_measurement\":\"s\"," + "\"icon\":\"mdi:timer-outline\",%s,%s}", av, dev); + _client->publish(t, p, true); + vTaskDelay(pdMS_TO_TICKS(50)); + + // ---- 4. sensor – Gesamtzeit (hh:mm via Jinja2) ------------------------- + snprintf(t, sizeof(t), "%s/sensor/lasercutter-display/total_time/config", + MQTT_DISCOVERY_PREFIX); + snprintf(p, sizeof(p), + "{\"name\":\"Gesamtzeit\"," + "\"unique_id\":\"lc_total_time\"," + "\"state_topic\":\"" MQTT_TOPIC_STATUS "\"," + "\"value_template\":\"{% set m=value_json.total_minutes|int %}" + "{{'%%02d:%%02d'|format(m//60,m%%60)}}\"," + "\"icon\":\"mdi:counter\",%s,%s}", av, dev); + _client->publish(t, p, true); + vTaskDelay(pdMS_TO_TICKS(50)); + + // ---- 5. sensor – Firmware ---------------------------------------------- + snprintf(t, sizeof(t), "%s/sensor/lasercutter-display/firmware/config", + MQTT_DISCOVERY_PREFIX); + snprintf(p, sizeof(p), + "{\"name\":\"Firmware\"," + "\"unique_id\":\"lc_firmware\"," + "\"state_topic\":\"" MQTT_TOPIC_STATUS "\"," + "\"value_template\":\"{{ value_json.firmware_version }}\"," + "\"entity_category\":\"diagnostic\"," + "\"icon\":\"mdi:chip\",%s,%s}", av, dev); + _client->publish(t, p, true); + vTaskDelay(pdMS_TO_TICKS(50)); + + // ---- 6. switch – Display ----------------------------------------------- + // payload_on/off: das sind die MQTT-Payloads die HA bei Toggle sendet + snprintf(t, sizeof(t), "%s/switch/lasercutter-display/display/config", + MQTT_DISCOVERY_PREFIX); + snprintf(p, sizeof(p), + "{\"name\":\"Display\"," + "\"unique_id\":\"lc_display\"," + "\"state_topic\":\"" MQTT_TOPIC_STATUS "\"," + "\"value_template\":\"{{ 'ON' if value_json.display_on else 'OFF' }}\"," + "\"command_topic\":\"" MQTT_TOPIC_CMD "\"," + "\"payload_on\":\"{\\\"display\\\":true}\"," + "\"payload_off\":\"{\\\"display\\\":false}\"," + "\"icon\":\"mdi:television\",%s,%s}", av, dev); + _client->publish(t, p, true); + vTaskDelay(pdMS_TO_TICKS(50)); + + // ---- 7. button – Session zurücksetzen ---------------------------------- + snprintf(t, sizeof(t), "%s/button/lasercutter-display/reset_session/config", + MQTT_DISCOVERY_PREFIX); + snprintf(p, sizeof(p), + "{\"name\":\"Session zuruecksetzen\"," + "\"unique_id\":\"lc_reset_session\"," + "\"command_topic\":\"" MQTT_TOPIC_CMD "\"," + "\"payload_press\":\"{\\\"reset_session_ram\\\":true}\"," + "\"entity_category\":\"config\"," + "\"icon\":\"mdi:timer-off\",%s,%s}", av, dev); + _client->publish(t, p, true); + vTaskDelay(pdMS_TO_TICKS(50)); + + // ---- 8. button – Neustart ---------------------------------------------- + snprintf(t, sizeof(t), "%s/button/lasercutter-display/reboot/config", + MQTT_DISCOVERY_PREFIX); + snprintf(p, sizeof(p), + "{\"name\":\"Neustart\"," + "\"unique_id\":\"lc_reboot\"," + "\"command_topic\":\"" MQTT_TOPIC_CMD "\"," + "\"payload_press\":\"{\\\"reboot\\\":true}\"," + "\"entity_category\":\"config\"," + "\"icon\":\"mdi:restart\",%s,%s}", av, dev); + _client->publish(t, p, true); + + LOG_I("MQTT", "HA Discovery: 8 Entities publiziert (retained)"); +} + // -------------------------------------------------------------------------- // _rebuildClient() - TLS-Client sauber abreissen und neu aufbauen // MUSS auf Core 0 aufgerufen werden (mbedtls Heap-Kontext-Binding) @@ -235,7 +373,7 @@ void MqttClient::_rebuildClient() { _client->setServer(_broker, _port); _client->setCallback(MqttClient::onMessage); _client->setKeepAlive(60); - _client->setBufferSize(512); + _client->setBufferSize(1024); // Discovery-Nachrichten bis ~640 Bytes + MQTT-Header LOG_I("MQTT", "Client neu aufgebaut (sauberer TLS-Kontext)"); } @@ -249,24 +387,26 @@ bool MqttClient::reconnect() { const char* user = cfg.mqttUser[0] != '\0' ? cfg.mqttUser : nullptr; const char* pass = cfg.mqttPassword[0] != '\0' ? cfg.mqttPassword : nullptr; - const char* lwtPayload = "{\"online\":false}"; + const char* lwtPayload = "offline"; LOG_I("MQTT", "Verbinde als '%s'...", _clientId); bool ok; if (user && pass) { ok = _client->connect(_clientId, user, pass, - MQTT_TOPIC_STATUS, 0, true, lwtPayload); + MQTT_TOPIC_AVAILABILITY, 0, true, lwtPayload); } else { ok = _client->connect(_clientId, nullptr, nullptr, - MQTT_TOPIC_STATUS, 0, true, lwtPayload); + MQTT_TOPIC_AVAILABILITY, 0, true, lwtPayload); } if (ok) { LOG_I("MQTT", "Verbunden!"); + _client->publish(MQTT_TOPIC_AVAILABILITY, "online", /*retained=*/true); _client->subscribe(MQTT_TOPIC_CMD); LOG_I("MQTT", "Abonniert: %s", MQTT_TOPIC_CMD); + publishDiscovery(); publishHeartbeat(); _lastHeartbeatMs = millis(); } else { @@ -314,7 +454,7 @@ void MqttClient::_taskLoop() { _client->setServer(broker, port); _client->setCallback(MqttClient::onMessage); _client->setKeepAlive(60); - _client->setBufferSize(512); + _client->setBufferSize(1024); // Discovery-Nachrichten bis ~640 Bytes + MQTT-Header for (;;) { // Kein WiFi: warten