Works with HA

This commit is contained in:
Simon 2026-05-30 14:23:58 +02:00
parent 34154d716d
commit dab36592a6
7 changed files with 129 additions and 122 deletions

1
.gitignore vendored
View File

@ -36,3 +36,4 @@
*.exe *.exe
*.out *.out
*.app *.app
schematic/water_temp/water_temp-backups/*

View File

@ -16,6 +16,7 @@ monitor_speed = 115200
build_flags = build_flags =
-D DEBUG -D DEBUG
-D MOCK_LORA -D MOCK_LORA
-D MQTT_MAX_PACKET_SIZE=1024
lib_deps = lib_deps =
https://github.com/tzapu/WiFiManager.git https://github.com/tzapu/WiFiManager.git
bblanchon/ArduinoJson@^7.0.4 bblanchon/ArduinoJson@^7.0.4

View File

@ -24,14 +24,9 @@ void LoraHandler::setup()
spi = new SPIClass(VSPI); spi = new SPIClass(VSPI);
spi->begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); spi->begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
// Create SX1278 instance // --- FIX APPLIED HERE ---
radio = new SX1278(new SPIDriver(spi, LORA_CS)); Module* mod = new Module(LORA_CS, LORA_IRQ, LORA_RST, RADIOLIB_NC, *spi);
radio = new SX1278(mod);
// Configure RXEN/TXEN pins
pinMode(LORA_RXEN, OUTPUT);
pinMode(LORA_TXEN, OUTPUT);
digitalWrite(LORA_RXEN, LOW);
digitalWrite(LORA_TXEN, LOW);
// Initialize LoRa module // Initialize LoRa module
int state = radio->begin(); int state = radio->begin();
@ -82,7 +77,6 @@ void LoraHandler::setup()
String currentLoRainfo = "LoRa Freq: " + String(freq) + " / SF:" + String(config.loraConfig.spreadingFactor) + " / CR: " + String(config.loraConfig.codingRate4); String currentLoRainfo = "LoRa Freq: " + String(freq) + " / SF:" + String(config.loraConfig.spreadingFactor) + " / CR: " + String(config.loraConfig.codingRate4);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", currentLoRainfo.c_str()); logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "LoRa", currentLoRainfo.c_str());
} }
ReceivedLoRaPacket LoraHandler::receivePacket() ReceivedLoRaPacket LoraHandler::receivePacket()
{ {
ReceivedLoRaPacket receivedLoraPacket; ReceivedLoRaPacket receivedLoraPacket;

View File

@ -10,8 +10,6 @@
#define LORA_CS 18 // CS --> NSS #define LORA_CS 18 // CS --> NSS
#define LORA_RST 14 #define LORA_RST 14
#define LORA_IRQ 26 // IRQ --> DIO0 #define LORA_IRQ 26 // IRQ --> DIO0
#define LORA_RXEN 2 // RX Enable
#define LORA_TXEN 13 // TX Enable
struct ReceivedLoRaPacket { struct ReceivedLoRaPacket {
String text; String text;

View File

@ -51,63 +51,55 @@ void setup()
void loop() void loop()
{ {
ReceivedLoRaPacket packet = lora.receivePacket(); ReceivedLoRaPacket packet = lora.receivePacket();
// Process received LoRa packet if (!packet.text.isEmpty())
if (!packet.text.isEmpty()) {
{ logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Processing LoRa packet: %s", packet.text.c_str());
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Processing LoRa packet: %s", packet.text.c_str());
// Try to parse JSON payload from sender JsonDocument doc;
JsonDocument doc; DeserializationError error = deserializeJson(doc, packet.text);
DeserializationError error = deserializeJson(doc, packet.text);
if (!error) { if (!error) {
float temp = doc["temp"] | 0.0; float temp = doc["temp"] | 0.0;
int battery = doc["battery"] | 0; int battery = doc["battery"] | 0;
int boot = doc["boot"] | 0; int boot = doc["boot"] | 0;
int update = doc["update"] | 0; int update = doc["update"] | 0;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Parsed - Temp: %.2f, Battery: %d, Boot: %d, Update: %d", temp, battery, boot, update); logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MAIN", "Parsed - Temp: %.2f, Battery: %d, Boot: %d, Update: %d", temp, battery, boot, update);
// Publish to MQTT with Home Assistant format if (WiFi.status() == WL_CONNECTED) {
if (WiFi.status() == WL_CONNECTED) { mqtt.publishSensorData(temp, packet.rssi, battery);
mqtt.publishSensorData(temp, packet.rssi, battery); }
}
} else {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "MAIN", "Failed to parse JSON: %s", error.c_str());
}
}
else
{
packet.rssi = 0;
packet.snr = 0.0;
}
if (WiFi.status() == WL_CONNECTED) { // Check WiFi connection if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP(); IPAddress ip = WiFi.localIP();
String line0 = "IP: " + ip.toString(); String line0 = "IP: " + ip.toString();
display.show_message(line0.c_str(), 0); display.show_message(line0.c_str(), 0);
} else { } else {
// Optional: Display message indicating WiFi not connected display.show_message("WiFi not connected", 0);
display.show_message("WiFi not connected", 0); }
String line1 = "LoRa RSSI: " + String(packet.rssi) + " dBm";
display.show_message(line1.c_str(), 1);
String line2 = "SNR: " + String(packet.snr, 1) + " dB";
display.show_message(line2.c_str(), 2);
display.show_message("Data received OK", 3);
} else {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "MAIN", "Failed to parse JSON: %s", error.c_str());
}
} }
String line1 = "LoRa RSSI: " + String(packet.rssi) + " dBm"; // Kör MQTT-klientens loop
display.show_message(line1.c_str(), 1);
String line2 = "SNR: " + String(packet.snr, 1) + " dB";
display.show_message(line2.c_str(), 2);
display.show_message("Data received OK", 3);
// Keep MQTT connection alive
mqtt.mqttRun(); mqtt.mqttRun();
sleep(5); // FIX: Ändra från 100ms till 1000ms (1 sekund).
// Detta förhindrar att ESP32:an kraschar/tappar WiFi av logg-spamming,
// men är tillräckligt snabbt för att inte missa LoRa-paket.
delay(1000);
} }
void checkButton() void checkButton()
{ {
// check for button press // check for button press

View File

@ -9,21 +9,19 @@ extern Config config;
Mqtt::Mqtt(WiFiClient& wifiClient) : mqttClient(wifiClient) { Mqtt::Mqtt(WiFiClient& wifiClient) : mqttClient(wifiClient) {
deviceId = "water_temp_receiver"; deviceId = "water_temp_receiver";
baseTopic = "homeassistant/sensor/water_temp"; // FIX 1: Låt bas-topic ligga helt fritt på roten, UTAN "homeassistant/".
// Det är hit din data (temperatur, rssi, batteri) kommer att skickas.
baseTopic = "water_temp_receiver";
} }
void Mqtt::mqttSetup() void Mqtt::mqttSetup()
{ {
String message = "MqttServer: " + config.mqttConfig.server + ", MqttPort: " + config.mqttConfig.port;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", message.c_str());
if (config.mqttConfig.server.length() > 0) if (config.mqttConfig.server.length() > 0)
{ {
String message = "MqttServer: " + config.mqttConfig.server + ", MqttPort: " + config.mqttConfig.port; String message = "MqttServer: " + config.mqttConfig.server + ", MqttPort: " + config.mqttConfig.port;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", message.c_str()); logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", message.c_str());
IPAddress mqttServerIP; IPAddress mqttServerIP;
if(mqttServerIP.fromString(config.mqttConfig.server)){ if(mqttServerIP.fromString(config.mqttConfig.server)){
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", mqttServerIP.toString().c_str());
mqttClient.setServer(mqttServerIP, config.mqttConfig.port); mqttClient.setServer(mqttServerIP, config.mqttConfig.port);
}else{ }else{
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "MQTT Invalid IP address format %s", config.mqttConfig.server); logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "MQTT Invalid IP address format %s", config.mqttConfig.server);
@ -48,34 +46,22 @@ void Mqtt::mqttRun()
void Mqtt::mqttReconnect() void Mqtt::mqttReconnect()
{ {
int retryCount = 0; int retryCount = 0;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Attempting MQTT connection..."); logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Attempting MQTT connection...");
// Loop until we're reconnected
while (!mqttClient.connected() && retryCount < 10) while (!mqttClient.connected() && retryCount < 10)
{ {
// Attempt to connect
String mqttId = config.mqttConfig.id; String mqttId = config.mqttConfig.id;
if (mqttId.isEmpty()) if (mqttId.isEmpty())
{ {
mqttId = "ESP32-"; mqttId = "ESP32-";
mqttId += String(random(0xffff), HEX); mqttId += String(random(0xffff), HEX);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Set ID: %s", mqttId );
config.setMqttId(mqttId); config.setMqttId(mqttId);
config.writeData(); config.writeData();
} }
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "Server: %s", config.mqttConfig.server.c_str());
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "Port: %d", config.mqttConfig.port);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "Username: %s", config.mqttConfig.username.c_str());
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT CONFIG", "ID: %s", config.mqttConfig.id.c_str());
if (mqttClient.connect(mqttId.c_str(), config.mqttConfig.username.c_str(), config.mqttConfig.password.c_str())) if (mqttClient.connect(mqttId.c_str(), config.mqttConfig.username.c_str(), config.mqttConfig.password.c_str()))
{ {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "connected"); logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "connected");
// Setup Home Assistant Discovery on successful connection
setupHomeAssistantDiscovery(); setupHomeAssistantDiscovery();
String topic = config.mqttConfig.topic; String topic = config.mqttConfig.topic;
@ -84,90 +70,120 @@ void Mqtt::mqttReconnect()
topic = config.mqttConfig.id; topic = config.mqttConfig.id;
} }
mqttClient.subscribe(topic.c_str()); mqttClient.subscribe(topic.c_str());
String message = "Subscribe to: " + topic;
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", message.c_str());
} }
else else
{ {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "MQTT", "Not connected to mqtt, try again in 5 s."); logger.log(logging::LoggerLevel::LOGGER_LEVEL_WARN, "MQTT", "Not connected to mqtt, try again in 5 s.");
// Wait 5 seconds before retrying
delay(5000); delay(5000);
} }
retryCount++;
} }
// Check if we're connected after the loop
if (!mqttClient.connected()) { if (!mqttClient.connected()) {
// Connection failed after maximum retries
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to connect to MQTT broker after 10 retries"); logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to connect to MQTT broker after 10 retries");
} }
} }
void Mqtt::setupHomeAssistantDiscovery() void Mqtt::setupHomeAssistantDiscovery()
{ {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Setting up Home Assistant MQTT Discovery"); logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Setting up Home Assistant MQTT Discovery using char buffers");
// Temperature sensor discovery // Skapa fasta teckenbuffertar för ämnen (topics) och JSON-data
char topicBuffer[128];
char jsonBuffer[512]; // 512 bytes räcker gott och väl för denna JSON
// Vi låter alla sensorer lyssna på den gemensamma JSON-strängen på /state
// Bygg upp strängarna med snabba snprintf istället för dynamiska String-objekt
char sharedStateTopic[64];
char availTopic[64];
snprintf(sharedStateTopic, sizeof(sharedStateTopic), "%s/state", baseTopic.c_str());
snprintf(availTopic, sizeof(availTopic), "%s/availability", baseTopic.c_str());
// 1. Temperature sensor discovery
{ {
JsonDocument doc; JsonDocument doc; // I v7 använder man bara JsonDocument, inte DynamicJsonDocument
doc["name"] = "Water Temperature"; doc["name"] = "Water Temperature";
doc["unique_id"] = "water_temp_temperature";
// Bygg unikt ID i en temporär buffert
char uniqueId[64];
snprintf(uniqueId, sizeof(uniqueId), "%s_temperature", deviceId.c_str());
doc["unique_id"] = uniqueId;
doc["unit_of_measurement"] = "°C"; doc["unit_of_measurement"] = "°C";
doc["device_class"] = "temperature"; doc["device_class"] = "temperature";
doc["state_topic"] = baseTopic + "/temperature"; doc["state_class"] = "measurement";
doc["availability_topic"] = baseTopic + "/availability"; doc["state_topic"] = sharedStateTopic;
doc["value_template"] = "{{ value_json.temperature }}";
doc["availability_topic"] = availTopic;
// I ArduinoJson v7 skapar man nästlade objekt direkt så här:
JsonObject device = doc["device"].to<JsonObject>(); JsonObject device = doc["device"].to<JsonObject>();
device["identifiers"][0] = deviceId; device["identifiers"][0] = deviceId;
device["name"] = "Water Temperature Sensor"; device["name"] = "Water Temperature Sensor";
device["model"] = "Water Temp v1"; device["model"] = "Water Temp v1";
device["manufacturer"] = "DIY"; device["manufacturer"] = "DIY";
String payload; // Serialisera direkt till vår fasta char-buffert
serializeJson(doc, payload); serializeJson(doc, jsonBuffer, sizeof(jsonBuffer));
String topic = baseTopic + "/temperature/config";
mqttPublish(topic.c_str(), payload.c_str()); // Bygg config-topic och publicera
snprintf(topicBuffer, sizeof(topicBuffer), "homeassistant/sensor/%s/temperature/config", deviceId.c_str());
mqttPublishRetained(topicBuffer, jsonBuffer);
} }
// RSSI sensor discovery // 2. RSSI sensor discovery
{ {
JsonDocument doc; JsonDocument doc;
doc["name"] = "LoRa RSSI"; doc["name"] = "LoRa RSSI";
doc["unique_id"] = "water_temp_rssi";
char uniqueId[64];
snprintf(uniqueId, sizeof(uniqueId), "%s_rssi", deviceId.c_str());
doc["unique_id"] = uniqueId;
doc["unit_of_measurement"] = "dBm"; doc["unit_of_measurement"] = "dBm";
doc["device_class"] = "signal_strength"; doc["device_class"] = "signal_strength";
doc["state_topic"] = baseTopic + "/rssi"; doc["state_class"] = "measurement";
doc["availability_topic"] = baseTopic + "/availability"; doc["state_topic"] = sharedStateTopic;
doc["value_template"] = "{{ value_json.rssi }}";
doc["availability_topic"] = availTopic;
JsonObject device = doc["device"].to<JsonObject>(); JsonObject device = doc["device"].to<JsonObject>();
device["identifiers"][0] = deviceId; device["identifiers"][0] = deviceId;
device["name"] = "Water Temperature Sensor";
String payload; serializeJson(doc, jsonBuffer, sizeof(jsonBuffer));
serializeJson(doc, payload);
String topic = baseTopic + "/rssi/config"; snprintf(topicBuffer, sizeof(topicBuffer), "homeassistant/sensor/%s/rssi/config", deviceId.c_str());
mqttPublish(topic.c_str(), payload.c_str()); mqttPublishRetained(topicBuffer, jsonBuffer);
} }
// Battery level discovery // 3. Battery level discovery
{ {
JsonDocument doc; JsonDocument doc;
doc["name"] = "Sender Battery"; doc["name"] = "Sender Battery";
doc["unique_id"] = "water_temp_battery";
char uniqueId[64];
snprintf(uniqueId, sizeof(uniqueId), "%s_battery", deviceId.c_str());
doc["unique_id"] = uniqueId;
doc["unit_of_measurement"] = "%"; doc["unit_of_measurement"] = "%";
doc["device_class"] = "battery"; doc["device_class"] = "battery";
doc["state_topic"] = baseTopic + "/battery"; doc["state_class"] = "measurement";
doc["availability_topic"] = baseTopic + "/availability"; doc["state_topic"] = sharedStateTopic;
doc["value_template"] = "{{ value_json.battery }}";
doc["availability_topic"] = availTopic;
JsonObject device = doc["device"].to<JsonObject>(); JsonObject device = doc["device"].to<JsonObject>();
device["identifiers"][0] = deviceId; device["identifiers"][0] = deviceId;
device["name"] = "Water Temperature Sensor";
String payload; serializeJson(doc, jsonBuffer, sizeof(jsonBuffer));
serializeJson(doc, payload);
String topic = baseTopic + "/battery/config"; snprintf(topicBuffer, sizeof(topicBuffer), "homeassistant/sensor/%s/battery/config", deviceId.c_str());
mqttPublish(topic.c_str(), payload.c_str()); mqttPublishRetained(topicBuffer, jsonBuffer);
} }
// Set availability to online // Sätt enheten till "online" med retain
String availTopic = baseTopic + "/availability"; mqttPublishRetained(availTopic, "online");
mqttPublish(availTopic.c_str(), "online");
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Home Assistant Discovery setup complete"); logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Home Assistant Discovery setup complete");
} }
@ -197,7 +213,7 @@ void Mqtt::publishSensorData(float temp, int rssi, int battery)
String payload = String(battery); String payload = String(battery);
mqttPublish(topic.c_str(), payload.c_str()); mqttPublish(topic.c_str(), payload.c_str());
// Also publish combined JSON // Kombinerad JSON-statustopic
{ {
JsonDocument doc; JsonDocument doc;
doc["temperature"] = serialized(String(temp, 2)); doc["temperature"] = serialized(String(temp, 2));
@ -213,17 +229,11 @@ void Mqtt::publishSensorData(float temp, int rssi, int battery)
} }
void Mqtt::callback(char *topic, uint8_t *payload, unsigned int length) void Mqtt::callback(char *topic, uint8_t *payload, unsigned int length)
// Not implemented yet. Just for debug
{ {
// Create spot in memory for message
char message[length + 1]; char message[length + 1];
// Write payload to String
strncpy(message, (char *)payload, length); strncpy(message, (char *)payload, length);
// Nullify last character to eliminate garbage at end
message[length] = '\0'; message[length] = '\0';
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Received: %s From: %s", message, topic);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Received: %s", message);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "From: %s", topic);
} }
void Mqtt::mqttPublish(const char* topic, const char* payload){ void Mqtt::mqttPublish(const char* topic, const char* payload){
@ -234,3 +244,12 @@ void Mqtt::mqttPublish(const char* topic, const char* payload){
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to publish message. MQTT client not connected."); logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to publish message. MQTT client not connected.");
} }
} }
void Mqtt::mqttPublishRetained(const char* topic, const char* payload) {
if (mqttClient.connected()) {
mqttClient.publish(topic, payload, true);
logger.log(logging::LoggerLevel::LOGGER_LEVEL_INFO, "MQTT", "Published RETAINED message to %s", topic);
} else {
logger.log(logging::LoggerLevel::LOGGER_LEVEL_ERROR, "MQTT", "Failed to publish retained message. Not connected.");
}
}

View File

@ -4,6 +4,7 @@
#include "wifi.hpp" #include "wifi.hpp"
#include "config.hpp" #include "config.hpp"
#include <WiFi.h> #include <WiFi.h>
#define MQTT_MAX_PACKET_SIZE 1024
#include <PubSubClient.h> #include <PubSubClient.h>
@ -13,6 +14,7 @@ class Mqtt {
void mqttSetup(); void mqttSetup();
void mqttRun(); void mqttRun();
void mqttPublish(const char* topic, const char* payload); void mqttPublish(const char* topic, const char* payload);
void mqttPublishRetained(const char* topic, const char* payload);
void publishTemperature(float temp); void publishTemperature(float temp);
void publishRSSI(int rssi); void publishRSSI(int rssi);
void publishSensorData(float temp, int rssi, int battery); void publishSensorData(float temp, int rssi, int battery);