feat(mqtt): FR-012 Home Assistant MQTT Discovery (v1.5.0)
- MQTT_TOPIC_AVAILABILITY + MQTT_DISCOVERY_PREFIX in config.h
- LWT auf lasercutter/availability (online/offline, retained)
- publishDiscovery(): 8 retained Entities nach jedem (Re-)Connect
binary_sensor: laser_active (device_class: running)
sensor: session_sum (min), session_sec (s)
sensor: total_time (hh:mm via Jinja2 value_template)
sensor: firmware (diagnostic)
switch: display (state aus status.display_on, cmd -> lasercutter/cmd)
button: reset_session, reboot (entity_category: config)
- Heartbeat: neue Felder laser_active, session_minutes_sum,
session_seconds, total_minutes fuer HA value_templates
- PubSubClient Buffer: 512 -> 1024 (Discovery-Nachrichten ~640 Bytes)
This commit is contained in:
parent
d8c9489e21
commit
ae3e40f98d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user