signalk-rec-bms 1.1.0 → 1.1.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,6 +1,6 @@
1
1
  # signalk-rec-bms
2
2
 
3
- ![alt text](https://github.com/ofernander/signalk-rec-bms/blob/main/public/assets/images/rec-sk.png)
3
+ ![alt text](https://raw.githubusercontent.com/ofernander/signalk-rec-bms/refs/heads/main/public/assets/images/rec-sk.png)
4
4
 
5
5
  ## Features
6
6
 
@@ -24,11 +24,11 @@ Tested with the 4 cell ABMS model only
24
24
 
25
25
  #### Serial connection:
26
26
 
27
- - RS485 to USB adapter with 5v output. The REC-BMS serial chip is not powered internally and you must supply 5v to power it.
27
+ - RS485 to USB adapter with 3v output. The REC-BMS serial chip is not powered internally and you must supply 3v to power it.
28
28
 
29
- ![alt text](https://github.com/ofernander/signalk-rec-bms/blob/main/public/assets/images/rs485.jpg)
29
+ ![alt text](https://raw.githubusercontent.com/ofernander/signalk-rec-bms/refs/heads/main/public/assets/images/rs485.jpg)
30
30
 
31
- ![alt text](https://github.com/ofernander/signalk-rec-bms/blob/main/public/assets/images/bms_serial.png)
31
+ ![alt text](https://raw.githubusercontent.com/ofernander/signalk-rec-bms/refs/heads/main/public/assets/images/bms_serial.png)
32
32
 
33
33
  ## Dependencies
34
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-rec-bms",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "SignalK plugin for REC-BMS",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
@@ -20,7 +20,7 @@ module.exports = {
20
20
  websocketURL: {
21
21
  title: "WebSocket URL",
22
22
  type: "string",
23
- default: "ws://192.168.88.10/ws"
23
+ default: "ws://WiFi_module_IP/ws"
24
24
  }
25
25
  }
26
26
  },
@@ -35,8 +35,8 @@ function parseLCD1Response(input) {
35
35
  current: packet.readFloatLE(12),
36
36
  temperature: packet.readFloatLE(16),
37
37
  packVoltage: packet.readFloatLE(20),
38
- soc: packet.readFloatLE(24) * 100,
39
- soh: packet.readFloatLE(28) * 100
38
+ soc: packet.readFloatLE(24),
39
+ soh: packet.readFloatLE(28)
40
40
  }
41
41
  };
42
42
  return result;
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  module.exports = function Derived(app, sendDeltaCallback, config) {
2
4
  app.debug("[DERIVED] Module instantiated with config: " + JSON.stringify(config));
3
5
  let voltage, current, ampHourUsed, capacity;
@@ -11,13 +13,29 @@ module.exports = function Derived(app, sendDeltaCallback, config) {
11
13
  if ([voltage, current, ampHourUsed, capacity].every(v => typeof v === 'number')) {
12
14
  const power = voltage * current;
13
15
  const ahRemaining = capacity - ampHourUsed;
16
+
17
+ const values = [
18
+ { path: prefix + 'power', value: power },
19
+ { path: prefix + 'ampHourRemaining', value: ahRemaining }
20
+ ];
21
+
22
+ if (current > 0 && ahRemaining < capacity) {
23
+ const missingAh = capacity - ahRemaining;
24
+ const timeToFull = (missingAh / current) * 3600;
25
+ values.push({ path: prefix + 'timeToFull', value: timeToFull });
26
+ values.push({ path: prefix + 'timeToEmpty', value: 0 });
27
+ } else if (current < 0 && ahRemaining > 0) {
28
+ const timeToEmpty = (ahRemaining / Math.abs(current)) * 3600;
29
+ values.push({ path: prefix + 'timeToFull', value: 0 });
30
+ values.push({ path: prefix + 'timeToEmpty', value: timeToEmpty });
31
+ } else {
32
+ // fallback when current is 0 or data is abnormal
33
+ values.push({ path: prefix + 'timeToFull', value: 0 });
34
+ values.push({ path: prefix + 'timeToEmpty', value: 0 });
35
+ }
36
+
14
37
  sendDeltaCallback({
15
- updates: [{
16
- values: [
17
- { path: prefix + 'power', value: power },
18
- { path: prefix + 'ampHourRemaining', value: ahRemaining }
19
- ]
20
- }]
38
+ updates: [{ values }]
21
39
  });
22
40
  }
23
41
  }
@@ -56,7 +74,7 @@ module.exports = function Derived(app, sendDeltaCallback, config) {
56
74
  });
57
75
 
58
76
  function trySendCellDiff() {
59
- const keys = Object.keys(cellVoltages).map(k => parseInt(k,10));
77
+ const keys = Object.keys(cellVoltages).map(k => parseInt(k, 10));
60
78
  if (keys.length === totalCells) {
61
79
  const vals = keys.map(i => cellVoltages[i]);
62
80
  const minV = Math.min(...vals);
@@ -68,6 +86,8 @@ module.exports = function Derived(app, sendDeltaCallback, config) {
68
86
  ]
69
87
  }]
70
88
  });
89
+ } else {
90
+ setTimeout(trySendCellDiff, 10);
71
91
  }
72
92
  }
