JuncTek Battery Monitor MQTT Controller

 I recently installed some 48 volt Lithium Iron Phosphate battery packs at the Ranch to replace the 9 year old Lead-Acid batteries that had started to fail.


 

eg4-lifepower4-battery-48v-100ah 

These are rack-mount battery packs that are intended for powering computer servers.  They are reported to work very well in an off-grid power system. 

The Problem

I had a problem to solve before the upgrade, in that the old 48 volt inverter I had been using in our solar power system required a programming interface to change its operating parameters to match the new battery specifications.  Unfortunately, the manufacturer wants over $400 for the required interface to make these changes.  I am unwilling to pay this extortion money for a simple device that should cost less than $100. The communications protocol and interface is proprietary, so it would take a considerable effort to hack the interface.

There are two sets of parameters in dealing with the inverter/battery connections:

1. The charging parameters for when the inverter is plugged into an external source, such as grid power, or a generator.

2. The low voltage cutoff parameter that determines when the inverter will turn itself off as the battery voltage falls below a certain threshold.

For problem #1, I simply decided that I will not charge the batteries through the inverter from now on.  This should seldom come up, but if the need arises, I will charge through a separate charging system from the generator.

For problem #2, the current settings tell the inverter to disconnect at a substantially lower voltage than the Lithium Iron technology prefers.  This would result in the batteries shutting themselves off, before the inverter can gracefully turn itself off.

The Solution

I decided to build a system to shut down the inverter at an appropriate level for the LifePo4 batteries.  Since I would be using a microcontroller to accomplish this, I could add live battery monitoring straight to our main residence 200 miles to the south, via the internet.

I chose to use an ESP-8266 NodeMCU microcontroller for this project.


 

The ESP-8266 is compatible with the Arduino programming environment, and includes libraries to accomplish all of the functions that I require, including Wi-Fi, MQTT protocol, a serial interface, and an output to drive a relay.

I bought a battery monitor to provide a display inside the LittleHouse.  This battery monitor talks to the display via an RS-485 serial interface which is well documented by the manufacturer.  It also provides a second RS-485 interface that can be used to talk to external devices (like a homebrew project such as this).


 

It includes a 400 Amp shunt to read current in and out of the battery packs.  


 

I used an RS-485 to TTL converter to interface the battery monitor with my controller.  

 


Then I used a relay module to switch on/off the inverter through its external switch connections.


 

By monitoring the communications between the display unit and the battery monitor, I had all of the information necessary to make decisions on inverter turn on/off and reporting battery status back to our main house via the internet.

To test the battery monitor, I first connected it to a low voltage power supply with a load resistor between the shunt and the power supply.  I was able to test the communications interface using an RS-485 to USB adapter to my computer first.


 

  


 

Soon, I was seeing communications screaming to my putty session on the computer at a 115kb rate.


 

Once that was accomplished, I analyzed the communications and figured out how to extract the in/out current flow from the batteries, battery voltage, state of charge, and other parameters.

Now that it was working on the bench, I needed to simulate its operation using real-world voltages.

I came up with this testing setup:



12 volts from the bench power supply feeds a boost converter that kicks the voltage up to the 48 volt region (more like 55 volts in my case).

The 48 volts is then fed to the battery monitor.  So now it is measuring higher voltage levels consistent with what I am using at the Ranch.


 

But the rest of my circuitry needs 12 volts to run.  I had a small wall-wart running off the 115 volts of the inverter output at the ranch, but I needed the 12 volts to be available even when the inverter was turned off, since the purpose of this project is to turn the inverter on and off appropriately.  So I turned to a 48 volt to 12 volt buck converter to accomplish this.


 

It only needs to provide an amp or so of current; enough to drive my ESP-8266 circuit and two 12 volt computer fans with their associated controller (already installed and in use for several years).

So that provided me with a 12 volt source from the batteries, without needing to relay on the inverter being on.

The 12 volts supplies my controller board, which has a 7805 voltage regulator to step the voltage down for the ESP-8266, while still providing 12 volts to the auxiliary cards (RS-485 interface and relay module).

The controller board is built on a surplus Arduino-Mega prototyping board that I had left over from a previous project.  Any prototyping board would have worked, I imagine.


 

 


