Skip to content

Commit

Permalink
Merge pull request #178 from kharnt0x/mqtt-cleanup
Browse files Browse the repository at this point in the history
Better JSON and Mqtt cleanup
  • Loading branch information
dalathegreat authored Feb 21, 2024
2 parents b1d2654 + ff18c2f commit 9207db3
Show file tree
Hide file tree
Showing 5 changed files with 7,685 additions and 129 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
*build/

# Ignore .exe (unit tests)
*.exe
*.exe
**/.DS_Store
1 change: 1 addition & 0 deletions Software/Software.ino
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "src/devboard/utils/events.h"
#include "src/inverter/INVERTERS.h"
#include "src/lib/adafruit-Adafruit_NeoPixel/Adafruit_NeoPixel.h"
#include "src/lib/bblanchon-ArduinoJson/ArduinoJson.h"
#include "src/lib/eModbus-eModbus/Logging.h"
#include "src/lib/eModbus-eModbus/ModbusServerRTU.h"
#include "src/lib/eModbus-eModbus/scripts/mbServerFCs.h"
Expand Down
230 changes: 103 additions & 127 deletions Software/src/devboard/mqtt/mqtt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <freertos/FreeRTOS.h>
#include "../../../USER_SETTINGS.h"
#include "../../battery/BATTERIES.h"
#include "../../lib/bblanchon-ArduinoJson/ArduinoJson.h"
#include "../../lib/knolleary-pubsubclient/PubSubClient.h"
#include "../utils/timer.h"

Expand All @@ -26,169 +27,143 @@ static void publish_values(void) {
publish_cell_voltages();
}

static String generateCellVoltageAutoConfigTopic(int cell_number, const char* hostname) {
return String("homeassistant/sensor/battery-emulator_") + String(hostname) + "/cell_voltage" + String(cell_number) +
"/config";
}