73
93
 
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+
3
+ // Cache latest values across messages
4
+ let latestMaster = null;
5
+ let latestSettings = null;
6
+
7
+ module.exports = function(parsedData, prefix) {
8
+ const values = [];
9
+
10
+ // Update cache if master data is present
11
+ if (parsedData?.bms_array?.master) {
12
+ latestMaster = parsedData.bms_array.master;
13
+ }
14
+
15
+ // Update cache if settings data is present
16
+ if (parsedData?.type === "settings") {
17
+ latestSettings = parsedData;
18
+ }
19
+
20
+ // If we don't have both yet, don't compute anything
21
+ if (!latestMaster || !latestSettings) return values;
22
+
23
+ const master = latestMaster;
24
+ const settings = latestSettings;
25
+
26
+ // power = voltage * current
27
+ if (typeof master.vbat === "number" && typeof master.ibat === "number") {
28
+ values.push({
29
+ path: `${prefix}.power`,
30
+ value: master.vbat * master.ibat
31
+ });
32
+ }
33
+
34
+ // cellVoltageDifference = max - min
35
+ if (typeof master.maxcell === "number" && typeof master.mincell === "number") {
36
+ values.push({
37
+ path: `${prefix}.cellVoltageDifference`,
38
+ value: master.maxcell - master.mincell
39
+ });
40
+ }
41
+
42
+ // ampHourRemaining = capacity - ampHourUsed
43
+ if (typeof settings.capa === "number" && typeof settings.Ah === "number") {
44
+ values.push({
45
+ path: `${prefix}.ampHourRemaining`,
46
+ value: settings.capa - settings.Ah
47
+ });
48
+ }
49
+
50
+ // timeToFull / timeToEmpty in seconds
51
+ if (
52
+ typeof settings.capa === "number" &&
53
+ typeof settings.Ah === "number" &&
54
+ typeof master.ibat === "number"
55
+ ) {
56
+ const capacity = settings.capa;
57
+ const current = master.ibat;
58
+ const soc = typeof master.soc === "number" ? master.soc : null;
59
+ const remainingAh = (soc !== null) ? (soc / 100) * capacity : (capacity - settings.Ah);
60
+
61
+ if (current > 0 && remainingAh < capacity) {
62
+ const missingAh = capacity - remainingAh;
63
+ const timeToFull = (missingAh / current) * 3600;
64
+ values.push({ path: `${prefix}.timeToFull`, value: timeToFull });
65
+ values.push({ path: `${prefix}.timeToEmpty`, value: 0 });
66
+ } else if (current < 0 && remainingAh > 0) {
67
+ const timeToEmpty = (remainingAh / Math.abs(current)) * 3600;
68
+ values.push({ path: `${prefix}.timeToFull`, value: 0 });
69
+ values.push({ path: `${prefix}.timeToEmpty`, value: timeToEmpty });
70
+ } else {
71
+ // idle or insufficient current
72
+ values.push({ path: `${prefix}.timeToFull`, value: 0 });
73
+ values.push({ path: `${prefix}.timeToEmpty`, value: 0 });
74
+ }
75
+ }
76
+
77
+ return values;
78
+ };
@@ -8,11 +8,6 @@ module.exports = function(parsedData, prefix, config) {
8
8
  const settings = parsedData;
9
9
  const values = [];
10
10
 
11
- // Derived data - NOT FROM BMS
12
- const AhR = (parsedData.capa !== undefined && parsedData.Ah !== undefined)
13
- ? parsedData.capa - parsedData.Ah
14
- : null;
15
- values.push({ path: `${prefix}.ampHourRemaining`, value: AhR });
16
11
  // Data from WiFi module WebSocket under SETTINGS message
17
12
  values.push({ path: `${prefix}.address`, value: parsedData.addr });
18
13
  values.push({ path: `${prefix}.ampHourUsed`, value: parsedData.Ah });
@@ -23,15 +23,10 @@ module.exports = function(parsedData, prefix, config) {
23
23
  return null;
24
24
  }
25
25
 
26
- // Derived data - NOT FROM BMS
27
- const power = (master.vbat !== undefined && master.ibat !== undefined)
28
- ? master.vbat * master.ibat
29
- : null;
30
- values.push({ path: `${prefix}.power`, value: power });
31
26
  // Data from WiFi module WebSocket under SETTINGS message
32
27
  values.push({ path: `${prefix}.date`, value: master.date });
33
28
  values.push({ path: `${prefix}.time`, value: master.time });
34
- values.push({ path: `${prefix}.cellVoltageDifference`, value: master.maxcell - master.mincell });
29
+ //values.push({ path: `${prefix}.cellVoltageDifference`, value: master.maxcell - master.mincell });
35
30
  values.push({ path: `${prefix}.current`, value: master.ibat });
36
31
  values.push({ path: `${prefix}.errorAddress`, value: master.erro.addr });
37
32
  values.push({ path: `${prefix}.errorConnectionState`, value: master.erro.con_st });
@@ -42,7 +37,7 @@ module.exports = function(parsedData, prefix, config) {
42
37
  values.push({ path: `${prefix}.minCellVoltage`, value: master.mincell });
43
38
  values.push({ path: `${prefix}.stateOfCharge`, value: master.soc });
44
39
  values.push({ path: `${prefix}.stateOfHealth`, value: master.soh });
45
- values.push({ path: `${prefix}.timeRemaining`, value: extractMinutes(master.time_remaining) });
40
+ //values.push({ path: `${prefix}.timeRemaining`, value: extractMinutes(master.time_remaining) });
46
41
  values.push({ path: `${prefix}.voltage`, value: master.vbat });
47
42
  values.push({ path: `${prefix}.bmsTemperature`, value: slave.temp_bms });
48
43
  values.push({ path: `${prefix}.slaveAddress`, value: slave.address });
@@ -2,6 +2,7 @@
2
2
  const WebSocket = require('ws');
3
3
  const status = require('./status');
4
4
  const settings = require('./settings');
5
+ const derived = require('./derived');
5
6
  const schema = require('../schema');
6
7
 
7
8
  module.exports = function(app, publishDelta) {
@@ -84,6 +85,11 @@ module.exports = function(app, publishDelta) {
84
85
  }
85
86
  }
86
87
 
88
+ // Add derived values
89
+ const derivedValues = derived(parsedData, prefix) || [];
90
+ delta.updates[0].values.push(...derivedValues);
91
+
92
+ // Filter out null/undefined
87
93
  delta.updates[0].values = delta.updates[0].values.filter(item => item.value !== null && item.value !== undefined);
88
94
 
89
95
  if (delta.updates[0].values.length > 0) {
@@ -111,9 +117,8 @@ module.exports = function(app, publishDelta) {
111
117
  });
112
118
 
113
119
  ws.on('error', (error) => {
114
- app.error(`[WEBSOCKET] WebSocket error: ${error.message}`);
115
120
  app.setPluginStatus(`Error: ${error.message} - ERROR`);
116
- app.debug("[WEBSOCKET] WebSocket error occurred, terminating connection.");
121
+ app.debug(`[WEBSOCKET] WebSocket error: "${error.message}", terminating connection.`);
117
122
  clearInterval(pingInterval);
118
123
  if (ws) ws.terminate();
119
124
  if (shouldReconnect) {
Binary file
Binary file
package/public/index.html CHANGED
@@ -36,7 +36,7 @@
36
36
  </div>
37
37
  <div class="metric-box">
38
38
  <div class="metric-label">State of Charge</div>
39
- <div class="metric-value" id="soc">-- %</div>
39
+ <div class="metric-value" id="stateOfCharge">-- %</div>
40
40
  </div>
41
41
  <div class="metric-box">
42
42
  <div class="metric-label">Pack Voltage</div>
package/public/js/main.js CHANGED
@@ -14,7 +14,8 @@ class BMSSystem {
14
14
  voltage: null,
15
15
  current: null,
16
16
  power: null,
17
- soc: null,
17
+ stateOfCharge: null,
18
+ stateOfHealth: null,
18
19
  cellVoltages: {},
19
20
  cellResistances: {},
20
21
  cellVoltageDifference: null,
@@ -38,11 +39,12 @@ class BMSSystem {
38
39
 
39
40
  cacheDomElements() {
40
41
  const metrics = [
41
- 'voltage', 'current', 'power', 'soc', 'cellVoltageDifference',
42
- 'bmsTemperature', 'cellTemperature1', 'maxCellVoltage', 'minCellVoltage',
43
- 'ampHourRemaining', 'ampHourUsed', 'capacity'
42
+ 'voltage', 'current', 'power', 'stateOfCharge', 'stateOfHealth',
43
+ 'cellVoltageDifference', 'bmsTemperature', 'cellTemperature1',
44
+ 'maxCellVoltage', 'minCellVoltage', 'ampHourRemaining',
45
+ 'ampHourUsed', 'capacity'
44
46
  ];
45
-
47
+
46
48
  metrics.forEach(metric => {
47
49
  this.dom.metricElements[metric] = document.getElementById(metric);
48
50
  if (!this.dom.metricElements[metric]) {
@@ -54,7 +56,7 @@ class BMSSystem {
54
56
  setupTabSwitching() {
55
57
  const statusTab = document.getElementById('nav-status');
56
58
  const settingsTab = document.getElementById('nav-settings');
57
-
59
+
58
60
  if (!statusTab || !settingsTab) {
59
61
  console.warn("Tab elements not found");
60
62
  return;
@@ -68,7 +70,7 @@ class BMSSystem {
68
70
  switchView(view) {
69
71
  const statusView = document.getElementById('status-view');
70
72
  const settingsView = document.getElementById('settings-view');
71
-
73
+
72
74
  if (view === 'status') {
73
75
  statusView?.classList.add('active');
74
76
  settingsView?.classList.remove('active');
@@ -90,8 +92,23 @@ class BMSSystem {
90
92
  setValue(this.dom.metricElements.voltage, this.liveData.voltage, "V");
91
93
  setValue(this.dom.metricElements.current, this.liveData.current, "A");
92
94
  setValue(this.dom.metricElements.power, this.liveData.power, "W");
93
- setValue(this.dom.metricElements.soc, this.liveData.soc, "%");
94
- setValue(this.dom.metricElements.cellVoltageDifference, this.liveData.cellVoltageDifference, "V");
95
+
96
+ setValue(
97
+ this.dom.metricElements.stateOfCharge,
98
+ this.liveData.stateOfCharge !== null ? this.liveData.stateOfCharge * 100 : null,
99
+ "%"
100
+ );
101
+ setValue(
102
+ this.dom.metricElements.stateOfHealth,
103
+ this.liveData.stateOfHealth !== null ? this.liveData.stateOfHealth * 100 : null,
104
+ "%"
105
+ );
106
+
107
+ if (this.dom.metricElements.cellVoltageDifference) {
108
+ const v = this.liveData.cellVoltageDifference;
109
+ this.dom.metricElements.cellVoltageDifference.textContent = v !== null ? `${v.toFixed(4)}V` : "--";
110
+ }
111
+
95
112
  setValue(this.dom.metricElements.bmsTemperature, this.liveData.bmsTemperature, "°C");
96
113
  setValue(this.dom.metricElements.cellTemperature1, this.liveData.cellTemperature1, "°C");
97
114
  setValue(this.dom.metricElements.maxCellVoltage, this.liveData.maxCellVoltage, "V");