My first iteration of this circuit used different relay output pins than what I ended up with.  Some of the pins, it turns out, go through a little toggle on power up or reboot.  This would have caused the inverter to be turned off and on in quick secession. I can't imagine that would be good for it.  So after finding this out, I switched to some pins that are not affected by power ups or reboots.

I went through quite a learning cycle in getting the data back to our house in Mesa.  First I tried the Arduino iot-mqtt platform, but found it to be too inconsistent in forwarding the data to our house from the ranch. 

Next, I tried the HiveMQ MQTT platform.  It provides a free server, but required a lot of setup and was quite complicated, with its security level that I didn't need.

Finally, I installed a Mosquitto MQTT broker on one of my Raspberry Pi machines in Mesa.  This machine is used for other purposes, as well, so is on and running 24x7 anyway.  After opening up a port from he internet to this machine for the mqtt data, and setting appropriate security measures in place, I soon had reliable data reporting from the ranch every 30 seconds.  I tested the system at a 2 second data rate, and it performed just fine at that rate, but I figured that a 30 second rate was fast enough for this purpose.

The last part of the project was to build an easily accessible display for the data that I could view in Mesa.  I used my existing Node-Red installation to accomplish this with just a little work.  It connects to the mqtt broker and extracts the data from there on my local lan.  Then displays it using a few gauges and button fields.

 

Schematic:


ESP8266 MQTT Client Code:

 Note: This code uses ascii text formatting to display debug info on an ascii terminal supporting color.  The serial debugging output is best viewed using a terminal program that supports this, such as Putty.

#define RELAY D1
#define RX485 D5
#define TX485 D6
#define WAIT 60000
#define REASONLOW 36.0
#define REASONHI 70.0
#define LVCUTOFF 47.5
#define INV_ENABLE 54.0
#define NORMCURR 30.0
#define WARNCURR 10.0
#define AHLOW 11.5
#define AHWARN 11.8
#define MINLOW 700
#define MINWARN 710
#define PRINTWAIT 2
#define RECONRETRY 60000
#define MQTTPUBTIME 30000
#define MSG_BUFFER_SIZE  (200)
/********************************************************************************/
#include<SoftwareSerial.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
/********************************************************************************/
unsigned long lastRead = 0;
unsigned long lowVoltTime = 0;
unsigned long normVoltTime = 0;
unsigned long lvTimer = 0;
unsigned long nvTimer = 0;
unsigned long printTime = 0;
unsigned long prevPrint = 0;
unsigned long lastMsg = 0;
unsigned long lastread = 0;
unsigned long lastReconnectAttempt = 0;

bool debugPrint = true;
bool lowVolts = false;
bool normVolts = false;
bool dumpInverter = false;
bool validData = false;

int minRemain = 0;
int cStatus = 0;
int value = 0;
int chrgDir = 0;
int tempC = 0.0;

float voltage = 0.0;
float current = 0.0;
float ahRemain = 0.0;
float ahUsed = 0.0;

char msg[MSG_BUFFER_SIZE];

const char *txtStatus[]= {"uninitialized       ","Low Voltage         ","Inverter Disabled   ",
                    "LV Timeout Hold     ","Nominal Voltage     ","Normal Voltage      ",
                    "Inverter Enabled    ","Inverter Enable Hold"};

String voltTxtColor = "";
String currTxtColor = "";
String ahTxtColor = "";
String minTxtColor = "";
String diTxtColor = "";
String nvTxtColor = "";
String lvTxtColor = "";
String clearScreen = "\033[0H\033[0J";
String regWhite = "\033[0;37m";
String bldWhite = "\033[1;37m";
String regGreen = "\033[0;32m";
String bldGreen = "\033[1;32m";
String bldYellow = "\033[1;33m";
String bldRed = "\033[1;31m";
String bldHiRed = "\033[1;91m";

