├── node_red_juntek_rs485.json ├── README.md └── vat1300_rf24_data_logger.ino /node_red_juntek_rs485.json: -------------------------------------------------------------------------------- 1 | [{"id":"e6fd8163.23991","type":"function","z":"af172f10.bae62","name":"Parse","func":"const BATT_AMP_HOURS = 400.0 //total amp-hours of battery bank\n\n//look for header before grabbing data\n//the third number is the frequency of the unit A=1, B=2, etc..\n//the forth number is the address\nvar header = [170, 28, 1, 27]; //Freq=A=1, Address = 27\nvar body = msg.payload;\nfor (i = 0; i < header.length; i++) {\n if (header[i] != body[i]) {\n return null;\n }\n}\n\n// Voltage 4 * 256 + 5 as 16-bit unsigned\nvar voltage = body[4] << 8 | body[5];\nvoltage /= 100.0;\n\n// Current 6 * 256 + 7 as 16-bit signed\nvar current = body[6] << 8 | body[7];\nif (current & 0x8000) {\n current = -((current ^ 0xFFFF) + 1);\n}\ncurrent /= 10.0;\n\n// Wattage 8..12 as 32-bit unsigned\nvar wattage = body[8] << 24 | \n body[9] << 16 |\n body[10] << 8 |\n body[11];\nwattage /= 1000.0;\n\n// Amp Hours 12..16 as 32-bit unsigned\nvar amp_hrs = body[12] << 24 | \n body[13] << 16 |\n body[14] << 8 |\n body[15];\namp_hrs /= 1000.0;\n\n// Watt Hours 16..20 as 32-bit unsigned\nvar watt_hrs = body[16] << 24 | \n body[17] << 16 |\n body[18] << 8 |\n body[19];\nwatt_hrs /= 1000.0;\n\n// Seconds 20..24 as 32-bit unsigned\nvar time_s = body[20] << 24 | \n body[21] << 16 |\n body[22] << 8 |\n body[23];\n\n// Status flag\nvar status_on = !(body[24]);\n\n// Temperature 25 as 8-bit signed\nvar temp = body[25];\nif (temp & 0x80) {\n body = -((body ^ 0xFF) + 1);\n}\n\n// Batter state-of-charge\nvar batt_soc = (BATT_AMP_HOURS - amp_hrs) /\n BATT_AMP_HOURS * 100.0;\n\nmsg.payload = { voltage: voltage,\n current: current,\n wattage: wattage,\n amp_hrs: amp_hrs,\n watt_hrs: watt_hrs,\n time_s: time_s,\n status_on: status_on,\n temp: temp,\n batt_soc: batt_soc };\nreturn msg;","outputs":1,"noerr":0,"x":330,"y":360,"wires":[["6ea3229b.2e2f9c"]]}] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vat1300 2 | Arduino nRF24L01+ data logger for Juntek VAT-1300 wireless battery monitor 3 | 4 | Tested on Arduino ESP8266 (LOLIN(WEMOS) D1 R2 & mini) 5 | 6 | 7 | //Change the following lines to match your setup: 8 | 9 | const char* ssid = "change this to your Wifi access point SSID"; 10 | 11 | const char* password = "change this to your Wifi access point password"; 12 | 13 | 14 | //Change the following to match the address and frequency as set on the Juntek controller 15 | 16 | const int address = 17; 17 | 18 | const char frequency = 'A'; 19 | 20 | 21 | Watch the following YouTube videos for more information 22 | 23 | Part 1: https://www.youtube.com/watch?v=DNORE1HBbRo 24 | 25 | Part 2: https://www.youtube.com/watch?v=bQKVNZRaFuQ&t=1s 26 | 27 | Part 3: https://www.youtube.com/watch?v=S0-O9cTS9Uw 28 | 29 | Part 4: https://youtu.be/nuUWXwzuHhk 30 | 31 | 32 | Part 4: Storing and displaying the data: Adding a Juntek VAT-1300 Wireless DC Ammeter, Watt-hour Meter, Coulomb counter to your network using an Arduino ESP8266 D1 Mini and a nRF24L01+ radio module. 33 | 34 | Part 4 shows how to log data over Wifi via TCP and then store and display the data on a Raspberry Pi using the Influxdb (database), Node-red (to grab and store the data), and Grafana (to host web access to graphically display the data). 35 | 36 | Arduino ESP8266 D1 Mini, Source code is located at: https://github.com/rjflatley/vat1300 37 | 38 | 39 | Basic setup steps on the Raspberry Pi (Raspbian Buster): 40 | 41 | $sudo apt update 42 | 43 | $sudo apt full-upgrade 44 | 45 | $curl -sL https://repos.influxdata.com/influxdb... | sudo apt-key add - 46 | 47 | $echo "deb https://repos.influxdata.com/debian buster stable" | sudo tee /etc/apt/sources.list.d/influxdb.list 48 | 49 | $sudo apt update 50 | 51 | $sudo apt-get install influxdb 52 | 53 | $sudo systemctl enable --now influxdb 54 | 55 | $influx 56 | 57 | -CREATE USER root WITH PASSWORD 'root' WITH ALL PRIVILEGES 58 | 59 | -CREATE DATABASE "battsoc" 60 | 61 | -exit 62 | 63 | $bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered) 64 | 65 | $node-red-start 66 | 67 | ctrl-c 68 | 69 | $sudo systemctl enable nodered.service 70 | 71 | From remote computer use browser to connect to Node-RED at the ip_address_of_the_rpi:1880 72 | 73 | Import the following. Change the IP address of TCP input to match the address of your raspberry pi 74 | 75 | [{"id":"3484cf5b.784ca","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"59bc08ea.500ef8","type":"tcp in","z":"3484cf5b.784ca","name":"","server":"client","host":"192.168.1.32","port":"23","datamode":"stream","datatype":"utf8","newline":"","topic":"","base64":false,"x":140,"y":260,"wires":[["fac80c1d.a2f52","863d0475.cfb1a8"]]},{"id":"fac80c1d.a2f52","type":"debug","z":"3484cf5b.784ca","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":420,"y":280,"wires":[]},{"id":"863d0475.cfb1a8","type":"http request","z":"3484cf5b.784ca","name":"","method":"POST","ret":"txt","paytoqs":false,"url":"http://localhost:8086/write?db=battsoc","tls":"","persist":false,"proxy":"","authType":"basic","x":640,"y":580,"wires":[["e06f6cba.0c32"]]},{"id":"e06f6cba.0c32","type":"debug","z":"3484cf5b.784ca","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":890,"y":580,"wires":[]}] 76 | 77 | Make sure you click "Deploy" 78 | 79 | Return to SSH of the Raspberry Pi 80 | 81 | $influx 82 | 83 | -use battsoc 84 | 85 | -show measurements 86 | 87 | -show field keys 88 | 89 | -select * from SOC 90 | 91 | -exit 92 | 93 | $wget https://dl.grafana.com/oss/release/gr... 94 | 95 | $sudo dpkg -i grafana_6.7.2_armhf.deb 96 | 97 | $sudo /bin/systemctl daemon-reload 98 | 99 | $sudo /bin/systemctl enable grafana-server 100 | 101 | $sudo /bin/systemctl start grafana-server 102 | 103 | $systemctl status grafana-server 104 | 105 | From remote computer use browser to connect to Grafana at ip_address_of_rpi:3000 106 | 107 | Log in user:admin password:admin 108 | 109 | set up database and dashboard 110 | 111 | See Youtube video for setup example 112 | 113 | Don't forget to safe the dashboard 114 | -------------------------------------------------------------------------------- /vat1300_rf24_data_logger.ino: -------------------------------------------------------------------------------- 1 | /* Juntek VAT-1300 data logger 2 | tested on Arduino ESP8266 (LOLIN(WEMOS) D1 R2 & mini) 3 | */ 4 | #include 5 | #include 6 | #include "RF24.h" 7 | #include "printf.h" 8 | 9 | 10 | #if defined(ARDUINO_AVR_UNO) 11 | // Uno pin assignments 12 | const int irqPin = 2; 13 | const int cePin = 7; 14 | const int csnPin = 8; 15 | const int mosiPin = 11; 16 | const int misoPin = 12; 17 | const int sckPin = 13; 18 | //#elif defined(ARDUINO_SAM_DUE) 19 | // // Due assignments 20 | // // SPI pins csn, mosi, miso, sck are wired directly to SPI header 21 | // #define RF24_DUE 22 | // const int irqPin = 6; 23 | // const int cePin = 7; 24 | // const int csnPin = 10; 25 | #elif defined(ARDUINO_ARCH_ESP32) 26 | const int irqPin = 4; 27 | const int cePin = 17; 28 | const int csnPin = 5; 29 | const int mosiPin = 23; 30 | const int misoPin = 19; 31 | const int sckPin = 18; 32 | #elif defined(ARDUINO_ARCH_ESP8266) 33 | const int irqPin = D3; 34 | const int cePin = D2; 35 | const int csnPin = D8; 36 | const int mosiPin = D6; 37 | const int misoPin = D7; 38 | const int sckPin = D5; 39 | #else 40 | #error Unsupported board selection. 41 | #endif 42 | 43 | //how many clients should be able to telnet to this ESP8266 44 | #define MAX_SRV_CLIENTS 2 45 | //Change the following lines to match your setup: 46 | const char* ssid = "change this to your Wifi access point SSID"; 47 | const char* password = "change this to your Wifi access point password"; 48 | 49 | WiFiServer server(23); 50 | WiFiClient serverClients[MAX_SRV_CLIENTS]; 51 | 52 | unsigned long previousMillis = 0; 53 | const long interval = 60000; //output every 60 seconds 54 | 55 | //Change the following to match the address and frequency as set on the Juntek controller 56 | const int address = 17; 57 | const char frequency = 'A'; 58 | 59 | float battAmpHours = -2.0; 60 | 61 | 62 | /****************** User Config ***************************/ 63 | /*** Set this radio as radio number 0 or 1 ***/ 64 | bool radioNumber = 0; 65 | 66 | int lpCnt = 0; 67 | 68 | /* Hardware configuration: Set up nRF24L01 radio on SPI bus */ 69 | RF24 radio(cePin, csnPin); 70 | /**********************************************************/ 71 | 72 | uint64_t pipes[2] = { 0x8967452300LL, 0x8967452300LL }; 73 | 74 | 75 | 76 | /**********************************************************/ 77 | //Function to configure the radio 78 | void configureRadio() { 79 | 80 | radio.begin(); 81 | 82 | radio.setAutoAck(false); 83 | radio.setDataRate(RF24_2MBPS); 84 | for (int i = 0; i < 6; i++) 85 | radio.closeReadingPipe(i); 86 | // Open a writing and reading pipe 87 | pipes[0] |= (uint64_t)address; 88 | pipes[1] |= (uint64_t)address; 89 | radio.openWritingPipe(pipes[0]); 90 | radio.openReadingPipe(0, pipes[1]); 91 | radio.setAutoAck(0, true); 92 | radio.setChannel((frequency - 'A') * 4); 93 | radio.setPALevel(RF24_PA_MAX); 94 | 95 | radio.printDetails(); 96 | 97 | } 98 | 99 | /**********************************************************/ 100 | 101 | void setup() { 102 | Serial.begin(115200); 103 | pinMode(irqPin, INPUT); 104 | printf_begin(); 105 | 106 | configureRadio(); 107 | 108 | WiFi.mode(WIFI_STA); 109 | WiFi.begin(ssid, password); 110 | uint8_t i = 0; 111 | while (WiFi.status() != WL_CONNECTED && i++ < 20) delay(500); 112 | if (i == 21) { 113 | Serial.print("Could not connect to"); Serial.println(ssid); 114 | while (1) delay(500); 115 | } 116 | server.begin(); 117 | server.setNoDelay(true); 118 | 119 | 120 | 121 | } 122 | 123 | uint32_t configTimer = millis(); 124 | 125 | void loop() { 126 | 127 | 128 | if (radio.failureDetected) { 129 | radio.failureDetected = false; 130 | delay(250); 131 | Serial.println("Radio failure detected, restarting radio"); 132 | configureRadio(); 133 | } 134 | //Every 5 seconds, verify the configuration of the radio. This can be done using any 135 | //setting that is different from the radio defaults. 136 | if (millis() - configTimer > 5000) { 137 | configTimer = millis(); 138 | if (radio.getDataRate() != RF24_2MBPS) { 139 | radio.failureDetected = true; 140 | Serial.print("Radio configuration error detected"); 141 | } 142 | } 143 | 144 | 145 | radio.stopListening(); // First, stop listening so we can talk. 146 | 147 | //***** wifi stuff ******* 148 | 149 | unsigned long currentMillis = millis(); 150 | uint8_t i; 151 | //check if there are any new clients 152 | if (server.hasClient()) { 153 | for (i = 0; i < MAX_SRV_CLIENTS; i++) { 154 | //find free/disconnected spot 155 | if (!serverClients[i] || !serverClients[i].connected()) { 156 | if (serverClients[i]) serverClients[i].stop(); 157 | serverClients[i] = server.available(); 158 | continue; 159 | } 160 | } 161 | //no free/disconnected spot so reject 162 | WiFiClient serverClient = server.available(); 163 | serverClient.stop(); 164 | } 165 | 166 | //***** end wifi stuff ****** 167 | 168 | 169 | 170 | 171 | 172 | //Serial.println(F("Now sending")); 173 | byte aTX_PAYLOAD[] = { 0xAA, 0x04, 0x02, address, address + 0xB0, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00 }; 174 | 175 | 176 | aTX_PAYLOAD[4] = address + 0xB0; 177 | /* first time in loop set TX_Payload to fetch setup data */ 178 | if (lpCnt == 0) 179 | { 180 | aTX_PAYLOAD[2] = 2; 181 | aTX_PAYLOAD[4] = address + 0xB0; 182 | } 183 | /* second time onward in loop set TX_Payload to fetch run data */ 184 | if (lpCnt == 1) 185 | { 186 | aTX_PAYLOAD[2] = 1; 187 | aTX_PAYLOAD[4] = address + 0xAF; 188 | } 189 | /* special recovery of base unit for power failures */ 190 | if (lpCnt < 0) 191 | { 192 | aTX_PAYLOAD[2] = 0xFF; 193 | aTX_PAYLOAD[4] = address + 0xAD; 194 | lpCnt = 0; 195 | } 196 | 197 | 198 | unsigned long start_time = micros(); // Take the time, and send it. This will block until complete 199 | if (!radio.write(&aTX_PAYLOAD, sizeof(aTX_PAYLOAD))) { 200 | Serial.println(F("failed")); 201 | } 202 | 203 | radio.startListening(); // Now, continue listening 204 | 205 | unsigned long started_waiting_at = micros(); // Set up a timeout period, get the current microseconds 206 | boolean timeout = false; // Set up a variable to indicate if a response was received or not 207 | byte aRX_PAYLOAD[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00, 0X00 }; 208 | 209 | while ( ! radio.available() ) { // While nothing is received 210 | if (micros() - started_waiting_at > 200000 ) { // If waited longer than 200ms, indicate timeout and exit while loop 211 | timeout = true; 212 | break; 213 | } 214 | } 215 | 216 | if ( timeout ) { // Describe the results 217 | Serial.println(F("Failed, response timed out.")); 218 | lpCnt = -2; 219 | } else { 220 | unsigned long got_time; // Grab the response, compare, and send to debugging spew 221 | 222 | //Failure Handling: 223 | uint32_t failTimer = millis(); 224 | while (radio.available()) { //If available always returns true, there is a problem 225 | if (millis() - failTimer > 250) { 226 | radio.failureDetected = true; 227 | Serial.println("Radio available failure detected"); 228 | break; 229 | } 230 | radio.read( &aRX_PAYLOAD, sizeof(aRX_PAYLOAD) ); // Get the payload 231 | } 232 | unsigned long end_time = micros(); 233 | 234 | 235 | 236 | /* Print headers and setup data */ 237 | if (lpCnt == 0) 238 | { 239 | if (battAmpHours < -1.0) 240 | battAmpHours = (uint16_t)(aRX_PAYLOAD[6] << 8 | aRX_PAYLOAD[7]) / 10.0; 241 | float ovp = (int16_t)(aRX_PAYLOAD[8] << 8 | aRX_PAYLOAD[9]) / 100.0; 242 | float lvp = (int16_t)(aRX_PAYLOAD[10] << 8 | aRX_PAYLOAD[11]) / 100.0; 243 | float ocp = (int16_t)(aRX_PAYLOAD[12] << 8 | aRX_PAYLOAD[13]); 244 | float ncp = (int16_t)(aRX_PAYLOAD[14] << 8 | aRX_PAYLOAD[15]); 245 | int rDelay = aRX_PAYLOAD[17]; 246 | int calVoltage = aRX_PAYLOAD[18] << 8 | aRX_PAYLOAD[19]; 247 | int calCurrentIeq0 = aRX_PAYLOAD[22] << 8 | aRX_PAYLOAD[23]; 248 | int calCurrentIgt0 = aRX_PAYLOAD[24] << 8 | aRX_PAYLOAD[25]; 249 | 250 | Serial.println("Battary\t\t\t\t\t\tcal\tcal Currrent"); 251 | Serial.println("AmpHour\tOVP\tLVP\tOCP\tNCP\tDelay\tVoltage\tI=0\tI>0"); 252 | Serial.print(battAmpHours); 253 | Serial.print("\t"); 254 | Serial.print(ovp); 255 | Serial.print("\t"); 256 | Serial.print(lvp); 257 | Serial.print("\t"); 258 | Serial.print(ocp, 0); 259 | Serial.print("\t"); 260 | Serial.print(ncp, 0); 261 | Serial.print("\t"); 262 | Serial.print(rDelay); 263 | Serial.print("\t"); 264 | Serial.print(calVoltage); 265 | Serial.print("\t"); 266 | Serial.print(calCurrentIeq0); 267 | Serial.print("\t"); 268 | Serial.println(calCurrentIgt0); 269 | //Serial.println("\t\t\tAmp\tWatt\tTime\t\t"); 270 | //Serial.println("Voltage\tCurrent\tWatts\thours\thours\tseconds\tTemp\tOutput"); 271 | } 272 | else 273 | /* print run data */ 274 | { 275 | 276 | //***** more wifi stuff ***** 277 | 278 | //do every 1000ms 279 | if (currentMillis - previousMillis >= interval) { 280 | previousMillis = currentMillis; 281 | 282 | //***** end more wifi stuff 283 | 284 | float voltage = (int16_t)(aRX_PAYLOAD[4] << 8 | aRX_PAYLOAD[5]) / 100.0; 285 | float amperage = (int16_t)(aRX_PAYLOAD[6] << 8 | aRX_PAYLOAD[7]) / 10.0; 286 | float wattage = (int32_t)(aRX_PAYLOAD[8] << 24 | aRX_PAYLOAD[9] << 16 | aRX_PAYLOAD[10] << 8 | aRX_PAYLOAD[11]) / 1000.0; 287 | float ampHours = (int32_t)(aRX_PAYLOAD[12] << 24 | aRX_PAYLOAD[13] << 16 | aRX_PAYLOAD[14] << 8 | aRX_PAYLOAD[15]) / 1000.0; 288 | float wattHours = (int32_t)(aRX_PAYLOAD[16] << 24 | aRX_PAYLOAD[17] << 16 | aRX_PAYLOAD[18] << 8 | aRX_PAYLOAD[19]) / 1000.0; 289 | int32_t seconds = (int32_t)(aRX_PAYLOAD[20] << 24 | aRX_PAYLOAD[21] << 16 | aRX_PAYLOAD[22] << 8 | aRX_PAYLOAD[23]); 290 | int8_t temperature = (int8_t)aRX_PAYLOAD[25]; 291 | float batterySOC = (battAmpHours - ampHours) / battAmpHours * 100.0; 292 | 293 | bool outputOn; 294 | if (aRX_PAYLOAD[24] == 0) 295 | outputOn = true; 296 | else 297 | outputOn = false; 298 | 299 | 300 | int isensorValue = temperature; 301 | String sSensorValue = String("SOC BatteryVoltage=") + String(voltage, 2) 302 | + String(",LoadCurrent=") + String(amperage, 1) 303 | + String(",LoadPower=") + String(wattage, 3) 304 | + String(",BatteryTemperature=") + String(temperature) 305 | + String(",EquipmentStatus=") + String(outputOn) 306 | + String(",AmpHours=") + String(ampHours, 3) 307 | + String(",WattHours=") + String(wattHours, 3) 308 | + String(",OnTime=") + String(seconds) 309 | + String(",BatterySOC=") + String(batterySOC, 5) + "\n\r"; 310 | /* String sSensorValue = String("BatteryVoltage value=") + String(voltage, 2) + "\n\r" 311 | + String("LoadCurrent value=") + String(amperage, 1) + "\n\r" 312 | + String("LoadPower value=") + String(wattage, 3) + "\n\r" 313 | + String("BatteryTemperature value=") + String(temperature) + "\n\r" 314 | + String("EquipmentStatus vaue=") + String(outputOn) + "\n\r" 315 | + String("AmpHours value=") + String(ampHours, 3) + "\n\r" 316 | + String("WattHours value=") + String(wattHours, 3) + "\n\r" 317 | + String("OnTime value=") + String(seconds) + "\n\r" 318 | + String("BatterySOC value=") + String(batterySOC) + "\n\r";*/ 319 | Serial.print(sSensorValue); 320 | size_t len = sSensorValue.length(); 321 | uint8_t sbuf[len]; 322 | sSensorValue.getBytes(sbuf, len); 323 | for (i = 0; i < MAX_SRV_CLIENTS; i++) { 324 | if (serverClients[i] && serverClients[i].connected()) { 325 | serverClients[i].write(sbuf, len); 326 | delay(1); 327 | } 328 | } 329 | 330 | 331 | 332 | } 333 | } 334 | lpCnt = 1; 335 | } 336 | 337 | // } 338 | // delay(1000); 339 | 340 | } // Loop 341 | --------------------------------------------------------------------------------