static void publish_cell_voltages(void) {
static bool mqtt_first_transmission = true;
static JsonDocument doc;
static const char* hostname = WiFi.getHostname();
static String state_topic = String("battery-emulator_") + String(hostname) + "/spec_data";

// If the cell voltage number isn't initialized...
if (system_number_of_cells == 0u) {
return;
}
// At startup, re-post the discovery message for home assistant

if (mqtt_first_transmission == true) {
mqtt_first_transmission = false;

// Base topic for any cell voltage "sensor"
String topic = "homeassistant/sensor/battery-emulator/cell_voltage";

for (int i = 0; i < system_number_of_cells; i++) {
// Build JSON message with device configuration for each cell voltage
// Probably shouldn't be BatteryEmulator here, instead "LeafBattery"
// or similar but hey, it works.
// mqtt_msg is a global buffer, should be fine since we run too much
// in a single thread :)
snprintf(mqtt_msg, sizeof(mqtt_msg),
"{"
"\"device\": {"
"\"identifiers\": ["
"\"battery-emulator\""
"],"
"\"manufacturer\": \"DalaTech\","
"\"model\": \"BatteryEmulator\","
"\"name\": \"BatteryEmulator\""
"},"
"\"device_class\": \"voltage\","
"\"enabled_by_default\": true,"
"\"object_id\": \"sensor_battery_voltage_cell%d\","
"\"origin\": {"
"\"name\": \"BatteryEmulator\","
"\"sw\": \"%s-mqtt\","
"\"url\": \"https://github.com/dalathegreat/Battery-Emulator\""
"},"
"\"state_class\": \"measurement\","
"\"name\": \"Battery Cell Voltage %d\","
"\"state_topic\": \"battery-emulator/spec_data\","
"\"unique_id\": \"battery-emulator_battery_voltage_cell%d\","
"\"unit_of_measurement\": \"V\","
"\"value_template\": \"{{ value_json.cell_voltages[%d] }}\""
"}",
i + 1, version_number, i + 1, i + 1, i);
// End each discovery topic with cell number and '/config'
String cell_topic = topic + String(i + 1) + "/config";
mqtt_publish_retain(cell_topic.c_str());
int cellNumber = i + 1;
doc["name"] = "Battery Cell Voltage " + String(cellNumber);
doc["object_id"] = "battery_voltage_cell" + String(cellNumber);
doc["unique_id"] = "battery-emulator_" + String(hostname) + "_battery_voltage_cell" +
String(cellNumber); //"battery-emulator_" + String(hostname) + "_" +
doc["device_class"] = "voltage";
doc["state_class"] = "measurement";
doc["state_topic"] = state_topic;
doc["unit_of_measurement"] = "V";
doc["enabled_by_default"] = true;
doc["expire_after"] = 240;
doc["value_template"] = "{{ value_json.cell_voltages[" + String(i) + "] }}";
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = "BatteryEmulator_" + String(hostname);
doc["origin"]["name"] = "BatteryEmulator";
doc["origin"]["sw"] = String(version_number) + "-mqtt";
doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator";

serializeJson(doc, mqtt_msg, sizeof(mqtt_msg));
mqtt_publish(generateCellVoltageAutoConfigTopic(cellNumber, hostname).c_str(), mqtt_msg, true);
}
doc.clear(); // clear after sending autoconfig
} else {
// Every 5-ish seconds, build the JSON payload for the state topic. This requires
// some annoying formatting due to C++ not having nice Python-like string formatting.
// msg_length is a cumulative variable to track start position (param 1) and for
// modifying the maxiumum amount of characters to write (param 2). The third parameter
// is the string content

// If cell voltages haven't been populated...
if (system_number_of_cells == 0u) {
return;
}
}

size_t msg_length = snprintf(mqtt_msg, sizeof(mqtt_msg), "{\n\"cell_voltages\":[");
for (size_t i = 0; i < system_number_of_cells; ++i) {
msg_length += snprintf(mqtt_msg + msg_length, sizeof(mqtt_msg) - msg_length, "%s%.3f", (i == 0) ? "" : ", ",
((float)system_cellvoltages_mV[i]) / 1000);
}
snprintf(mqtt_msg + msg_length, sizeof(mqtt_msg) - msg_length, "]\n}\n");
JsonArray cell_voltages = doc["cell_voltages"].to<JsonArray>();
for (size_t i = 0; i < system_number_of_cells; ++i) {
cell_voltages.add(((float)system_cellvoltages_mV[i]) / 1000.0);
}

serializeJson(doc, mqtt_msg, sizeof(mqtt_msg));

// Publish and print error if not OK
if (mqtt_publish_retain("battery-emulator/spec_data") == false) {
Serial.println("Cell voltage MQTT msg could not be sent");
if (!mqtt_publish(state_topic.c_str(), mqtt_msg, false)) {
Serial.println("Cell voltage MQTT msg could not be sent");
}
doc.clear();
}
}

struct SensorConfig {
const char* object_id;
const char* topic;
const char* name;
const char* value_template;
const char* unit;
const char* device_class;
};

SensorConfig sensorConfigs[] = {
{"SOC", "homeassistant/sensor/battery-emulator/SOC/config", "Battery Emulator SOC", "{{ value_json.SOC }}", "%",
"battery"},
{"state_of_health", "homeassistant/sensor/battery-emulator/state_of_health/config",
"Battery Emulator State Of Health", "{{ value_json.state_of_health }}", "%", "battery"},
{"temperature_min", "homeassistant/sensor/battery-emulator/temperature_min/config",
"Battery Emulator Temperature Min", "{{ value_json.temperature_min }}", "°C", "temperature"},
{"temperature_max", "homeassistant/sensor/battery-emulator/temperature_max/config",
"Battery Emulator Temperature Max", "{{ value_json.temperature_max }}", "°C", "temperature"},
{"stat_batt_power", "homeassistant/sensor/battery-emulator/stat_batt_power/config",
"Battery Emulator Stat Batt Power", "{{ value_json.stat_batt_power }}", "W", "power"},
{"battery_current", "homeassistant/sensor/battery-emulator/battery_current/config",
"Battery Emulator Battery Current", "{{ value_json.battery_current }}", "A", "current"},
{"cell_max_voltage", "homeassistant/sensor/battery-emulator/cell_max_voltage/config",
"Battery Emulator Cell Max Voltage", "{{ value_json.cell_max_voltage }}", "V", "voltage"},
{"cell_min_voltage", "homeassistant/sensor/battery-emulator/cell_min_voltage/config",
"Battery Emulator Cell Min Voltage", "{{ value_json.cell_min_voltage }}", "V", "voltage"},
{"battery_voltage", "homeassistant/sensor/battery-emulator/battery_voltage/config",
"Battery Emulator Battery Voltage", "{{ value_json.battery_voltage }}", "V", "voltage"},
{"SOC", "Battery Emulator SOC", "{{ value_json.SOC }}", "%", "battery"},
{"state_of_health", "Battery Emulator State Of Health", "{{ value_json.state_of_health }}", "%", "battery"},
{"temperature_min", "Battery Emulator Temperature Min", "{{ value_json.temperature_min }}", "°C", "temperature"},
{"temperature_max", "Battery Emulator Temperature Max", "{{ value_json.temperature_max }}", "°C", "temperature"},
{"stat_batt_power", "Battery Emulator Stat Batt Power", "{{ value_json.stat_batt_power }}", "W", "power"},
{"battery_current", "Battery Emulator Battery Current", "{{ value_json.battery_current }}", "A", "current"},
{"cell_max_voltage", "Battery Emulator Cell Max Voltage", "{{ value_json.cell_max_voltage }}", "V", "voltage"},
{"cell_min_voltage", "Battery Emulator Cell Min Voltage", "{{ value_json.cell_min_voltage }}", "V", "voltage"},
{"battery_voltage", "Battery Emulator Battery Voltage", "{{ value_json.battery_voltage }}", "V", "voltage"},
};

