Files
kc868-a2_solar/src/mqtt_client.cpp
T
gilles 14b3967590 Ajout publication MQTT (PubSubClient)
- Topics individuels par capteur sous un topic de base configurable (défaut: solar/)
  PV, batterie (tension/SOC/temp/statut), load, énergie, soleil, RS485, relais, entrées DI
- Abonnement relay/1/set et relay/2/set pour piloter les relais depuis MQTT
- Config NVS : serveur, port, user/pass optionnel, topic base, intervalle (défaut 30s)
- Reconnexion automatique toutes les 15s si broker inaccessible
- Publication immédiate après connexion et après changement de config
- Route GET/POST /api/mqtt + UI onglet Config avec liste des topics générée dynamiquement
- Stubs QEMU (#ifndef QEMU_BUILD)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 07:13:05 +02:00

250 lines
8.0 KiB
C++

#include "mqtt_client.h"
#ifndef QEMU_BUILD
#include <WiFi.h>
#include <PubSubClient.h>
#include <Preferences.h>
#include <ArduinoJson.h>
#include "state.h"
#include "config.h"
// --- Config ---
static bool mqttActif = false;
static String mqttServer = "192.168.1.36";
static uint16_t mqttPort = 1883;
static String mqttUser;
static String mqttPass;
static String mqttBase = "solar"; // topic de base
static uint32_t mqttInterval = 30000; // ms entre publications
// --- État runtime ---
static WiFiClient wifiClient;
static PubSubClient mqttClient(wifiClient);
static bool mqttConnecte = false;
static unsigned long tDernierePubli = 0;
static unsigned long tDerniereConnex = 0;
static const unsigned long RECONNECT_INTERVAL = 15000UL;
// ---------------------------------------------------------------------------
// NVS
// ---------------------------------------------------------------------------
static void chargerNVS() {
Preferences p;
p.begin("mqtt", true);
mqttActif = p.getBool("enabled", false);
mqttServer = p.getString("server", "192.168.1.36");
mqttPort = p.getUShort("port", 1883);
mqttUser = p.getString("user", "");
mqttPass = p.getString("pass", "");
mqttBase = p.getString("base", "solar");
mqttInterval = p.getULong("interval", 30000);
p.end();
}
// ---------------------------------------------------------------------------
// Topics dérivés du topic de base
// ---------------------------------------------------------------------------
static String t(const char* suffixe) { return mqttBase + "/" + suffixe; }
// ---------------------------------------------------------------------------
// Callback réception (commande relais / entrées)
// ---------------------------------------------------------------------------
static void mqttCallback(char* topic, byte* payload, unsigned int len) {
String msg;
for (unsigned int i = 0; i < len; i++) msg += (char)payload[i];
msg.trim();
bool etat = (msg == "ON" || msg == "1" || msg == "true");
String top = String(topic);
if (top == t("relay/1/set")) {
state.relay1 = etat;
digitalWrite(PIN_RELAY1, etat ? HIGH : LOW);
Serial.printf("[MQTT] Relais 1 → %s\n", etat ? "ON" : "OFF");
} else if (top == t("relay/2/set")) {
state.relay2 = etat;
digitalWrite(PIN_RELAY2, etat ? HIGH : LOW);
Serial.printf("[MQTT] Relais 2 → %s\n", etat ? "ON" : "OFF");
}
}
// ---------------------------------------------------------------------------
// Connexion / reconnexion
// ---------------------------------------------------------------------------
static void connecterMqtt() {
if (WiFi.status() != WL_CONNECTED) return;
mqttClient.setServer(mqttServer.c_str(), mqttPort);
mqttClient.setCallback(mqttCallback);
mqttClient.setBufferSize(512);
String clientId = "kc868-" + WiFi.macAddress();
clientId.replace(":", "");
bool ok;
if (mqttUser.length() > 0) {
ok = mqttClient.connect(clientId.c_str(), mqttUser.c_str(), mqttPass.c_str());
} else {
ok = mqttClient.connect(clientId.c_str());
}
if (ok) {
mqttConnecte = true;
Serial.printf("[MQTT] Connecté — %s:%u base: %s intervalle: %us\n",
mqttServer.c_str(), mqttPort,
mqttBase.c_str(), mqttInterval / 1000);
// Abonnements commandes
mqttClient.subscribe(t("relay/1/set").c_str());
mqttClient.subscribe(t("relay/2/set").c_str());
// Publication immédiate après connexion
mqttPublierEtat();
} else {
mqttConnecte = false;
Serial.printf("[MQTT] Échec connexion (rc=%d) — retry dans %lus\n",
mqttClient.state(), RECONNECT_INTERVAL / 1000);
}
}
// ---------------------------------------------------------------------------
// Publication état complet
// ---------------------------------------------------------------------------
static void pub(const char* suffixe, float val, int decimales = 2) {
char buf[16];
dtostrf(val, 1, decimales, buf);
mqttClient.publish(t(suffixe).c_str(), buf, true);
}
static void pub(const char* suffixe, bool val) {
mqttClient.publish(t(suffixe).c_str(), val ? "ON" : "OFF", true);
}
static void pub(const char* suffixe, int val) {
mqttClient.publish(t(suffixe).c_str(), String(val).c_str(), true);
}
void mqttPublierEtat() {
if (!mqttConnecte || !mqttClient.connected()) return;
// Panneau solaire
pub("pv/voltage", state.pv);
pub("pv/current", state.pvCurrent);
// Batterie
pub("battery/voltage", state.battery);
pub("battery/soc", (int)state.batSOC);
pub("battery/temperature", state.batTemperature, 1);
pub("battery/status", (int)state.batStatut);
// Sortie de charge
pub("load/voltage", state.loadVoltage);
pub("load/current", state.loadCurrent);
pub("load/power", state.loadPower, 1);
// Énergie
pub("energy/generated/today", state.energieGenJour);
pub("energy/generated/total", state.energieGenTotal);
pub("energy/consumed/today", state.energieConJour);
pub("energy/consumed/total", state.energieConTotal);
// État général
pub("sun", state.sun);
pub("rs485/ok", state.rs485_ok);
// Relais
pub("relay/1", state.relay1);
pub("relay/2", state.relay2);
// Entrées numériques
pub("input/1", state.di1);
pub("input/2", state.di2);
tDernierePubli = millis();
}
// ---------------------------------------------------------------------------
// API publique
// ---------------------------------------------------------------------------
void initMqtt() {
chargerNVS();
Serial.printf("[MQTT] %s — %s:%u base: %s intervalle: %us\n",
mqttActif ? "Activé" : "Désactivé",
mqttServer.c_str(), mqttPort,
mqttBase.c_str(), mqttInterval / 1000);
}
void gererMqtt() {
if (!mqttActif) return;
if (WiFi.status() != WL_CONNECTED) {
if (mqttConnecte) { mqttConnecte = false; }
return;
}
if (!mqttClient.connected()) {
mqttConnecte = false;
unsigned long maintenant = millis();
if ((maintenant - tDerniereConnex) >= RECONNECT_INTERVAL) {
tDerniereConnex = maintenant;
connecterMqtt();
}
return;
}
mqttClient.loop();
// Publication périodique
if ((millis() - tDernierePubli) >= mqttInterval) {
mqttPublierEtat();
}
}
bool setMqttConfig(bool enabled, const char* server, uint16_t port,
const char* user, const char* pass,
const char* topicBase, uint32_t intervalMs) {
Preferences p;
p.begin("mqtt", false);
p.putBool("enabled", enabled);
p.putString("server", server);
p.putUShort("port", port);
p.putString("user", user);
p.putString("pass", pass);
p.putString("base", topicBase);
p.putULong("interval", intervalMs);
p.end();
if (mqttConnecte) { mqttClient.disconnect(); mqttConnecte = false; }
tDerniereConnex = 0;
chargerNVS();
Serial.printf("[MQTT] Config mise à jour — %s\n", mqttActif ? "activé" : "désactivé");
return true;
}
void getMqttJson(String &out) {
JsonDocument doc;
doc["enabled"] = mqttActif;
doc["connected"] = mqttConnecte;
doc["server"] = mqttServer;
doc["port"] = mqttPort;
doc["user"] = mqttUser;
doc["pass"] = mqttPass;
doc["base"] = mqttBase;
doc["interval"] = mqttInterval / 1000; // en secondes pour l'UI
serializeJson(doc, out);
}
#else
// --- Stubs QEMU ---
void initMqtt() { Serial.println("[MQTT] Désactivé (build QEMU)"); }
void gererMqtt() {}
void mqttPublierEtat() {}
void getMqttJson(String &out) { out = "{\"enabled\":false,\"connected\":false}"; }
bool setMqttConfig(bool, const char*, uint16_t, const char*, const char*,
const char*, uint32_t) { return false; }
#endif