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

Feature/mqtt on main #152

Merged
merged 25 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Software/Software.ino
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ void loop() {
#ifdef WEBSERVER
// Over-the-air updates by ElegantOTA
ElegantOTA.loop();
#ifdef MQTT
mqtt_loop();
#endif
#endif

// Input
Expand Down
6 changes: 6 additions & 0 deletions Software/USER_SETTINGS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ volatile uint16_t MAXCHARGEAMP =
volatile uint16_t MAXDISCHARGEAMP =
300; //30.0A , BYD CAN specific setting, Max discharge speed in Amp (Some inverters needs to be artificially limited)

// MQTT
// For more detailed settings, see mqtt.h
#ifdef MQTT
const char* mqtt_user = "REDACTED";
const char* mqtt_password = "REDACTED";
#endif // USE_MQTT
/* Charger settings */
volatile float CHARGER_SET_HV = 384; // Reasonably appropriate 4.0v per cell charging of a 96s pack
volatile float CHARGER_MAX_HV = 420; // Max permissible output (VDC) of charger
Expand Down
11 changes: 6 additions & 5 deletions Software/USER_SETTINGS.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,24 @@

/* Select inverter communication protocol. See Wiki for which to use with your inverter: https://github.com/dalathegreat/BYD-Battery-Emulator-For-Gen24/wiki */
//#define BYD_CAN //Enable this line to emulate a "BYD Battery-Box Premium HVS" over CAN Bus
//#define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU
//#define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU
//#define LUNA2000_MODBUS //Enable this line to emulate a "Luna2000 battery" over Modbus RTU
//#define PYLON_CAN //Enable this line to emulate a "Pylontech battery" over CAN bus
//#define SMA_CAN //Enable this line to emulate a "BYD Battery-Box H 8.9kWh, 7 mod" over CAN bus
//#define SOFAR_CAN //Enable this line to emulate a "Sofar Energy Storage Inverter High Voltage BMS General Protocol (Extended Frame)" over CAN bus
//#define SOLAX_CAN //Enable this line to emulate a "SolaX Triple Power LFP" over CAN bus

/* Other options */
#define DEBUG_VIA_USB //Enable this line to have the USB port output serial diagnostic data while program runs
//define INTERLOCK_REQUIRED //Nissan LEAF specific setting, if enabled requires both high voltage conenctors to be seated before starting
//#define DEBUG_VIA_USB //Enable this line to have the USB port output serial diagnostic data while program runs
//#define INTERLOCK_REQUIRED //Nissan LEAF specific setting, if enabled requires both high voltage conenctors to be seated before starting
//#define CONTACTOR_CONTROL //Enable this line to have pins 25,32,33 handle automatic precharge/contactor+/contactor- closing sequence
//#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM logic for contactors, which lower power consumption and heat generation
//#define DUAL_CAN //Enable this line to activate an isolated secondary CAN Bus using add-on MCP2515 controller (Needed for FoxESS inverters)
//#define SERIAL_LINK_RECEIVER //Enable this line to receive battery data over RS485 pins from another Lilygo (This LilyGo interfaces with inverter)
//#define SERIAL_LINK_TRANSMITTER //Enable this line to send battery data over RS485 pins to another Lilygo (This LilyGo interfaces with battery)
#define WEBSERVER //Enable this line to enable WiFi, and to run the webserver. See USER_SETTINGS.cpp for the Wifi settings.
#define LOAD_SAVED_SETTINGS_ON_BOOT //Enable this line to read settings stored via the webserver on boot
//#define WEBSERVER //Enable this line to enable WiFi, and to run the webserver. See USER_SETTINGS.cpp for the Wifi settings.
//#define MQTT // Enable this line to enable MQTT
//#define LOAD_SAVED_SETTINGS_ON_BOOT //Enable this line to read settings stored via the webserver on boot

/* Select charger used (Optional) */
//#define CHEVYVOLT_CHARGER //Enable this line to control a Chevrolet Volt charger connected to battery - for example, when generator charging or using an inverter without a charging function.
Expand Down
81 changes: 81 additions & 0 deletions Software/src/battery/NISSAN-LEAF-BATTERY.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#include "NISSAN-LEAF-BATTERY.h"
#ifdef MQTT
#include "../devboard/mqtt/mqtt.h"
#endif
#include "../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h"
#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"

Expand All @@ -17,6 +20,10 @@ static uint8_t mprun10r = 0; //counter 0-20 for 0x1F2 message
static uint8_t mprun10 = 0; //counter 0-3
static uint8_t mprun100 = 0; //counter 0-3

#ifdef MQTT
bool mqtt_first_transmission = true;
#endif

CAN_frame_t LEAF_1F2 = {.FIR = {.B =
{
.DLC = 8,
Expand Down Expand Up @@ -155,6 +162,10 @@ static uint16_t temp_raw_min = 0;
static int16_t temp_polled_max = 0;
static int16_t temp_polled_min = 0;

#ifdef MQTT
void publish_data(void);
#endif

void print_with_units(char* header, int value, char* units) {
Serial.print(header);
Serial.print(value);
Expand Down Expand Up @@ -402,6 +413,9 @@ void update_values_leaf_battery() { /* This function maps all the values fetched
}

#endif
#ifdef MQTT
publish_data();
#endif
}

void receive_can_leaf_battery(CAN_frame_t rx_frame) {
Expand Down Expand Up @@ -917,3 +931,70 @@ uint16_t Temp_fromRAW_to_F(uint16_t temperature) { //This function feels horrib
}
return static_cast<uint16_t>(1094 + (309 - temperature) * 2.5714285714285715);
}

#ifdef MQTT
void publish_data(void) {

// 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"
// TODO: make the discovery topic configurable centrally... but this is fine
String topic = "homeassistant/sensor/battery-emulator/cell_voltage";
for (int i = 0; i < 96; 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\": \"4.4.0-mqtt\","
"\"url\": \"https://github.com/dalathegreat/Battery-Emulator\""
"},"
"\"state_class\": \"measurement\","
"\"name\": \"Battery Cell Voltage %d\","
"\"state_topic\": \"battery/spec_data\","
"\"unique_id\": \"battery-emulator_battery_voltage_cell%d\","
"\"unit_of_measurement\": \"V\","
"\"value_template\": \"{{ value_json.cell_voltages[%d] }}\""
"}",
i + 1, 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());
}
}

// 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
size_t msg_length = snprintf(mqtt_msg, sizeof(mqtt_msg), "{\n\"cell_voltages\":[");
for (size_t i = 0; i < 97; ++i) {
msg_length +=
snprintf(mqtt_msg + msg_length, sizeof(mqtt_msg) - msg_length, "%s%d", (i == 0) ? "" : ", ", cell_voltages[i]);
Cabooman marked this conversation as resolved.
Show resolved Hide resolved
}
snprintf(mqtt_msg + msg_length, sizeof(mqtt_msg) - msg_length, "]\n}\n");

// Publish and print error if not OK
if (mqtt_publish_retain("battery/spec_data") == false) {
Serial.println("Nissan MQTT msg could not be sent");
}
}
#endif
102 changes: 102 additions & 0 deletions Software/src/devboard/mqtt/mqtt.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#include "mqtt.h"
#include <Arduino.h>
#include <WiFi.h>
#include <freertos/FreeRTOS.h>
#include "../../../USER_SETTINGS.h"
#include "../../lib/knolleary-pubsubclient/PubSubClient.h"
#include "../utils/timer.h"

const char* mqtt_subscriptions[] = MQTT_SUBSCRIPTIONS;
const size_t mqtt_nof_subscriptions = sizeof(mqtt_subscriptions) / sizeof(mqtt_subscriptions[0]);

WiFiClient espClient;
PubSubClient client(espClient);
char mqtt_msg[MSG_BUFFER_SIZE];
int value = 0;
static unsigned long previousMillisUpdateVal;
MyTimer publish_global_timer(5000);

/** Publish global values and call callbacks for specific modules */
static void publish_values(void) {

snprintf(mqtt_msg, sizeof(mqtt_msg),
"{\n"
" \"SOC\": %.3f,\n"
" \"StateOfHealth\": %.3f,\n"
" \"temperature_min\": %.3f,\n"
" \"temperature_max\": %.3f,\n"
" \"cell_max_voltage\": %d,\n"
" \"cell_min_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, cell_max_voltage, cell_min_voltage);
bool result = client.publish("battery/info", mqtt_msg, true);
Serial.println(mqtt_msg); // Uncomment to print the payload on serial
}

/* This is called whenever a subscribed topic changes (hopefully) */
static void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (unsigned int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
}

/* If we lose the connection, get it back and re-sub */
static void reconnect() {
// attempt one reconnection
Serial.print("Attempting MQTT connection... ");
// Create a random client ID
String clientId = "LilyGoClient-";
clientId += String(random(0xffff), HEX);
// Attempt to connect
if (client.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
Serial.println("connected");

for (int i = 0; i < mqtt_nof_subscriptions; i++) {
client.subscribe(mqtt_subscriptions[i]);
Serial.print("Subscribed to: ");
Serial.println(mqtt_subscriptions[i]);
}
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
}
}

void init_mqtt(void) {
client.setServer(MQTT_SERVER, MQTT_PORT);
client.setCallback(callback);
Serial.println("MQTT initialized");

previousMillisUpdateVal = millis();
reconnect();
}

void mqtt_loop(void) {
if (client.connected()) {
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.
}
} 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.
}
}
}

bool mqtt_publish_retain(const char* topic) {
if (client.connected() == true) {
return client.publish(topic, mqtt_msg, true);
}
return false;
}
62 changes: 62 additions & 0 deletions Software/src/devboard/mqtt/mqtt.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* MQTT add-on for the battery emulator
*
* Usage:
*
* Subscription - Add topics to MQTT_SUBSCRIPTIONS in USER_SETTINGS.h and handle the messages in mqtt.cpp:callback()
*
* Publishing - See example in mqtt.cpp:publish_values() for constructing the payload
*
* Home assistant - See below for an example, and the official documentation is quite good (https://www.home-assistant.io/integrations/sensor.mqtt/)
* in configuration.yaml:
* mqtt: !include mqtt.yaml
*
* in mqtt.yaml:
* sensor:
* - name: "Cell max"
* state_topic: "battery/info"
* unit_of_measurement: "mV"
* value_template: "{{ value_json.cell_max_voltage | int }}"
* - name: "Cell min"
* state_topic: "battery/info"
* unit_of_measurement: "mV"
* value_template: "{{ value_json.cell_min_voltage | int }}"
* - name: "Temperature max"
* state_topic: "battery/info"
* unit_of_measurement: "C"
* value_template: "{{ value_json.temperature_max | float }}"
* - name: "Temperature min"
* state_topic: "battery/info"
* unit_of_measurement: "C"
* value_template: "{{ value_json.temperature_min | float }}"
*/

#ifndef __MQTT_H__
#define __MQTT_H__

#include <Arduino.h>
#include "../../../USER_SETTINGS.h"

#define MSG_BUFFER_SIZE (1024)
#define MQTT_SUBSCRIPTIONS \
{ "my/topic/abc", "my/other/topic" }
#define MQTT_SERVER "192.168.xxx.yyy"
Cabooman marked this conversation as resolved.
Show resolved Hide resolved
#define MQTT_PORT 1883

extern uint16_t SOC;
extern uint16_t StateOfHealth;
extern uint16_t temperature_min; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t temperature_max; //C+1, Goes thru convert2unsignedint16 function (15.0C = 150, -15.0C = 65385)
extern uint16_t cell_max_voltage; //mV, 0-4350
extern uint16_t cell_min_voltage; //mV, 0-4350

extern const char* mqtt_user;
extern const char* mqtt_password;

extern char mqtt_msg[MSG_BUFFER_SIZE];

void init_mqtt(void);
void mqtt_loop(void);
bool mqtt_publish_retain(const char* topic);

#endif
12 changes: 12 additions & 0 deletions Software/src/devboard/utils/timer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#include "timer.h"

MyTimer::MyTimer(unsigned long interval) : interval(interval), previousMillis(0) {}

bool MyTimer::elapsed() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
return true;
}
return false;
}
18 changes: 18 additions & 0 deletions Software/src/devboard/utils/timer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#ifndef __MYTIMER_H__
#define __MYTIMER_H__

#include <Arduino.h>

class MyTimer {
public:
/** interval in ms */
MyTimer(unsigned long interval);
/** Returns true and resets the timer if it has elapsed */
bool elapsed();

private:
unsigned long interval;
unsigned long previousMillis;
};

#endif // __MYTIMER_H__
5 changes: 5 additions & 0 deletions Software/src/devboard/webserver/webserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ void init_webserver() {

// Start server
server.begin();

#ifdef MQTT
// Init MQTT
init_mqtt();
#endif
}

void init_WiFi_AP() {
Expand Down
6 changes: 6 additions & 0 deletions Software/src/devboard/webserver/webserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
#include <WiFi.h>
#include "../../../USER_SETTINGS.h" // Needed for WiFi ssid and password
#include "../../lib/ayushsharma82-ElegantOTA/src/ElegantOTA.h"
#ifdef MQTT
#include "../../lib/knolleary-pubsubclient/PubSubClient.h"
#endif
#include "../../lib/me-no-dev-AsyncTCP/src/AsyncTCP.h"
#include "../../lib/me-no-dev-ESPAsyncWebServer/src/ESPAsyncWebServer.h"
#include "../../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h"
#include "../config.h" // Needed for LED defines
#ifdef MQTT
#include "../mqtt/mqtt.h"
#endif

extern uint16_t SOC; //SOC%, 0-100.00 (0-10000)
extern uint16_t StateOfHealth; //SOH%, 0-100.00 (0-10000)
Expand Down
Loading
Loading