signalk-rec-bms 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,2 +1,19 @@
1
1
  # signalk-rec-bms
2
+
2
3
  SignalK plugin for REC-BMS
4
+
5
+ ## Requirements
6
+
7
+ - A REC WiFi module with firmware >= SW164_2, which enables websockets
8
+ [Download firmware update](https://www.rec-bms.com/wp-content/uploads/2025/01/Wi-Fi-FW-Update_SW164_2.zip)
9
+
10
+ ## Dependencies
11
+
12
+ - npm package: [websocket](https://www.npmjs.com/package/websocket)
13
+
14
+ ## Installation
15
+
16
+ 1. Use the SignalK application store
17
+ 2. Run the following command:
18
+ ```bash
19
+ npm install signalk-rec-bms
package/index.js CHANGED
@@ -1,94 +1,87 @@
1
1
  const WebSocket = require('ws');
2
+ const status = require('./status');
3
+ const settings = require('./settings');
4
+ const schema = require('./schema');
2
5
 
3
6
  module.exports = function(app) {
4
7
  let ws;
5
- const initialReconnectInterval = 5000; // 5 seconds
8
+ const initialReconnectInterval = 5000;
6
9
  let currentReconnectInterval = initialReconnectInterval;
7
- const heartbeatInterval = 5000; // 5 seconds
10
+ const heartbeatInterval = 5000;
8
11
  let pingInterval;
9
12
  let shouldReconnect = true;
13
+ let heartbeatTimeout;
10
14
 
11
15
  var plugin = {
12
- id: "rec-bms-plugin",
13
- name: "REC BMS Plugin",
16
+ id: "signalk-rec-bms",
17
+ name: "SignalK-REC-BMS",
14
18
  description: "SignalK plugin for REC-BMS",
15
- schema: {
16
- type: "object",
17
- properties: {
18
- bmsUrl: {
19
- title: "BMS WebSocket URL",
20
- type: "string",
21
- default: "ws://bms-ip/ws"
22
- },
23
- deltaPrefix: {
24
- title: "Delta",
25
- type: "string",
26
- default: "electrical.batteries.bms"
27
- }
28
- }
29
- },
19
+ schema: schema,
30
20
  start: function(options) {
31
- app.debug("REC BMS Plugin: START function invoked with options " + JSON.stringify(options));
21
+ app.debug(`[signalk-rec-bms] (${new Date().toISOString()}) START function invoked with options: ${JSON.stringify(options)}`);
32
22
  app.setPluginStatus("Connecting to BMS WebSocket…");
33
23
  shouldReconnect = true;
34
24
  currentReconnectInterval = initialReconnectInterval;
35
25
 
36
- // Determine the vessel ID (use app.getSelfId if available, otherwise app.selfId or fallback to "self")
37
- let vesselId;
38
- if (typeof app.getSelfId === "function") {
39
- vesselId = app.getSelfId();
40
- } else if (app.selfId) {
41
- vesselId = app.selfId;
42
- } else {
43
- vesselId = "self";
44
- }
45
- const context = "vessels." + vesselId;
46
- app.debug("REC BMS Plugin: Using vessel context: " + context);
26
+ let vesselId = typeof app.getSelfId === "function" ? app.getSelfId() : app.selfId || "self";
27
+ const context = `vessels.${vesselId}`;
28
+ app.debug(`[signalk-rec-bms] (${new Date().toISOString()}) Using vessel context: ${context}`);
47
29
 
48
- // Get the user-defined delta prefix (or default if not provided)
49
30
  const prefix = options.deltaPrefix || "electrical.batteries.bms";
50
- app.debug("REC BMS Plugin: Using delta prefix: " + prefix);
31
+ app.debug(`[signalk-rec-bms] (${new Date().toISOString()}) Using delta prefix: ${prefix}`);
32
+
33
+ function resetHeartbeat() {
34
+ clearTimeout(heartbeatTimeout);
35
+ heartbeatTimeout = setTimeout(() => {
36
+ app.debug("[signalk-rec-bms] Heartbeat timeout. Terminating connection.");
37
+ if (ws) {
38
+ ws.terminate();
39
+ }
40
+ }, heartbeatInterval);
41
+ }
51
42
 
52
- // Function to (re)connect.
53
43
  function connect() {
54
- app.debug("REC BMS Plugin: Attempting to connect to " + options.bmsUrl);
44
+ app.debug(`[signalk-rec-bms] (${new Date().toISOString()}) Attempting to connect to ${options.bmsUrl}`);
55
45
  ws = new WebSocket(options.bmsUrl);
56
46
 
57
47
  ws.on('open', () => {
58
- app.debug("REC BMS Plugin: WebSocket connection opened");
59
- app.setPluginStatus("Connected to BMS");
60
- // Reset reconnection interval on successful connection.
48
+ app.debug("[signalk-rec-bms] WebSocket connection opened");
49
+ app.setPluginStatus("Connected to BMS - waiting for serial number");
61
50
  currentReconnectInterval = initialReconnectInterval;
62
- // Start the ping interval for heartbeat.
51
+
63
52
  pingInterval = setInterval(() => {
64
53
  if (ws && ws.readyState === WebSocket.OPEN) {
65
54
  ws.ping();
66
- app.debug("REC BMS Plugin: Sent ping");
55
+ app.debug("[signalk-rec-bms] Sent ping");
67
56
  }
68
57
  }, heartbeatInterval);
58
+
59
+ resetHeartbeat();
69
60
  });
70
61
 
71
62
  ws.on('pong', () => {
72
- app.debug("REC BMS Plugin: Received pong");
63
+ app.debug("[signalk-rec-bms] Received pong");
73
64
  });
74
65
 
75
66
  ws.on('message', (data) => {
67
+ resetHeartbeat();
76
68
  let message = data;
69
+
77
70
  if (typeof message !== 'string') {
78
71
  if (Buffer.isBuffer(message)) {
79
72
  message = message.toString();
80
73
  } else {
81
- app.debug("REC BMS Plugin: Received payload is not a string or Buffer");
74
+ app.debug("[signalk-rec-bms] Received payload is not a string or Buffer");
82
75
  return;
83
76
  }
84
77
  }
85
- app.debug("REC BMS Plugin: Received message: " + message);
78
+
79
+ app.debug(`[signalk-rec-bms] (${new Date().toISOString()}) Received message: ${message}`);
86
80
 
87
81
  try {
88
82
  const parsedData = JSON.parse(message);
89
- app.debug("REC BMS Plugin: JSON parsed successfully");
83
+ app.debug("[signalk-rec-bms] JSON parsed successfully");
90
84
 
91
- // Build the delta update with proper context and timestamp.
92
85
  let delta = {
93
86
  context: context,
94
87
  updates: [{
@@ -97,208 +90,65 @@ module.exports = function(app) {
97
90
  }]
98
91
  };
99
92
 
100
- // Helper: Convert time strings like "1 h 15 min" into minutes.
101
- function extractMinutes(timeString) {
102
- if (!timeString) return null;
103
- let match = timeString.match(/(\d+)\s*h\s*(\d+)?\s*min/);
104
- if (match) {
105
- let hours = parseInt(match[1], 10);
106
- let minutes = match[2] ? parseInt(match[2], 10) : 0;
107
- return hours * 60 + minutes;
108
- }
109
- return null;
110
- }
111
-
112
- // Process "status" messages.
113
- if (parsedData.type === "status" && parsedData.bms_array) {
114
- let master = parsedData.bms_array.master;
115
- let power = (master.vbat !== undefined && master.ibat !== undefined)
116
- ? master.vbat * master.ibat : null;
117
- let cellVoltageDifference = master.maxcell - master.mincell;
118
-
119
- delta.updates[0].values.push(
120
- { path: `${prefix}.MinCellVoltage`, value: master.mincell },
121
- { path: `${prefix}.MaxCellVoltage`, value: master.maxcell },
122
- { path: `${prefix}.CellVoltageDifference`, value: cellVoltageDifference },
123
- { path: `${prefix}.Current`, value: master.ibat },
124
- { path: `${prefix}.Temperature`, value: master.tmax },
125
- { path: `${prefix}.Voltage`, value: master.vbat },
126
- { path: `${prefix}.StateOfCharge`, value: master.soc },
127
- { path: `${prefix}.StateOfHealth`, value: master.soh },
128
- { path: `${prefix}.TimeRemaining`, value: extractMinutes(master.time_remaining) },
129
- { path: `${prefix}.ChargeDirection`, value: master.st_naprav },
130
- { path: `${prefix}.ErrorPresent`, value: master.erro.present },
131
- { path: `${prefix}.ErrorAddress`, value: master.erro.addr },
132
- { path: `${prefix}.ErrorStatus`, value: master.erro.st },
133
- { path: `${prefix}.ErrorConnection`, value: master.erro.con_st },
134
- { path: `${prefix}.ErrorMessage`, value: master.error },
135
- { path: `${prefix}.Power`, value: power },
136
- { path: `${prefix}.MasterTime`, value: master.time },
137
- { path: `${prefix}.MasterDate`, value: master.date }
138
- );
139
-
140
- if (parsedData.bms_array.slave) {
141
- let slave = parsedData.bms_array.slave["0"];
142
- if (slave) {
143
- delta.updates[0].values.push(
144
- { path: `${prefix}.SlaveAddress`, value: slave.address },
145
- { path: `${prefix}.SlaveTempSensorStatus`, value: slave.st_temp },
146
- { path: `${prefix}.BMSTemperature`, value: slave.temp_bms },
147
- { path: `${prefix}.SlaveCellCount`, value: slave.st_celic }
148
- );
149
-
150
- Object.entries(slave.temp || {}).forEach(([key, value], index) => {
151
- delta.updates[0].values.push({
152
- path: `${prefix}.CellTemperature${index + 1}`,
153
- value
154
- });
155
- });
156
-
157
- Object.entries(slave.res || {}).forEach(([key, value], index) => {
158
- delta.updates[0].values.push({
159
- path: `${prefix}.CellResistance${index + 1}`,
160
- value
161
- });
162
- });
163
-
164
- Object.entries(slave.nap || {}).forEach(([key, value], index) => {
165
- delta.updates[0].values.push({
166
- path: `${prefix}.CellVoltage${index + 1}`,
167
- value
168
- });
169
- });
170
- }
171
- }
93
+ if (parsedData.type === "status") {
94
+ app.debug("[signalk-rec-bms] Processing STATUS message...");
95
+ const statusValues = status(parsedData, prefix, options.statusDeltas) || [];
96
+ delta.updates[0].values.push(...statusValues);
172
97
  }
173
98
 
174
- // Process "settings" messages.
175
99
  if (parsedData.type === "settings") {
176
- let AhRemaining = (parsedData.capa !== undefined && parsedData.Ah !== undefined)
177
- ? parsedData.capa - parsedData.Ah : null;
178
- let maxChargeVoltage = (parsedData.cmax !== undefined && parsedData.cans !== undefined)
179
- ? parsedData.cmax * parsedData.cans : null;
180
-
181
- delta.updates[0].values.push(
182
- { path: `${prefix}.MinVoltageThreshold`, value: parsedData.cmin },
183
- { path: `${prefix}.MaxVoltageThreshold`, value: parsedData.cmax },
184
- { path: `${prefix}.Tmax`, value: parsedData.tmax },
185
- { path: `${prefix}.BVoltage`, value: parsedData.bvol },
186
- { path: `${prefix}.BMinVoltage`, value: parsedData.bmin },
187
- { path: `${prefix}.TBalance`, value: parsedData.tbal },
188
- { path: `${prefix}.Tmin`, value: parsedData.tmin },
189
- { path: `${prefix}.Capacity`, value: parsedData.capa },
190
- { path: `${prefix}.ChargeVoltage`, value: parsedData.char },
191
- { path: `${prefix}.Ioff`, value: parsedData.ioff },
192
- { path: `${prefix}.ChargeHysteresis`, value: parsedData.chis },
193
- { path: `${prefix}.VoltageDiffThreshold`, value: parsedData.razl },
194
- { path: `${prefix}.MaxH`, value: parsedData.maxh },
195
- { path: `${prefix}.MinH`, value: parsedData.minh },
196
- { path: `${prefix}.BMSThreshold`, value: parsedData.bmth },
197
- { path: `${prefix}.Ioja`, value: parsedData.ioja },
198
- { path: `${prefix}.Soch`, value: parsedData.soch },
199
- { path: `${prefix}.Op2L`, value: parsedData.op2l },
200
- { path: `${prefix}.Op2H`, value: parsedData.op2h },
201
- { path: `${prefix}.Re1L`, value: parsedData.re1l },
202
- { path: `${prefix}.Re1H`, value: parsedData.re1h },
203
- { path: `${prefix}.ChargeMode`, value: parsedData.chac },
204
- { path: `${prefix}.DischargeMode`, value: parsedData.dchc },
205
- { path: `${prefix}.MaxChargeCurrent`, value: parsedData.maxc },
206
- { path: `${prefix}.MaxDischargeCurrent`, value: parsedData.maxd },
207
- { path: `${prefix}.LowVoltageCutoff`, value: parsedData.clow },
208
- //{ path: `${prefix}.StateOfChargeS`, value: parsedData.socs },
209
- { path: `${prefix}.CycleCount`, value: parsedData.cycl },
210
- { path: `${prefix}.Cans`, value: parsedData.cans },
211
- { path: `${prefix}.Chemistry`, value: parsedData.chem },
212
- { path: `${prefix}.Strn`, value: parsedData.strn },
213
- { path: `${prefix}.Re1T`, value: parsedData.re1t },
214
- { path: `${prefix}.Op2T`, value: parsedData.op2t },
215
- { path: `${prefix}.Re1V`, value: parsedData.re1v },
216
- { path: `${prefix}.Op2V`, value: parsedData.op2v },
217
- { path: `${prefix}.Cfvc`, value: parsedData.cfvc },
218
- { path: `${prefix}.Rsbr`, value: parsedData.rsbr },
219
- { path: `${prefix}.BMSClockTime`, value: parsedData.mcu_time },
220
- { path: `${prefix}.BMSClockDate`, value: parsedData.mcu_date },
221
- { path: `${prefix}.BMSName`, value: parsedData.bms_name },
222
- { path: `${prefix}.Address`, value: parsedData.addr },
223
- { path: `${prefix}.TUnit`, value: parsedData.tunit },
224
- { path: `${prefix}.AhUsed`, value: parsedData.Ah },
225
- { path: `${prefix}.Current`, value: parsedData.cur },
226
- { path: `${prefix}.NewLogEveryMidnight`, value: parsedData.new_log_every_midnight },
227
- { path: `${prefix}.Out`, value: parsedData.out },
228
- { path: `${prefix}.AhRemaining`, value: AhRemaining },
229
- { path: `${prefix}.MaxChargeVoltage`, value: maxChargeVoltage }
230
- );
231
-
232
- if (parsedData.baud) {
233
- delta.updates[0].values.push(
234
- { path: `${prefix}.LcdBaudRate`, value: parsedData.baud.lcd },
235
- { path: `${prefix}.ComBaudRate`, value: parsedData.baud.com }
236
- );
237
- }
100
+ app.debug("[signalk-rec-bms] Processing SETTINGS message...");
101
+ const settingsValues = settings(parsedData, prefix, options.settingsDeltas) || [];
102
+ delta.updates[0].values.push(...settingsValues);
238
103
 
239
- if (parsedData.err) {
240
- delta.updates[0].values.push(
241
- { path: `${prefix}.ErrorPresenceFlag`, value: parsedData.err.p },
242
- { path: `${prefix}.ErrorNumber`, value: parsedData.err.num }
243
- );
244
- }
245
-
246
- if (parsedData.nnc) {
247
- delta.updates[0].values.push(
248
- { path: `${prefix}.NearestNeighborBMS`, value: parsedData.nnc.bms },
249
- { path: `${prefix}.NearestNeighborCell`, value: parsedData.nnc.cell }
250
- );
251
- }
252
-
253
- if (parsedData.vnc) {
254
- delta.updates[0].values.push(
255
- { path: `${prefix}.VNCBMS`, value: parsedData.vnc.bms },
256
- { path: `${prefix}.VNCCell`, value: parsedData.vnc.cell }
257
- );
258
- }
259
-
260
- if (parsedData.toor) {
261
- delta.updates[0].values.push(
262
- { path: `${prefix}.ToorBMS`, value: parsedData.toor.bms },
263
- { path: `${prefix}.ToorCell`, value: parsedData.toor.cell }
264
- );
104
+ if (parsedData.bms_name) {
105
+ app.setPluginStatus(`Connected to BMS: ${parsedData.bms_name}`);
265
106
  }
266
107
  }
267
108
 
268
109
  delta.updates[0].values = delta.updates[0].values.filter(item => item.value !== null && item.value !== undefined);
269
110
 
270
- app.debug("REC BMS Plugin: Sending delta: " + JSON.stringify(delta));
271
- app.handleMessage(plugin.id, delta, 'v1');
111
+ if (delta.updates[0].values.length > 0) {
112
+ app.debug(`[signalk-rec-bms] (${new Date().toISOString()}) Sending delta: ${JSON.stringify(delta)}`);
113
+ app.handleMessage(plugin.id, delta, 'v1');
114
+ } else {
115
+ app.debug("[signalk-rec-bms] No valid deltas to send.");
116
+ }
272
117
  } catch (err) {
273
- app.error("REC BMS Plugin: Failed to parse JSON: " + err.message);
118
+ app.error(`[signalk-rec-bms] Failed to parse JSON: ${err.message}`);
274
119
  }
275
120
  });
276
121
 
277
122
  ws.on('close', () => {
278
- app.debug("REC BMS Plugin: WebSocket connection closed");
123
+ app.debug("[signalk-rec-bms] WebSocket connection closed");
279
124
  app.setPluginStatus("BMS disconnected - ERROR");
280
125
  clearInterval(pingInterval);
126
+
281
127
  if (shouldReconnect) {
282
- app.debug("REC BMS Plugin: Reconnecting in " + currentReconnectInterval + "ms");
128
+ app.debug(`[signalk-rec-bms] Reconnecting in ${currentReconnectInterval}ms`);
283
129
  setTimeout(connect, currentReconnectInterval);
284
- // Exponential backoff, up to a maximum of 60 seconds.
285
130
  currentReconnectInterval = Math.min(currentReconnectInterval * 2, 60000);
286
131
  }
287
132
  });
288
133
 
289
134
  ws.on('error', (error) => {
290
- app.error("REC BMS Plugin: error: " + error.message);
291
- app.setPluginStatus("Error: " + error.message + " - ERROR");
135
+ app.error(`[signalk-rec-bms] WebSocket error: ${error.message}`);
136
+ app.setPluginStatus(`Error: ${error.message} - ERROR`);
292
137
  clearInterval(pingInterval);
293
138
  if (ws) {
294
139
  ws.terminate();
295
140
  }
141
+ if (shouldReconnect) {
142
+ app.debug(`[signalk-rec-bms] Reconnecting in ${currentReconnectInterval}ms due to error.`);
143
+ setTimeout(connect, currentReconnectInterval);
144
+ currentReconnectInterval = Math.min(currentReconnectInterval * 2, 60000);
145
+ }
296
146
  });
297
147
  }
298
148
 
299
- // Start by connecting.
300
149
  connect();
301
150
  },
151
+
302
152
  stop: function() {
303
153
  shouldReconnect = false;
304
154
  clearInterval(pingInterval);
@@ -306,7 +156,7 @@ module.exports = function(app) {
306
156
  ws.close();
307
157
  ws = null;
308
158
  }
309
- app.debug("REC BMS Plugin: stopped");
159
+ app.debug("[signalk-rec-bms] Plugin stopped");
310
160
  app.setPluginStatus("Stopped");
311
161
  }
312
162
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-rec-bms",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "SignalK plugin for REC-BMS",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -18,6 +18,10 @@
18
18
  "dependencies": {
19
19
  "ws": "^8.18.1"
20
20
  },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/ofernander/signalk-rec-bms"
24
+ },
21
25
  "engines": {
22
26
  "node": ">=10"
23
27
  }
package/schema.js ADDED
@@ -0,0 +1,106 @@
1
+ module.exports = {
2
+ type: "object",
3
+ properties: {
4
+ bmsUrl: {
5
+ title: "BMS WebSocket URL",
6
+ type: "string",
7
+ default: "ws://WiFi-module-ip/ws"
8
+ },
9
+ deltaPrefix: {
10
+ title: "Delta Path Prefix",
11
+ type: "string",
12
+ default: "electrical.batteries.bms"
13
+ },
14
+ statusDeltas: {
15
+ title: "Status Delta Options",
16
+ type: "object",
17
+ properties: {
18
+ vbat: { type: "boolean", title: "Voltage", default: true },
19
+ ibat: { type: "boolean", title: "Current", default: true },
20
+ wbat: { type: "boolean", title: "Power", default: true },
21
+ mincell: { type: "boolean", title: "Min Cell Voltage", default: true },
22
+ maxcell: { type: "boolean", title: "Max Cell Voltage", default: true },
23
+ res: { type: "boolean", title: "Cell Resistance (all)", default: true },
24
+ temp: { type: "boolean", title: "Cell Temperature (all)", default: true },
25
+ nap: { type: "boolean", title: "Cell Voltage (all)", default: true },
26
+ cvd: { type: "boolean", title: "Cell Voltage Difference", default: true },
27
+ soc: { type: "boolean", title: "State Of Charge", default: true },
28
+ soh: { type: "boolean", title: "State Of Health", default: true },
29
+ time_remaining: { type: "boolean", title: "Time to 0% SOC Remaining", default: true },
30
+ temp_bms: { type: "boolean", title: "BMS Temperature", default: true },
31
+ erro_present: { type: "boolean", title: "Error Present", default: false },
32
+ erro_addr: { type: "boolean", title: "Error Address", default: false },
33
+ erro_con_st: { type: "boolean", title: "Error Connection State", default: false },
34
+ erro_st: { type: "boolean", title: "Error Status", default: false },
35
+ error: { type: "boolean", title: "Error Message", default: false },
36
+ slaveAddress: { type: "boolean", title: "Slave Address", default: false },
37
+ st_celic: { type: "boolean", title: "Slave Cell Count", default: false },
38
+ st_temp: { type: "boolean", title: "Slave Temp Sensor Status", default: false },
39
+ st_naprav: { type: "boolean", title: "UNKNOWN", default: false },
40
+ }
41
+ },
42
+ settingsDeltas: {
43
+ title: "Settings Delta Options",
44
+ type: "object",
45
+ properties: {
46
+ AhR: { type: "boolean", title: "Amp Hours Remaining", default: true },
47
+ bvol: { type: "boolean", title: "Balancing END Voltage", default: false },
48
+ bmin: { type: "boolean", title: "Balancing START Voltage", default: false },
49
+ razl: { type: "boolean", title: "Max Allowed Cell Voltage Difference", default: false },
50
+ char: { type: "boolean", title: "End Of Charge Voltage", default: false },
51
+ chis: { type: "boolean", title: "End of Charging Voltage Hysteresis", default: false },
52
+ cmax: { type: "boolean", title: "Max Allowed Cell Voltage", default: false },
53
+ maxh: { type: "boolean", title: "Max Allowed Cell Voltage Hysteresis", default: false },
54
+ cmin: { type: "boolean", title: "Min Allowed Cell Voltage", default: false },
55
+ minh: { type: "boolean", title: "Min Allowed Cell Voltage Hysteresis", default: false },
56
+ clow: { type: "boolean", title: "Min Allowed Cell Discharge", default: false },
57
+ cfvc: { type: "boolean", title: "Float Voltage Coefficient", default: false },
58
+ ioja: { type: "boolean", title: "Current Sensor Coefficient", default: false },
59
+ ioff: { type: "boolean", title: "Current Sensor Offset", default: false },
60
+ maxc: { type: "boolean", title: "Max Charge Current", default: false },
61
+ maxd: { type: "boolean", title: "Max Discharge Current", default: false },
62
+ chac: { type: "boolean", title: "Charging Coefficient", default: false },
63
+ dchc: { type: "boolean", title: "Discharge Coefficient", default: false },
64
+ tmin: { type: "boolean", title: "minAllowedChargeTemp", default: false },
65
+ tmax: { type: "boolean", title: "maxAllowedChargeTemp", default: false },
66
+ tbal: { type: "boolean", title: "maxAllowedBMSTemp", default: false },
67
+ bmth: { type: "boolean", title: "maxAllowedBMSHysteresis", default: false },
68
+ mcu_date: { type: "boolean", title: "BMS Date", default: false },
69
+ mcu_time: { type: "boolean", title: "BMS Time", default: false },
70
+ bms_name: { type: "boolean", title: "BMS Name", default: true },
71
+ cycl: { type: "boolean", title: "Battery Cycle Count", default: true },
72
+ capa: { type: "boolean", title: "Capacity", default: false },
73
+ chem: { type: "boolean", title: "Chemistry", default: false },
74
+ addr: { type: "boolean", title: "Address", default: false },
75
+ tunit: { type: "boolean", title: "T-unit", default: false },
76
+ cmin: { type: "boolean", title: "Min Allowed Cell Voltage", default: false },
77
+ cmax: { type: "boolean", title: "Max Allowed Cell Voltage", default: false },
78
+ //cur: { type: "boolean", title: "Current 2", default: false },
79
+ new_log_every_midnight: { type: "boolean", title: "New Log Every Midnight", default: false },
80
+ out: { type: "boolean", title: "Out", default: false },
81
+ cans: { type: "boolean", title: "Can Bus Protocol", default: false },
82
+ lcd: { type: "boolean", title: "LCD Baud Rate", default: false },
83
+ com: { type: "boolean", title: "Com Baud Rate", default: false },
84
+ p: { type: "boolean", title: "Error Presence Flag", default: false },
85
+ num: { type: "boolean", title: "Error Number", default: false },
86
+ op2h: { type: "boolean", title: "Op2H", default: false },
87
+ op2l: { type: "boolean", title: "Op2L", default: false },
88
+ op2t: { type: "boolean", title: "Op2T", default: false },
89
+ op2v: { type: "boolean", title: "Op2V", default: false },
90
+ re1h: { type: "boolean", title: "Re1H", default: false },
91
+ re1l: { type: "boolean", title: "Re1L", default: false },
92
+ re1t: { type: "boolean", title: "Re1T", default: false },
93
+ re1v: { type: "boolean", title: "Re1V", default: false },
94
+ socs: { type: "boolean", title: "SoCs", default: false },
95
+ soch: { type: "boolean", title: "SoCh", default: false },
96
+ strn: { type: "boolean", title: "Strn", default: false },
97
+ rsbr: { type: "boolean", title: "Rsbr", default: false },
98
+ errorGroup: { type: "boolean", title: "Error Group", default: false },
99
+ nncGroup: { type: "boolean", title: "Nearest Neighbor Group", default: false },
100
+ vncGroup: { type: "boolean", title: "Vnc Group", default: false },
101
+ toorGroup: { type: "boolean", title: "Toor Group", default: false },
102
+ baudGroup: { type: "boolean", title: "Baud Group", default: false }
103
+ }
104
+ }
105
+ }
106
+ };
package/settings.js ADDED
@@ -0,0 +1,160 @@
1
+ module.exports = function(parsedData, prefix, config) {
2
+ if (parsedData.type !== "settings") return [];
3
+
4
+ const settings = parsedData;
5
+ const values = [];
6
+
7
+ // derived data - NOT FROM BMS
8
+ if (config.AhR) {
9
+ const AhR = (parsedData.capa !== undefined && parsedData.Ah !== undefined)
10
+ ? parsedData.capa - parsedData.Ah : null;
11
+ values.push({ path: `${prefix}.ampHourRemaining`, value: AhR });
12
+ }
13
+ // data from WiFi module WebSocket under SETTINGS message
14
+ if (config.cmin) {
15
+ values.push({ path: `${prefix}.minCellVoltage`, value: parsedData.cmin });
16
+ }
17
+ if (config.cmax) {
18
+ values.push({ path: `${prefix}.maxCellVoltage`, value: parsedData.cmax });
19
+ }
20
+ if (config.tmax) {
21
+ values.push({ path: `${prefix}.maxAllowedCellTemp`, value: parsedData.tmax });
22
+ }
23
+ if (config.bvol) {
24
+ values.push({ path: `${prefix}.balEndVoltage`, value: parsedData.bvol });
25
+ }
26
+ if (config.bmin) {
27
+ values.push({ path: `${prefix}.balStartVoltage`, value: parsedData.bmin });
28
+ }
29
+ if (config.tbal) {
30
+ values.push({ path: `${prefix}.maxAllowedBMSTemp`, value: parsedData.tbal });
31
+ }
32
+ if (config.tmin) {
33
+ values.push({ path: `${prefix}.minAllowedCellTemp`, value: parsedData.tmin });
34
+ }
35
+ if (config.capa) {
36
+ values.push({ path: `${prefix}.capacity`, value: parsedData.capa });
37
+ }
38
+ if (config.char) {
39
+ values.push({ path: `${prefix}.endChargeVoltage`, value: parsedData.char });
40
+ }
41
+ if (config.ioff) {
42
+ values.push({ path: `${prefix}.currentOffset`, value: parsedData.ioff });
43
+ }
44
+ if (config.chis) {
45
+ values.push({ path: `${prefix}.endChargeHysteresis`, value: parsedData.chis });
46
+ }
47
+ if (config.razl) {
48
+ values.push({ path: `${prefix}.maxAllowedCellVoltDiff`, value: parsedData.razl });
49
+ }
50
+ if (config.maxh) {
51
+ values.push({ path: `${prefix}.maxAllowedVoltageHysteresis`, value: parsedData.maxh });
52
+ }
53
+ if (config.minh) {
54
+ values.push({ path: `${prefix}.minAllowedVoltageHysteresis`, value: parsedData.minh });
55
+ }
56
+ if (config.bmth) {
57
+ values.push({ path: `${prefix}.maxAllowedBMSTempHysteresis`, value: parsedData.bmth });
58
+ }
59
+ if (config.ioja) {
60
+ values.push({ path: `${prefix}.currentSensorCoefficient`, value: parsedData.ioja });
61
+ }
62
+ if (config.soch) {
63
+ values.push({ path: `${prefix}.soch`, value: parsedData.soch });
64
+ }
65
+ if (config.op2l) {
66
+ values.push({ path: `${prefix}.opto2Low`, value: parsedData.op2l });
67
+ }
68
+ if (config.op2h) {
69
+ values.push({ path: `${prefix}.opto2High`, value: parsedData.op2h });
70
+ }
71
+ if (config.re1l) {
72
+ values.push({ path: `${prefix}.relay1Low`, value: parsedData.re1l });
73
+ }
74
+ if (config.re1h) {
75
+ values.push({ path: `${prefix}.relay1High`, value: parsedData.re1h });
76
+ }
77
+ if (config.chac) {
78
+ values.push({ path: `${prefix}.chargeCoefficient`, value: parsedData.chac });
79
+ }
80
+ if (config.dchc) {
81
+ values.push({ path: `${prefix}.dischargeCoefficient`, value: parsedData.dchc });
82
+ }
83
+ if (config.maxc) {
84
+ values.push({ path: `${prefix}.maxChargeCurrent`, value: parsedData.maxc });
85
+ }
86
+ if (config.maxd) {
87
+ values.push({ path: `${prefix}.maxDischargeCurrent`, value: parsedData.maxd });
88
+ }
89
+ if (config.clow) {
90
+ values.push({ path: `${prefix}.minDischargeCellVoltage`, value: parsedData.clow });
91
+ }
92
+ if (config.socs) {
93
+ values.push({ path: `${prefix}.socs`, value: parsedData.socs });
94
+ }
95
+ if (config.cycl) {
96
+ values.push({ path: `${prefix}.cycleCount`, value: parsedData.cycl });
97
+ }
98
+ if (config.cans) {
99
+ values.push({ path: `${prefix}.canbusSetting`, value: parsedData.cans });
100
+ }
101
+ if (config.chem) {
102
+ values.push({ path: `${prefix}.chemistry`, value: parsedData.chem });
103
+ }
104
+ if (config.strn) {
105
+ values.push({ path: `${prefix}.numberOfInverterDevices`, value: parsedData.strn });
106
+ }
107
+ if (config.mcu_time) {
108
+ values.push({ path: `${prefix}.mcuTime`, value: parsedData.mcu_time });
109
+ }
110
+ if (config.mcu_date) {
111
+ values.push({ path: `${prefix}.mcuDate`, value: parsedData.mcu_date });
112
+ }
113
+ if (config.bms_name) {
114
+ values.push({ path: `${prefix}.bmsName`, value: parsedData.bms_name });
115
+ }
116
+ if (config.addr) {
117
+ values.push({ path: `${prefix}.address`, value: parsedData.addr });
118
+ }
119
+ if (config.tunit) {
120
+ values.push({ path: `${prefix}.temperatureUnit`, value: parsedData.tunit });
121
+ }
122
+ if (config.Ah) {
123
+ values.push({ path: `${prefix}.Ah`, value: parsedData.Ah });
124
+ }
125
+ //if (config.cur) {
126
+ // values.push({ path: `${prefix}.current2`, value: parsedData.cur });
127
+ //}
128
+ if (config.new_log_every_midnight) {
129
+ values.push({ path: `${prefix}.newLogEveryMidnight`, value: parsedData.new_log_every_midnight });
130
+ }
131
+ if (config.out) {
132
+ values.push({ path: `${prefix}.outputStatus`, value: parsedData.out });
133
+ }
134
+ if (parsedData.err && config.errorGroup) {
135
+ Object.entries(parsedData.err).forEach(([childKey, childValue]) => {
136
+ values.push({ path: `${prefix}.err.${childKey}`, value: childValue });
137
+ });
138
+ }
139
+ if (parsedData.nnc && config.nncGroup) {
140
+ Object.entries(parsedData.nnc).forEach(([childKey, childValue]) => {
141
+ values.push({ path: `${prefix}.nnc.${childKey}`, value: childValue });
142
+ });
143
+ }
144
+ if (parsedData.vnc && config.vncGroup) {
145
+ Object.entries(parsedData.vnc).forEach(([childKey, childValue]) => {
146
+ values.push({ path: `${prefix}.vnc.${childKey}`, value: childValue });
147
+ });
148
+ }
149
+ if (parsedData.toor && config.toorGroup) {
150
+ Object.entries(parsedData.toor).forEach(([childKey, childValue]) => {
151
+ values.push({ path: `${prefix}.toor.${childKey}`, value: childValue });
152
+ });
153
+ }
154
+ if (parsedData.baud && config.baudGroup) {
155
+ Object.entries(parsedData.baud).forEach(([childKey, childValue]) => {
156
+ values.push({ path: `${prefix}.${childKey}`, value: childValue });
157
+ });
158
+ }
159
+ return values;
160
+ };
package/status.js ADDED
@@ -0,0 +1,112 @@
1
+ module.exports = function(parsedData, prefix, config) {
2
+ const values = [];
3
+ if (!parsedData.bms_array || !parsedData.bms_array.master) return values;
4
+
5
+ const master = parsedData.bms_array.master;
6
+
7
+ // convert time into seconds
8
+ function extractMinutes(timeString) {
9
+ if (!timeString) return null;
10
+ const match = timeString.match(/(\d+)\s*h\s*(\d+)?\s*min/);
11
+ if (match) {
12
+ const hours = parseInt(match[1], 10);
13
+ const minutes = match[2] ? parseInt(match[2], 10) : 0;
14
+ return hours * 60 + minutes;
15
+ }
16
+ return null;
17
+ }
18
+
19
+ // derived data - NOT FROM BMS
20
+ if (config.wbat) {
21
+ const power = (master.vbat !== undefined && master.ibat !== undefined)
22
+ ? master.vbat * master.ibat : null;
23
+ values.push({ path: `${prefix}.power`, value: power });
24
+ }
25
+
26
+ // data from WiFi module WebSocket under SETTINGS message
27
+ if (config.mincell) {
28
+ values.push({ path: `${prefix}.minCellVoltage`, value: master.mincell });
29
+ }
30
+ if (config.maxcell) {
31
+ values.push({ path: `${prefix}.maxCellVoltage`, value: master.maxcell });
32
+ }
33
+ if (config.cvd) {
34
+ values.push({ path: `${prefix}.cellVoltageDifference`, value: master.maxcell - master.mincell });
35
+ }
36
+ if (config.ibat) {
37
+ values.push({ path: `${prefix}.current`, value: master.ibat });
38
+ }
39
+ if (config.tmax) {
40
+ values.push({ path: `${prefix}.temperature`, value: master.tmax });
41
+ }
42
+ if (config.vbat) {
43
+ values.push({ path: `${prefix}.voltage`, value: master.vbat });
44
+ }
45
+ if (config.soc) {
46
+ values.push({ path: `${prefix}.stateOfCharge`, value: master.soc });
47
+ }
48
+ if (config.soh) {
49
+ values.push({ path: `${prefix}.stateOfHealth`, value: master.soh });
50
+ }
51
+ if (config.time_remaining) {
52
+ values.push({ path: `${prefix}.timeRemaining`, value: extractMinutes(master.time_remaining) });
53
+ }
54
+ if (config.st_naprav) {
55
+ values.push({ path: `${prefix}.chargeDirection`, value: master.st_naprav });
56
+ }
57
+ if (master.erro) {
58
+ if (config.erro_present) {
59
+ values.push({ path: `${prefix}.errorPresent`, value: master.erro.present });
60
+ }
61
+ if (config.erro_addr) {
62
+ values.push({ path: `${prefix}.errorAddress`, value: master.erro.addr });
63
+ }
64
+ if (config.erro_st) {
65
+ values.push({ path: `${prefix}.errorStatus`, value: master.erro.st });
66
+ }
67
+ if (config.erro_con_st) {
68
+ values.push({ path: `${prefix}.errorConnection`, value: master.erro.con_st });
69
+ }
70
+ }
71
+ if (config.error) {
72
+ values.push({ path: `${prefix}.errorMessage`, value: master.error });
73
+ }
74
+ if (config.mcu_time) {
75
+ values.push({ path: `${prefix}.mcuTime`, value: master.time });
76
+ }
77
+ if (config.mcu_date) {
78
+ values.push({ path: `${prefix}.mcuDate`, value: master.date });
79
+ }
80
+ if (parsedData.bms_array.slave && parsedData.bms_array.slave["0"]) {
81
+ const slave = parsedData.bms_array.slave["0"];
82
+
83
+ if (config.slaveAddress) {
84
+ values.push({ path: `${prefix}.slaveAddress`, value: slave.address });
85
+ }
86
+ if (config.st_temp) {
87
+ values.push({ path: `${prefix}.slaveTempSensorStatus`, value: slave.st_temp });
88
+ }
89
+ if (config.temp_bms) {
90
+ values.push({ path: `${prefix}.bmsTemperature`, value: slave.temp_bms });
91
+ }
92
+ if (config.st_celic) {
93
+ values.push({ path: `${prefix}.slaveCellCount`, value: slave.st_celic });
94
+ }
95
+ if (config.temp) {
96
+ Object.entries(slave.temp || {}).forEach(([key, value], index) => {
97
+ values.push({ path: `${prefix}.cellTemperature${index + 1}`, value });
98
+ });
99
+ }
100
+ if (config.res) {
101
+ Object.entries(slave.res || {}).forEach(([key, value], index) => {
102
+ values.push({ path: `${prefix}.cellResistance${index + 1}`, value });
103
+ });
104
+ }
105
+ if (config.nap) {
106
+ Object.entries(slave.nap || {}).forEach(([key, value], index) => {
107
+ values.push({ path: `${prefix}.cellVoltage${index + 1}`, value });
108
+ });
109
+ }
110
+ }
111
+ return values;
112
+ };