// Update these with values suitable for your hardware/network.
const char* ssid = "your_local_network";
const char* password = "your_network_password";
const char* mqtt_server = "yourServerName.org"; // ip address on Domain Name
const char* mqtt_user = "your_mqtt_user_name";
const char* mqtt_passwd = "your_mqtt_user_password";
/********************************************************************************/
// SoftwareSerial RX connects to RS-485 Module TXD Pin
// SoftwareSerial TX connects to RS-485 Module RXD Pin
SoftwareSerial SUART(D6,D5); //SRX(RX-485 Module TXD); STX(RS-485 Module RXD)
WiFiClient espClient;
PubSubClient client(espClient);
/********************************************************************************/
void setup_wifi() {
  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  randomSeed(micros());
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}
/********************************************************************************/
void callback(char* topic, byte* payload, unsigned int length) {
  // handle message arrived
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  // Switch on the LED if an 1 was received as first character
  if ((char)payload[0] == '1') {
    digitalWrite(BUILTIN_LED, LOW);   // Turn the LED on (Note that LOW is the voltage level
    // but actually the LED is on; this is because
    // it is active low on the ESP-01)
  } else {
    digitalWrite(BUILTIN_LED, HIGH);  // Turn the LED off by making the voltage HIGH
  }
}
/********************************************************************************/
boolean reconnect() {
  Serial.print("Attempting MQTT connection...");
  // Create a random client ID
  String clientId = "yourClientId-";
  clientId += String(random(0xffff), HEX);
  // Attempt to connect
  if (client.connect(clientId.c_str(), mqtt_user, mqtt_passwd)) {
    // Once connected, publish an announcement...
    Serial.println("connected");
    // Once connected, publish an announcement...
    client.publish("your_mqtt_publish_topic", "hello world");
    // ... and resubscribe
    client.subscribe("your_mqtt_subscribe_topic");
  }
  return client.connected();
}
/********************************************************************************/
void setup()
  {
  pinMode(BUILTIN_LED, OUTPUT);     // Initialize the BUILTIN_LED pin as an output
  pinMode(RELAY, OUTPUT);
  Serial.begin(115200);
  SUART.begin(115200); //enable SUART Port
  setup_wifi();
  //Serial.print("client.setServer...");
  client.setServer(mqtt_server, your_mqtt_server_port);
  //Serial.print("client.Callback...");
  client.setCallback(callback);
  delay(1500);
  lastReconnectAttempt = 0;
  //Serial.print("Exiting setup()...");
  /* Test Inverter Sutdown/Restore
  Serial.println("Test: Shutting OFF Inverter in 10 seconds...");
  delay(10000);
  digitalWrite(RELAY,HIGH);
  Serial.println("Test: Restoring Inverter in 60 seconds...");
  delay(60000);
  digitalWrite(RELAY,LOW);
  Serial.println("Exiting setup...");
  */
}
/**************************************************************************************/
void parseData(String rcvData) {
  char *token;
  // put data in the matrix
  int functionNum = -1;
  int crc = 0;
  int calcCrc = 0;
  // For reference only - no need to actually populate a variable in this version
  /*
  String matrix[2][19];
  matrix[0][0] = "Function:Measurments";
  matrix[0][1] = "Address:";
  matrix[0][2] = "Checksum:";
  matrix[0][3] = "Voltage:";
  matrix[0][4] = "Current:";
  matrix[0][5] = "Remaining AH:";
  matrix[0][6] = "Used AH:";
  matrix[0][7] = "Watt Hours:";
  matrix[0][8] = "Running Time:";
  matrix[0][9] = "Ambient Temperature:";
  matrix[0][10] = "Function Pending:";
  matrix[0][11] = "Output Status:";
  matrix[0][12] = "Discharge/Charge:";
  matrix[0][13] = "Battery Minutes Remaining:";
  matrix[0][14] = "Battery Internal Resistance:";
  matrix[0][15] = "Not Used:";
  matrix[0][16] = "Not Used:";
  matrix[0][17] = "Not Used:";
  matrix[0][18] = "Not Used:";
  matrix[1][0] = "Function Settings:";
  matrix[1][1] = "Address:";
  matrix[1][2] = "Checksum:";
  matrix[1][3] = "Overvoltage Setpoint:";
  matrix[1][4] = "UnderVoltage Setpoint:";
  matrix[1][5] = "Overcurrent Discharge Setpoint:";
  matrix[1][6] = "Overcurrent Charging Setpoint:";
  matrix[1][7] = "Overpower Setpoint:";
  matrix[1][8] = "OverTemperature Setpoint:";
  matrix[1][9] = "Protection Recovery Time:";
  matrix[1][10] = "Delay Time:";
  matrix[1][11] = "Battery Capacity Preset:";
  matrix[1][12] = "Voltage Calibration Factor:";
  matrix[1][13] = "Current Calibration Factor:";
  matrix[1][14] = "Temperature Calibration Factor:";
  matrix[1][15] = "Reserved:";
  matrix[1][16] = "Relay Type:";
  matrix[1][17] = "Current Ratio:";
  matrix[1][18] = "Not Used:";
  */
  int tokenCount = 0;
  const char *delimiter = ",=";

  nvTxtColor = bldGreen;
  diTxtColor = bldGreen;
  // convert the string to a character array for use with strtok function
  int str_len = rcvData.length() + 1;
  if(rcvData.indexOf("r50=") > 0) {
    functionNum = 0;
  }
  else if(rcvData.indexOf("r51=") > 0) {
    functionNum = 1;
  }
  char char_array[str_len];
  rcvData.toCharArray(char_array, str_len);
  token = strtok(char_array, delimiter);
  crc = 0;
  calcCrc = 0;
  while (token != NULL) {
    if(tokenCount==2) {
    crc = atoi(token);
    }
    if(tokenCount > 2) {
      // Add all the following values together for crc calculation
      calcCrc=calcCrc + atoi(token);
    }
    
    if(functionNum == 0) {
      // Convert some values to floats
      if(tokenCount==3){
        voltage = atof(token) * 0.01;
        lastRead = millis();
      }
      if(tokenCount==4){
        current = atof(token) * 0.01;
        if(current < NORMCURR){
          currTxtColor = bldGreen;
        }
        else if(current < WARNCURR){
          currTxtColor = bldYellow;
        }
        else {
          currTxtColor = bldRed;
        }
      }
      else if(tokenCount==5){
        ahRemain = atof(token) * 0.001;
        if(ahRemain < AHLOW) {
          ahTxtColor = bldRed;
        }
        else if (ahRemain < AHWARN) {
          ahTxtColor = bldYellow;
        }
        else {
          ahTxtColor = bldGreen;
        }
      }
      else if(tokenCount==6){
        ahUsed = atof(token) * 0.001;
      }
      else if(tokenCount==9){
        tempC = atoi(token)-100;
      }
      else if(tokenCount==12){
        chrgDir = atoi(token);
      }
      else if(tokenCount==13){
        minRemain = atoi(token);
        if (minRemain < MINLOW) {
          minTxtColor = bldRed;
        }
        else if (minRemain < MINWARN) {
          minTxtColor = bldYellow;
        }
        else {
          minTxtColor = bldGreen;
        }
      }
    }
  token=strtok(NULL, delimiter);
  tokenCount++;
  }
  // Finish crc Calc
  if((crc > 0)&&(functionNum == 0)){
    calcCrc = (calcCrc % 255)+1;
    if(calcCrc != crc){
      validData = false;
    }
    else {
      validData = true;
    }
  }
}
/**************************************************************************************/
void loop()
  {
  //Serial.println("void loop()...");
  unsigned long now = millis();
  lastread = millis();
  if (SUART.available() > 0) {
    // read the incoming string:
    String incomingString = SUART.readStringUntil('\n');
    //Serial.println(incomingString);
    validData = false;
    parseData(incomingString);
    if((validData)&&(voltage > REASONLOW)&&(voltage < REASONHI)){
      // Check for UnderVoltage
      if ((millis() - lastRead) < 5000){
        if(voltage <= LVCUTOFF) {
          normVolts = false;
          nvTxtColor = bldRed;
          normVoltTime = millis();
          if (!lowVolts) {
            // Batteries just entered low voltage state
            lowVolts = true;
            lvTxtColor = bldRed;
            lowVoltTime = millis();
            cStatus = 1;
            voltTxtColor = bldRed;
          }
          else if (millis() - lowVoltTime > WAIT) {
            // Batteries low for more than a minute
            // Turn off the inverter to save batteries
            dumpInverter = true;
            diTxtColor = bldRed;
            lvTxtColor = bldHiRed;
            digitalWrite(RELAY,HIGH);
            cStatus = 2;
            voltTxtColor = bldHiRed;
          }
          else {
            //Waiting for Low Volt Timeout
            cStatus = 3;
            voltTxtColor = bldYellow;
            lvTxtColor = bldYellow;
          }
        }
        else if((voltage < INV_ENABLE) && (voltage > LVCUTOFF)){
          // Between disconnect and recovery voltages
          lowVolts = false;
          lowVoltTime = millis();
          normVolts = false;
          nvTxtColor = bldYellow;
          lvTxtColor = bldYellow;
          normVoltTime = millis();
          cStatus = 4;
          voltTxtColor = regGreen;
        }
        else if(voltage > INV_ENABLE){
          lowVolts = false;
          lowVoltTime = millis();
          if (!normVolts) {
            // Batteries just entered normal voltage state
            normVolts = true;
            nvTxtColor = regGreen;
            lvTxtColor = bldGreen;
            normVoltTime = millis();
            cStatus = 5;
            voltTxtColor = regGreen;
          }
          else if (millis() - normVoltTime > WAIT) {
            // Batteries normal for more than a minute
            // Turn on the inverter
            dumpInverter = false;
            diTxtColor = bldGreen;
            nvTxtColor = bldGreen;
            digitalWrite(RELAY, LOW);
            cStatus = 6;
            voltTxtColor = bldGreen;
          }
          else {
            //Waiting for Return Timeout
            cStatus = 7;
            voltTxtColor = regGreen;
            lvTxtColor = regGreen;
          }
        }
        lvTimer = ((millis()-lowVoltTime)*.001);
        nvTimer = ((millis()-normVoltTime)*.001);
        if(!chrgDir){
          // Current is discharging
          current = current * -1;
        }
        if(debugPrint){
          printTime = ((millis() - prevPrint)*.001);
          if (printTime > PRINTWAIT){
            Serial.print(clearScreen);  // clear the ansi screen
            Serial.printf("%sBank Voltage:%s%6.2f%s Bank Current   :%s%6.2f%s      Bank Status      :%s%s\n\r",
              regWhite,voltTxtColor,voltage,regWhite,currTxtColor,current,regWhite,voltTxtColor,
              txtStatus[cStatus]);
            Serial.printf("%sAH Used     :%s%6.2f%s AH Remaining   :%s%6.2f%s      Minutes Remaining:%s%d\n\r",
              regWhite,bldWhite,ahUsed,regWhite,ahTxtColor,ahRemain,regWhite,minTxtColor,minRemain);
            Serial.printf("%sDump Status :%s%d%s      Low Volt Status:%s%d%s           Low Volt Timer   :%s%ld\n\r",
              regWhite,diTxtColor,dumpInverter,regWhite,lvTxtColor,lowVolts,regWhite,bldWhite,lvTimer);
            Serial.printf("%sNorm Status :%s%d%s      Norm Volt Timer:%s%ld%s       Charge Direction:%s%d\n\r",
              regWhite,nvTxtColor,normVolts,regWhite,bldWhite,nvTimer,regWhite,bldWhite,chrgDir);
            Serial.printf("%sTemperature :%s%d\n\r",
              regWhite,bldWhite,tempC);
            prevPrint = millis();
          }
        }
      }
      if (!client.connected()) {
        now = millis();
        if (now - lastReconnectAttempt > RECONRETRY) {
          lastReconnectAttempt = now;
          // Attempt to reconnect
          if (reconnect()) {
            lastReconnectAttempt = 0;
          }
          else {
            Serial.println(client.state());
          }
        }
      }
      else {
        // Client connected
        client.loop();
        now = millis();
        if (now - lastMsg > MQTTPUBTIME) {
          lastMsg = now;
          ++value;
          snprintf (msg, MSG_BUFFER_SIZE, "{\"voltage\":%6.2f,\"current\":%6.2f,\"ahUsed\":%6.2f,\"ahRemain\":%6.2f,\"dumpInverter\":%d,\"lowVolts\":%d,\"normVolts\":%d,\"chrgDir\":%d,\"tempC\":%d}",
            voltage,current,ahUsed,ahRemain,dumpInverter,lowVolts,normVolts,chrgDir,tempC);
          client.publish("AzRanch/BatteryBank/Status", msg);
        }
      }
    }
  }
}

 

Comments

Popular posts from this blog

Building the W8NX Short Trap Dipole

Yaesu G-450XL Rotator Repair