Merged battery repo
133
battery/bms.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
const SerialPort = require('serialport');
|
||||||
|
|
||||||
|
module.exports = class BMS {
|
||||||
|
constructor(tty) {
|
||||||
|
this.modules = {};
|
||||||
|
this.port = new SerialPort(tty);
|
||||||
|
this.serialParser = this.port.pipe(new SerialPort.parsers.Readline());
|
||||||
|
this.serialParser.on('data', rx => this.postprocess(rx));
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.serialParser.removeAllListeners();
|
||||||
|
this.port.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ampHours: this.ampHours,
|
||||||
|
avgCellVoltage: this.avgCellVoltage,
|
||||||
|
capacity: this.capacity,
|
||||||
|
current: this.current,
|
||||||
|
lifetimeCharge: this.lifetimeCharge,
|
||||||
|
lifetimeDischarge: this.lifetimeDischarge,
|
||||||
|
maxCellVoltage: this.maxCellVoltage,
|
||||||
|
maxChargeCurrent: this.maxChargeCurrent,
|
||||||
|
maxDischargeCurrent: this.maxDischargeCurrent,
|
||||||
|
maxVoltage: this.maxVoltage,
|
||||||
|
minCellVoltage: this.minCellVoltage,
|
||||||
|
minVoltage: this.minVoltage,
|
||||||
|
modules: this.modules,
|
||||||
|
negativeContactor: this.negativeContactor,
|
||||||
|
positiveContactor: this.positiveContactor,
|
||||||
|
power: this.power,
|
||||||
|
soc: this.soc,
|
||||||
|
temperature: this.avgTemp,
|
||||||
|
uptime: this.uptime,
|
||||||
|
version: this.version,
|
||||||
|
voltage: this.voltage,
|
||||||
|
wattHours: this.wattHours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postprocess(rx) {
|
||||||
|
// BMS Software Version
|
||||||
|
let version = new RegExp(/Version (\d(?:\.\d+)?)/g).exec(rx);
|
||||||
|
if(!!version) this.version = Number(version[1]);
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
let uptime = new RegExp(/Runtime:\s*(.*?)\s*\*/g).exec(rx);
|
||||||
|
if(!!uptime) this.uptime = uptime[1];
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
let module = new RegExp(/Module (\d+): (-?\d+(?:\.\d+)?)V (-?\d+(?:\.\d+)?)\/(-?\d+(?:\.\d+)?)C/g).exec(rx);
|
||||||
|
if(!!module) {
|
||||||
|
let id = Number(module[1]);
|
||||||
|
let voltage = Number(module[2]);
|
||||||
|
let negTemp = Number(module[3]);
|
||||||
|
let posTemp = Number(module[4]);
|
||||||
|
|
||||||
|
// Module data
|
||||||
|
this.modules[id] = {
|
||||||
|
cells: {},
|
||||||
|
voltage: voltage,
|
||||||
|
negativeTemperature: negTemp,
|
||||||
|
positiveTemperature: posTemp,
|
||||||
|
temperature: (negTemp + posTemp) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cell data
|
||||||
|
let cell, cellRegEx = new RegExp(/Cell(\d+):(-?\d+(?:\.\d+)?)V/g);
|
||||||
|
while((cell = cellRegEx.exec(rx)) != null) {
|
||||||
|
this.modules[id].cells[Number(cell[1])] = Number(cell[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status
|
||||||
|
let status = new RegExp(/PACK STATUS:(.*?) Modules: (\d+) Voltage: (-?\d+(?:\.\d+)?)v Avg Cell: (-?\d+(?:\.\d+)?)v Avg Temp: (-?\d+(?:\.\d+)?)C SOC: (-?\d+(?:\.\d+)?)%/g).exec(rx);
|
||||||
|
if(!!status) {
|
||||||
|
this.status = status[1];
|
||||||
|
// this.moduleCount = Number(status[2]); // Use this.modules.length instead
|
||||||
|
this.voltage = Number(status[3]);
|
||||||
|
this.avgCellVoltage = Number(status[4]);
|
||||||
|
this.avgTemp = Number(status[5]);
|
||||||
|
this.soc = Number(status[6]) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power
|
||||||
|
let power = new RegExp(/CURRENT:\s+(-?\d+(?:\.\d+)?)A\s+POWER:\s+(-?\d+(?:\.\d+)?)\s+Watts\s+AMPHOURS:\s+(-?\d+(?:\.\d+)?)\s+Ah\s+WATTHOURS:\s+(-?\d+(?:\.\d+)?)\s+Wh/g).exec(rx);
|
||||||
|
if(!!power) {
|
||||||
|
this.current = Number(power[1]);
|
||||||
|
this.power = Number(power[2]);
|
||||||
|
this.ampHours = Number(power[3]);
|
||||||
|
this.wattHours = Number(power[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charge/Discharge
|
||||||
|
let minMax = new RegExp(/Max System Discharge Current:\s+(-?\d+(?:\.\d+)?)A\s+Max System Charge Current:\s+(-?\d+(?:\.\d+)?)A/g).exec(rx);
|
||||||
|
if(!!minMax) {
|
||||||
|
this.maxDischargeCurrent = Number(minMax[1]);
|
||||||
|
this.maxChargeCurrent = Number(minMax[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack Min/Max
|
||||||
|
let packMinMax = new RegExp(/Max Pack Voltage:\s+(-?\d+(?:\.\d+)?)vdc\s+Min Pack Voltage:\s+(-?\d+(?:\.\d+)?)vdc/g).exec(rx);
|
||||||
|
if(!!packMinMax) {
|
||||||
|
this.maxVoltage = Number(packMinMax[1]);
|
||||||
|
this.minVoltage = Number(packMinMax[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cell Min/Max
|
||||||
|
let cellMinMax = new RegExp(/Current High Cell Voltage:\s+(-?\d+(?:\.\d+)?)V\s+Low Cell Voltage:\s+(-?\d+(?:\.\d+)?)V/g).exec(rx);
|
||||||
|
if(!!cellMinMax) {
|
||||||
|
this.maxCellVoltage = Number(cellMinMax[1]);
|
||||||
|
this.minCellVoltage = Number(cellMinMax[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capicity
|
||||||
|
let capacity = new RegExp(/Configured Battery Capacity:\s+(-?\d+(?:\.\d+)?)Ah/g).exec(rx);
|
||||||
|
if(!!capacity) this.capacity = Number(capacity[1]);
|
||||||
|
|
||||||
|
// Lifetime
|
||||||
|
let lifetime = new RegExp(/Battery Lifetime Charging:\s+(-?\d+(?:\.\d+)?) kWh\s+Discharging:\s+(-?\d+(?:\.\d+)?) kWh/g).exec(rx);
|
||||||
|
if(!!lifetime) {
|
||||||
|
this.lifetimeCharge = Number(lifetime[1]);
|
||||||
|
this.lifetimeDischarge = Number(lifetime[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contactors
|
||||||
|
let contactor = new RegExp(/\s+(\w+) Contactor:(\w+)/g).exec(rx);
|
||||||
|
if(!!contactor) this[contactor[1].toLowerCase() + 'Contactor'] = contactor[2] == 'ON';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
41
battery/monitor.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const bms = require('./bms');
|
||||||
|
const firebase = require('firebase');
|
||||||
|
const namespace = require('fs').readFileSync('/etc/hostname', 'utf8').trim();
|
||||||
|
|
||||||
|
// init
|
||||||
|
firebase.initializeApp({
|
||||||
|
apiKey: "AIzaSyAs3FvBCADM66wR1-leBz6aIjK1wZfUxRo",
|
||||||
|
authDomain: "homefront-2ccb4.firebaseapp.com",
|
||||||
|
databaseURL: "https://homefront-2ccb4.firebaseio.com",
|
||||||
|
projectId: "homefront-2ccb4",
|
||||||
|
storageBucket: "homefront-2ccb4.appspot.com",
|
||||||
|
messagingSenderId: "482384317544"
|
||||||
|
});
|
||||||
|
const firestore = firebase.firestore();
|
||||||
|
firestore.settings({timestampsInSnapshots: true});
|
||||||
|
const powerwall = new bms('/dev/ttyACM0');
|
||||||
|
const timestamp = new Date().getTime();
|
||||||
|
|
||||||
|
// Wait to accumulate data and then submit
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
let data = powerwall.data();
|
||||||
|
data.timestamp = timestamp;
|
||||||
|
|
||||||
|
console.log(`(${(new Date()).toISOString()}) Saving...`);
|
||||||
|
await firestore.collection('Battery').doc(namespace)
|
||||||
|
.collection('data').doc(timestamp.toString())
|
||||||
|
.set(data);
|
||||||
|
powerwall.close();
|
||||||
|
console.log(`(${(new Date()).toISOString()}) Saved`);
|
||||||
|
process.exit();
|
||||||
|
} catch(err) {
|
||||||
|
console.log(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Remove any records older than 30 days
|
||||||
|
firestore.collection('Battery').doc(namespace).collection('data')
|
||||||
|
.where('timestamp', '<', timestamp - 2592000000).get()
|
||||||
|
.then(snapshot => snapshot.forEach(doc => doc.ref.delete()));
|
1035
battery/package-lock.json
generated
Normal file
17
battery/package.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "homefront-battery",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Battery module for HomeFront",
|
||||||
|
"main": "batteryEmulator.js",
|
||||||
|
"scripts": {
|
||||||
|
"run": "node monitor.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "Zak Timson",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"firebase": "^8.6.8",
|
||||||
|
"serialport": "^9.2.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
pictures/03e615d9-f1b3-4439-9341-185cd3c14f3f.jpg
Normal file
After Width: | Height: | Size: 152 KiB |
BIN
pictures/0a325f1a-2a4f-4879-beab-b0c6318f03a4.jpg
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
pictures/IMG_20181227_155200.jpg
Normal file
After Width: | Height: | Size: 3.7 MiB |
BIN
pictures/IMG_20181231_114251.jpg
Normal file
After Width: | Height: | Size: 3.2 MiB |
BIN
pictures/IMG_20181231_114259.jpg
Normal file
After Width: | Height: | Size: 2.9 MiB |
BIN
pictures/IMG_20181231_114304.jpg
Normal file
After Width: | Height: | Size: 3.1 MiB |
BIN
pictures/Resized_20181121_161639_7238.jpg
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
pictures/Resized_20181124_121520_5410.jpg
Normal file
After Width: | Height: | Size: 52 KiB |