Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better JSON and Mqtt cleanup #178

Merged
merged 11 commits into from
Feb 21, 2024
Merged
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: 102 additions & 128 deletions Software/src/devboard/mqtt/mqtt.cpp
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider adding an MQTT transmit INFO event. It's not handled consistently here at the moment. One case is handled with a serial print, two ignore the return value and one assigns the return value to a variable but does nothing with it.

Cabooman marked this conversation as resolved.
Show resolved Hide resolved
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,168 +27,140 @@ 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;

// If the cell voltage number isn't initialized...
static JsonDocument doc;
static const char* hostname = WiFi.getHostname();
static String state_topic = String("battery-emulator_") + String(hostname) + "/spec_data";
if (nof_cellvoltages == 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 < nof_cellvoltages; 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 (nof_cellvoltages == 0u) {
return;
}
}

size_t msg_length = snprintf(mqtt_msg, sizeof(mqtt_msg), "{\n\"cell_voltages\":[");
for (size_t i = 0; i < nof_cellvoltages; ++i) {
msg_length += snprintf(mqtt_msg + msg_length, sizeof(mqtt_msg) - msg_length, "%s%.3f", (i == 0) ? "" : ", ",
((float)cellvoltages[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 < nof_cellvoltages; ++i) {
cell_voltages.add(((float)cellvoltages[i]) / 1000.0);
}

// 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");
serializeJson(doc, mqtt_msg, sizeof(mqtt_msg));

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)SOC) / 100.0, ((float)StateOfHealth) / 100.0, ((float)((int16_t)temperature_min)) / 10.0,
((float)((int16_t)temperature_max)) / 10.0, ((float)((int16_t)stat_batt_power)),
((float)((int16_t)battery_current)) / 10.0, ((float)cell_max_voltage) / 1000,
((float)cell_min_voltage) / 1000, battery_voltage / 10.0);
bool result = client.publish(state_topic, mqtt_msg, true);
doc["SOC"] = ((float)SOC) / 100.0;
doc["state_of_health"] = ((float)StateOfHealth) / 100.0;
doc["temperature_min"] = ((float)((int16_t)temperature_min)) / 10.0;
doc["temperature_max"] = ((float)((int16_t)temperature_max)) / 10.0;
doc["stat_batt_power"] = ((float)((int16_t)stat_batt_power));
doc["battery_current"] = ((float)((int16_t)battery_current)) / 10.0;
doc["cell_max_voltage"] = ((float)cell_max_voltage) / 1000.0;
doc["cell_min_voltage"] = ((float)cell_min_voltage) / 1000.0;
doc["battery_voltage"] = ((float)battery_voltage) / 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 @@ -206,9 +179,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 @@ -238,20 +212,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;
}
3 changes: 2 additions & 1 deletion Software/src/devboard/mqtt/mqtt.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extern uint16_t cellvoltages[120]; //mV 0-4350 per cell
extern uint8_t nof_cellvoltages; // Total number of cell voltages, set by each battery.
extern uint16_t battery_voltage; //V+1, 0-500.0 (0-5000)
extern uint16_t battery_current; //A+1, Goes thru convert2unsignedint16 function (5.0A = 50, -5.0A = 65485)
extern uint16_t stat_batt_power;

extern const char* mqtt_user;
extern const char* mqtt_password;
Expand All @@ -59,6 +60,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
Loading