static String generateCommonInfoAutoConfigTopic(const char* object_id, const char* hostname) {
return String("homeassistant/sensor/battery-emulator_") + String(hostname) + "/" + String(object_id) + "/config";
}

static void publish_common_info(void) {
static JsonDocument doc;
static bool mqtt_first_transmission = true;
static char* state_topic = "battery-emulator/info";
static const char* hostname = WiFi.getHostname();
static String state_topic = String("battery-emulator_") + String(hostname) + "/info";
if (mqtt_first_transmission == true) {
mqtt_first_transmission = false;
for (int i = 0; i < sizeof(sensorConfigs) / sizeof(sensorConfigs[0]); i++) {
SensorConfig& config = sensorConfigs[i];
snprintf(mqtt_msg, sizeof(mqtt_msg),
"{"
"\"name\": \"%s\","
"\"state_topic\": \"%s\","
"\"unique_id\": \"battery-emulator_%s\","
"\"object_id\": \"sensor_battery_%s\","
"\"device\": {"
"\"identifiers\": ["
"\"battery-emulator\""
"],"
"\"manufacturer\": \"DalaTech\","
"\"model\": \"BatteryEmulator\","
"\"name\": \"BatteryEmulator\""
"},"
"\"origin\": {"
"\"name\": \"BatteryEmulator\","
"\"sw\": \"%s-mqtt\","
"\"url\": \"https://github.com/dalathegreat/Battery-Emulator\""
"},"
"\"value_template\": \"%s\","
"\"unit_of_measurement\": \"%s\","
"\"device_class\": \"%s\","
"\"enabled_by_default\": true,"
"\"state_class\": \"measurement\""
"}",
config.name, state_topic, config.object_id, config.object_id, version_number, config.value_template,
config.unit, config.device_class);
mqtt_publish_retain(config.topic);
doc["name"] = config.name;
doc["state_topic"] = state_topic;
doc["unique_id"] = "battery-emulator_" + String(hostname) + "_" + String(config.object_id);
doc["object_id"] = String(hostname) + "_" + String(config.object_id);
doc["value_template"] = config.value_template;
doc["unit_of_measurement"] = config.unit;
doc["device_class"] = config.device_class;
doc["enabled_by_default"] = true;
doc["state_class"] = "measurement";
doc["expire_after"] = 240;
doc["device"]["identifiers"][0] = "battery-emulator";
doc["device"]["manufacturer"] = "DalaTech";
doc["device"]["model"] = "BatteryEmulator";
doc["device"]["name"] = "BatteryEmulator_" + String(hostname);
doc["origin"]["name"] = "BatteryEmulator";
doc["origin"]["sw"] = String(version_number) + "-mqtt";
doc["origin"]["url"] = "https://github.com/dalathegreat/Battery-Emulator";
serializeJson(doc, mqtt_msg);
mqtt_publish(generateCommonInfoAutoConfigTopic(config.object_id, hostname).c_str(), mqtt_msg, true);
}
doc.clear();
} else {
snprintf(mqtt_msg, sizeof(mqtt_msg),
"{\n"
" \"SOC\": %.3f,\n"
" \"state_of_health\": %.3f,\n"
" \"temperature_min\": %.3f,\n"
" \"temperature_max\": %.3f,\n"
" \"stat_batt_power\": %.3f,\n"
" \"battery_current\": %.3f,\n"
" \"cell_max_voltage\": %.3f,\n"
" \"cell_min_voltage\": %.3f,\n"
" \"battery_voltage\": %d\n"
"}\n",
((float)system_scaled_SOC_pptt) / 100.0, ((float)system_SOH_pptt) / 100.0,
((float)((int16_t)system_temperature_min_dC)) / 10.0, ((float)((int16_t)system_temperature_max_dC)) / 10.0,
((float)((int16_t)system_active_power_W)), ((float)((int16_t)system_battery_current_dA)) / 10.0,
((float)system_cell_max_voltage_mV) / 1000, ((float)system_cell_min_voltage_mV) / 1000,
system_battery_voltage_dV / 10.0);
bool result = client.publish(state_topic, mqtt_msg, true);
doc["SOC"] = ((float)system_scaled_SOC_pptt) / 100.0;
doc["SOC_real"] = ((float)system_real_SOC_pptt) / 100.0;
doc["state_of_health"] = ((float)system_SOH_pptt) / 100.0;
doc["temperature_min"] = ((float)((int16_t)system_temperature_min_dC)) / 10.0;
doc["temperature_max"] = ((float)((int16_t)system_temperature_max_dC)) / 10.0;
doc["stat_batt_power"] = ((float)((int16_t)system_active_power_W));
doc["battery_current"] = ((float)((int16_t)system_battery_current_dA)) / 10.0;
doc["cell_max_voltage"] = ((float)system_cell_max_voltage_mV) / 1000.0;
doc["cell_min_voltage"] = ((float)system_cell_min_voltage_mV) / 1000.0;
doc["battery_voltage"] = ((float)system_battery_voltage_dV) / 10.0;

serializeJson(doc, mqtt_msg);
if (!mqtt_publish(state_topic.c_str(), mqtt_msg, false)) {
Serial.println("Common info MQTT msg could not be sent");
}
doc.clear();
}

//Serial.println(mqtt_msg); // Uncomment to print the payload on serial
}

/* This is called whenever a subscribed topic changes (hopefully) */
Expand All @@ -207,9 +182,10 @@ static void reconnect() {
// attempt one reconnection
Serial.print("Attempting MQTT connection... ");
const char* hostname = WiFi.getHostname();
String clientId = "LilyGoClient-" + String(hostname);
char clientId[64]; // Adjust the size as needed
snprintf(clientId, sizeof(clientId), "LilyGoClient-%s", hostname);
// Attempt to connect
if (client.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
if (client.connect(clientId, mqtt_user, mqtt_password)) {
Serial.println("connected");

for (int i = 0; i < mqtt_nof_subscriptions; i++) {
Expand Down Expand Up @@ -239,20 +215,20 @@ void mqtt_loop(void) {
client.loop();
if (publish_global_timer.elapsed() == true) // Every 5s
{
publish_values(); // Update values heading towards inverter. Prepare for sending on CAN, or write directly to Modbus.
publish_values();
}
} else {
if (millis() - previousMillisUpdateVal >= 5000) // Every 5s
{
previousMillisUpdateVal = millis();
reconnect(); // Update values heading towards inverter. Prepare for sending on CAN, or write directly to Modbus.
reconnect();
}
}
}

bool mqtt_publish_retain(const char* topic) {
bool mqtt_publish(const char* topic, const char* mqtt_msg, bool retain) {
if (client.connected() == true) {
return client.publish(topic, mqtt_msg, true);
return client.publish(topic, mqtt_msg, retain);
}
return false;
}
2 changes: 1 addition & 1 deletion Software/src/devboard/mqtt/mqtt.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ extern char mqtt_msg[MQTT_MSG_BUFFER_SIZE];

void init_mqtt(void);
void mqtt_loop(void);
bool mqtt_publish_retain(const char* topic);
bool mqtt_publish(const char* topic, const char* mqtt_msg, bool retain);

#endif
Loading

0 comments on commit 9207db3

Please sign in to comment.