@blokcert/node-red-contrib-ocpp 1.1.0 → 1.2.0
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/CHANGELOG.md +18 -0
- package/nodes/ocpp-command.html +28 -60
- package/nodes/ocpp-command.js +173 -122
- package/nodes/ocpp-response.js +22 -6
- package/package.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.2.0] - 2026-02-12
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **BREAKING**: `ocpp-command` node now has single output (device response only)
|
|
9
|
+
- `ocpp-command` now subscribes to Redis Pub/Sub to receive responses directly
|
|
10
|
+
- Removed "Command Sent" output - node only outputs when device responds or times out
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
- **Removed `ocpp-response` node** - No longer needed, `ocpp-command` handles responses directly
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Responses from chargers now properly route back to `ocpp-command` node via WS Server
|
|
17
|
+
|
|
18
|
+
## [1.1.1] - 2026-02-12
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Fixed `ocpp-response` node to correctly find and notify `ocpp-command` nodes using `RED.nodes.getNode()` instead of `RED.nodes.eachNode()`
|
|
22
|
+
|
|
5
23
|
## [1.1.0] - 2026-02-12
|
|
6
24
|
|
|
7
25
|
### Added
|
package/nodes/ocpp-command.html
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
command: { value: '' },
|
|
9
9
|
timeout: { value: 30000, validate: RED.validators.number() },
|
|
10
10
|
streamMaxLen: { value: 1000, validate: RED.validators.number() },
|
|
11
|
-
waitForResponse: { value: true },
|
|
12
11
|
},
|
|
13
12
|
inputs: 1,
|
|
14
|
-
outputs:
|
|
15
|
-
outputLabels: ['
|
|
13
|
+
outputs: 1,
|
|
14
|
+
outputLabels: ['response'],
|
|
16
15
|
icon: 'font-awesome/fa-bolt',
|
|
17
16
|
paletteLabel: 'ocpp command',
|
|
18
17
|
label: function () {
|
|
@@ -75,11 +74,6 @@
|
|
|
75
74
|
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout (ms)</label>
|
|
76
75
|
<input type="number" id="node-input-timeout" placeholder="30000">
|
|
77
76
|
</div>
|
|
78
|
-
<div class="form-row">
|
|
79
|
-
<label for="node-input-waitForResponse"><i class="fa fa-reply"></i> Wait Response</label>
|
|
80
|
-
<input type="checkbox" id="node-input-waitForResponse" style="width: auto; margin-left: 0;">
|
|
81
|
-
<span style="margin-left: 10px;">Wait for device response (enables second output)</span>
|
|
82
|
-
</div>
|
|
83
77
|
<div class="form-row">
|
|
84
78
|
<label for="node-input-streamMaxLen"><i class="fa fa-list-ol"></i> Stream Max Len</label>
|
|
85
79
|
<input type="number" id="node-input-streamMaxLen" placeholder="1000">
|
|
@@ -87,7 +81,7 @@
|
|
|
87
81
|
</script>
|
|
88
82
|
|
|
89
83
|
<script type="text/html" data-help-name="ocpp-command">
|
|
90
|
-
<p>Sends CSMS-initiated commands to charging stations.</p>
|
|
84
|
+
<p>Sends CSMS-initiated commands to charging stations and outputs the device response.</p>
|
|
91
85
|
|
|
92
86
|
<h3>Inputs</h3>
|
|
93
87
|
<dl class="message-properties">
|
|
@@ -107,31 +101,23 @@
|
|
|
107
101
|
<dd>Custom message ID (auto-generated if not provided).</dd>
|
|
108
102
|
</dl>
|
|
109
103
|
|
|
110
|
-
<h3>
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
</
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<dd>True if this is a response</dd>
|
|
128
|
-
<dt>ocpp.error <span class="property-type">boolean</span></dt>
|
|
129
|
-
<dd>True if this is an error/timeout</dd>
|
|
130
|
-
<dt>ocpp.receivedAt <span class="property-type">number</span></dt>
|
|
131
|
-
<dd>Timestamp when response was received</dd>
|
|
132
|
-
</dl>
|
|
133
|
-
</li>
|
|
134
|
-
</ol>
|
|
104
|
+
<h3>Output</h3>
|
|
105
|
+
<dl class="message-properties">
|
|
106
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
107
|
+
<dd>Response payload from device (or null on timeout)</dd>
|
|
108
|
+
<dt>ocpp.messageId <span class="property-type">string</span></dt>
|
|
109
|
+
<dd>Message ID of the command</dd>
|
|
110
|
+
<dt>ocpp.action <span class="property-type">string</span></dt>
|
|
111
|
+
<dd>Command action that was sent</dd>
|
|
112
|
+
<dt>ocpp.response <span class="property-type">boolean</span></dt>
|
|
113
|
+
<dd>Always true for output messages</dd>
|
|
114
|
+
<dt>ocpp.error <span class="property-type">boolean</span></dt>
|
|
115
|
+
<dd>True if this is an error or timeout</dd>
|
|
116
|
+
<dt>ocpp.errorCode <span class="property-type">string</span></dt>
|
|
117
|
+
<dd>Error code (if error)</dd>
|
|
118
|
+
<dt>ocpp.latency <span class="property-type">number</span></dt>
|
|
119
|
+
<dd>Round-trip time in milliseconds</dd>
|
|
120
|
+
</dl>
|
|
135
121
|
|
|
136
122
|
<h3>Properties</h3>
|
|
137
123
|
<dl class="message-properties">
|
|
@@ -139,16 +125,14 @@
|
|
|
139
125
|
<dd>OCPP command to send. Can be overridden via <code>msg.ocpp.command</code>.</dd>
|
|
140
126
|
|
|
141
127
|
<dt>Timeout <span class="property-type">number</span></dt>
|
|
142
|
-
<dd>How long to wait for device response (ms)
|
|
143
|
-
|
|
144
|
-
<dt>Wait Response <span class="property-type">boolean</span></dt>
|
|
145
|
-
<dd>If enabled, waits for device response and outputs on second port.</dd>
|
|
128
|
+
<dd>How long to wait for device response (ms). Default: 30000</dd>
|
|
146
129
|
</dl>
|
|
147
130
|
|
|
148
131
|
<h3>Common Commands</h3>
|
|
149
132
|
<table>
|
|
150
133
|
<tr><th>Command</th><th>Description</th></tr>
|
|
151
134
|
<tr><td>Reset</td><td>Reboot the charging station</td></tr>
|
|
135
|
+
<tr><td>GetConfiguration</td><td>Get device configuration keys</td></tr>
|
|
152
136
|
<tr><td>RemoteStartTransaction</td><td>Start charging remotely (1.6)</td></tr>
|
|
153
137
|
<tr><td>RequestStartTransaction</td><td>Start charging remotely (2.0.1)</td></tr>
|
|
154
138
|
<tr><td>UnlockConnector</td><td>Unlock a connector</td></tr>
|
|
@@ -156,34 +140,18 @@
|
|
|
156
140
|
<tr><td>ChangeAvailability</td><td>Set connector availability</td></tr>
|
|
157
141
|
</table>
|
|
158
142
|
|
|
159
|
-
<h3>Example:
|
|
143
|
+
<h3>Example: GetConfiguration</h3>
|
|
160
144
|
<pre>
|
|
161
|
-
// For OCPP 1.6
|
|
162
145
|
msg.ocpp = {
|
|
163
|
-
tenant: "
|
|
146
|
+
tenant: "test",
|
|
164
147
|
deviceId: "charger001",
|
|
165
|
-
command: "
|
|
166
|
-
};
|
|
167
|
-
msg.payload = {
|
|
168
|
-
connectorId: 1,
|
|
169
|
-
idTag: "USER123"
|
|
148
|
+
command: "GetConfiguration"
|
|
170
149
|
};
|
|
150
|
+
msg.payload = {}; // Empty = get all keys
|
|
171
151
|
return msg;
|
|
172
152
|
|
|
173
|
-
//
|
|
174
|
-
msg.
|
|
175
|
-
tenant: "operator1",
|
|
176
|
-
deviceId: "station001",
|
|
177
|
-
command: "RequestStartTransaction"
|
|
178
|
-
};
|
|
179
|
-
msg.payload = {
|
|
180
|
-
evseId: 1,
|
|
181
|
-
idToken: {
|
|
182
|
-
idToken: "USER123",
|
|
183
|
-
type: "ISO14443"
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
return msg;
|
|
153
|
+
// Response will contain:
|
|
154
|
+
// msg.payload.configurationKey = [...]
|
|
187
155
|
</pre>
|
|
188
156
|
|
|
189
157
|
<h3>Example: Reset</h3>
|
package/nodes/ocpp-command.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OCPP Command Node
|
|
3
3
|
* Sends CSMS-initiated commands to charging stations
|
|
4
|
+
*
|
|
5
|
+
* Output: Device response (or timeout error)
|
|
4
6
|
*/
|
|
5
7
|
const { Keys } = require('../lib/redis-client');
|
|
6
8
|
const { buildCall } = require('../lib/ocpp-normalizer');
|
|
@@ -23,13 +25,16 @@ module.exports = function (RED) {
|
|
|
23
25
|
node.command = config.command || ''; // Can be overridden by msg
|
|
24
26
|
node.timeout = parseInt(config.timeout, 10) || 30000;
|
|
25
27
|
node.streamMaxLen = parseInt(config.streamMaxLen, 10) || 1000;
|
|
26
|
-
node.waitForResponse = config.waitForResponse !== false; // Default true
|
|
27
28
|
|
|
28
29
|
const redis = node.server.redis;
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
+
// Create a separate Redis connection for Pub/Sub
|
|
32
|
+
let subRedis = null;
|
|
31
33
|
const pendingCommands = new Map();
|
|
32
34
|
|
|
35
|
+
// Generate unique server ID for this node instance
|
|
36
|
+
const nodeServerId = `nodered-${node.id.slice(0, 8)}-${Date.now()}`;
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Generate unique message ID
|
|
35
40
|
*/
|
|
@@ -37,6 +42,101 @@ module.exports = function (RED) {
|
|
|
37
42
|
return crypto.randomUUID();
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Initialize Pub/Sub subscriber
|
|
47
|
+
*/
|
|
48
|
+
async function initSubscriber() {
|
|
49
|
+
try {
|
|
50
|
+
// Duplicate the Redis connection for Pub/Sub
|
|
51
|
+
subRedis = redis.duplicate();
|
|
52
|
+
|
|
53
|
+
subRedis.on('error', (err) => {
|
|
54
|
+
node.error(`Subscriber Redis error: ${err.message}`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Subscribe to our response channel
|
|
58
|
+
const respChannel = `ws:resp:${nodeServerId}`;
|
|
59
|
+
await subRedis.subscribe(respChannel);
|
|
60
|
+
node.log(`Subscribed to ${respChannel}`);
|
|
61
|
+
|
|
62
|
+
// Handle incoming messages
|
|
63
|
+
subRedis.on('message', (channel, message) => {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(message);
|
|
66
|
+
const messageId = data.messageId || data.uniqueID;
|
|
67
|
+
|
|
68
|
+
if (!messageId) {
|
|
69
|
+
node.warn('Received response without messageId');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pending = pendingCommands.get(messageId);
|
|
74
|
+
if (!pending) {
|
|
75
|
+
// Not our command, ignore
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Clear timeout
|
|
80
|
+
clearTimeout(pending.timeoutId);
|
|
81
|
+
pendingCommands.delete(messageId);
|
|
82
|
+
|
|
83
|
+
// Remove from Redis pending hash
|
|
84
|
+
const pendingKey = Keys.pending(pending.tenant, pending.deviceId);
|
|
85
|
+
redis.hdel(pendingKey, messageId).catch(() => {});
|
|
86
|
+
|
|
87
|
+
// Determine if error
|
|
88
|
+
const isError = data.messageType === 4 || data.error === true;
|
|
89
|
+
|
|
90
|
+
// Update status
|
|
91
|
+
node.status({
|
|
92
|
+
fill: isError ? 'red' : 'green',
|
|
93
|
+
shape: 'dot',
|
|
94
|
+
text: `${pending.action}: ${isError ? 'error' : 'ok'}`,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Build output message with device response
|
|
98
|
+
const responseMsg = {
|
|
99
|
+
payload: data.payload || data,
|
|
100
|
+
ocpp: {
|
|
101
|
+
messageId,
|
|
102
|
+
action: pending.action,
|
|
103
|
+
tenant: pending.tenant,
|
|
104
|
+
deviceId: pending.deviceId,
|
|
105
|
+
identity: `${pending.tenant}:${pending.deviceId}`,
|
|
106
|
+
response: true,
|
|
107
|
+
error: isError,
|
|
108
|
+
errorCode: data.errorCode,
|
|
109
|
+
errorDescription: data.errorDescription,
|
|
110
|
+
sentAt: pending.sentAt,
|
|
111
|
+
receivedAt: Date.now(),
|
|
112
|
+
latency: Date.now() - pending.sentAt,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
node.send(responseMsg);
|
|
117
|
+
|
|
118
|
+
} catch (err) {
|
|
119
|
+
node.error(`Error processing response: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
node.status({ fill: 'green', shape: 'ring', text: 'ready' });
|
|
124
|
+
|
|
125
|
+
} catch (err) {
|
|
126
|
+
node.error(`Failed to initialize subscriber: ${err.message}`);
|
|
127
|
+
node.status({ fill: 'red', shape: 'ring', text: 'sub error' });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Initialize subscriber when Redis is ready
|
|
132
|
+
if (redis.status === 'ready') {
|
|
133
|
+
initSubscriber();
|
|
134
|
+
} else {
|
|
135
|
+
redis.once('ready', () => {
|
|
136
|
+
initSubscriber();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
40
140
|
/**
|
|
41
141
|
* Handle incoming messages
|
|
42
142
|
*/
|
|
@@ -65,6 +165,22 @@ module.exports = function (RED) {
|
|
|
65
165
|
// Build OCPP CALL message
|
|
66
166
|
const callMessage = buildCall(messageId, action, payload);
|
|
67
167
|
|
|
168
|
+
// Store pending command for response matching
|
|
169
|
+
const pendingKey = Keys.pending(tenant, deviceId);
|
|
170
|
+
const sentAt = Date.now();
|
|
171
|
+
|
|
172
|
+
// Store command info in Redis for response matching
|
|
173
|
+
// Include our serverId so WS Server knows where to send response
|
|
174
|
+
await redis.hset(pendingKey,
|
|
175
|
+
messageId, JSON.stringify({
|
|
176
|
+
action,
|
|
177
|
+
sentAt,
|
|
178
|
+
timeout: node.timeout,
|
|
179
|
+
serverId: nodeServerId, // This tells WS Server where to send response
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
await redis.expire(pendingKey, Math.ceil(node.timeout / 1000) + 10);
|
|
183
|
+
|
|
68
184
|
// Send to device
|
|
69
185
|
const outboundKey = Keys.outbound(tenant, deviceId);
|
|
70
186
|
await redis.xadd(
|
|
@@ -73,10 +189,11 @@ module.exports = function (RED) {
|
|
|
73
189
|
'*',
|
|
74
190
|
'id', messageId,
|
|
75
191
|
'payload', callMessage,
|
|
76
|
-
'timestamp',
|
|
192
|
+
'timestamp', sentAt.toString(),
|
|
193
|
+
'serverId', nodeServerId // Include serverId in outbound message
|
|
77
194
|
);
|
|
78
195
|
|
|
79
|
-
node.log(`Sent
|
|
196
|
+
node.log(`Sent ${action} to ${tenant}:${deviceId} (${messageId})`);
|
|
80
197
|
|
|
81
198
|
// Update status
|
|
82
199
|
node.status({
|
|
@@ -85,82 +202,52 @@ module.exports = function (RED) {
|
|
|
85
202
|
text: `${action} → ${tenant}:${deviceId}`,
|
|
86
203
|
});
|
|
87
204
|
|
|
88
|
-
//
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
ocpp: {
|
|
92
|
-
...msg.ocpp,
|
|
93
|
-
messageId,
|
|
94
|
-
action,
|
|
95
|
-
tenant,
|
|
96
|
-
deviceId,
|
|
97
|
-
identity: `${tenant}:${deviceId}`,
|
|
98
|
-
sentAt: Date.now(),
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
if (node.waitForResponse) {
|
|
103
|
-
// Store pending command for response matching
|
|
104
|
-
const pendingKey = Keys.pending(tenant, deviceId);
|
|
105
|
-
|
|
106
|
-
// Store command info in Redis for response matching
|
|
107
|
-
await redis.hset(pendingKey,
|
|
108
|
-
messageId, JSON.stringify({
|
|
109
|
-
action,
|
|
110
|
-
sentAt: Date.now(),
|
|
111
|
-
timeout: node.timeout,
|
|
112
|
-
})
|
|
113
|
-
);
|
|
114
|
-
await redis.expire(pendingKey, Math.ceil(node.timeout / 1000) + 10);
|
|
115
|
-
|
|
116
|
-
// Set up timeout
|
|
117
|
-
const timeoutId = setTimeout(async () => {
|
|
118
|
-
pendingCommands.delete(messageId);
|
|
119
|
-
|
|
120
|
-
// Remove from Redis
|
|
121
|
-
await redis.hdel(pendingKey, messageId).catch(() => {});
|
|
122
|
-
|
|
123
|
-
node.status({
|
|
124
|
-
fill: 'yellow',
|
|
125
|
-
shape: 'dot',
|
|
126
|
-
text: `timeout: ${action}`,
|
|
127
|
-
});
|
|
205
|
+
// Set up timeout
|
|
206
|
+
const timeoutId = setTimeout(async () => {
|
|
207
|
+
pendingCommands.delete(messageId);
|
|
128
208
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
...outMsg,
|
|
132
|
-
ocpp: {
|
|
133
|
-
...outMsg.ocpp,
|
|
134
|
-
error: true,
|
|
135
|
-
errorCode: 'Timeout',
|
|
136
|
-
errorDescription: `Command ${action} timed out after ${node.timeout}ms`,
|
|
137
|
-
},
|
|
138
|
-
};
|
|
209
|
+
// Remove from Redis
|
|
210
|
+
await redis.hdel(pendingKey, messageId).catch(() => {});
|
|
139
211
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
pendingCommands.set(messageId, {
|
|
145
|
-
timeoutId,
|
|
146
|
-
msg: outMsg,
|
|
147
|
-
action,
|
|
212
|
+
node.status({
|
|
213
|
+
fill: 'yellow',
|
|
214
|
+
shape: 'dot',
|
|
215
|
+
text: `timeout: ${action}`,
|
|
148
216
|
});
|
|
149
217
|
|
|
150
|
-
// Send
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
218
|
+
// Send timeout error
|
|
219
|
+
const timeoutMsg = {
|
|
220
|
+
payload: null,
|
|
221
|
+
ocpp: {
|
|
222
|
+
messageId,
|
|
223
|
+
action,
|
|
224
|
+
tenant,
|
|
225
|
+
deviceId,
|
|
226
|
+
identity: `${tenant}:${deviceId}`,
|
|
227
|
+
response: true,
|
|
228
|
+
error: true,
|
|
229
|
+
errorCode: 'Timeout',
|
|
230
|
+
errorDescription: `Command ${action} timed out after ${node.timeout}ms`,
|
|
231
|
+
sentAt,
|
|
232
|
+
receivedAt: Date.now(),
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
158
236
|
if (send) {
|
|
159
|
-
send(
|
|
237
|
+
send(timeoutMsg);
|
|
160
238
|
} else {
|
|
161
|
-
node.send(
|
|
239
|
+
node.send(timeoutMsg);
|
|
162
240
|
}
|
|
163
|
-
}
|
|
241
|
+
}, node.timeout);
|
|
242
|
+
|
|
243
|
+
// Store for response matching
|
|
244
|
+
pendingCommands.set(messageId, {
|
|
245
|
+
timeoutId,
|
|
246
|
+
action,
|
|
247
|
+
tenant,
|
|
248
|
+
deviceId,
|
|
249
|
+
sentAt,
|
|
250
|
+
});
|
|
164
251
|
|
|
165
252
|
if (done) {
|
|
166
253
|
done();
|
|
@@ -176,63 +263,27 @@ module.exports = function (RED) {
|
|
|
176
263
|
}
|
|
177
264
|
});
|
|
178
265
|
|
|
179
|
-
/**
|
|
180
|
-
* Handle response messages (can be connected from another ocpp-in node filtering for responses)
|
|
181
|
-
*/
|
|
182
|
-
node.handleResponse = function (messageId, response, isError) {
|
|
183
|
-
const pending = pendingCommands.get(messageId);
|
|
184
|
-
if (!pending) {
|
|
185
|
-
return false; // Not our command
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Clear timeout
|
|
189
|
-
clearTimeout(pending.timeoutId);
|
|
190
|
-
pendingCommands.delete(messageId);
|
|
191
|
-
|
|
192
|
-
// Update status
|
|
193
|
-
node.status({
|
|
194
|
-
fill: isError ? 'red' : 'green',
|
|
195
|
-
shape: 'dot',
|
|
196
|
-
text: `${pending.action}: ${isError ? 'error' : 'response'}`,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// Send response on second output
|
|
200
|
-
const responseMsg = {
|
|
201
|
-
...pending.msg,
|
|
202
|
-
payload: response,
|
|
203
|
-
ocpp: {
|
|
204
|
-
...pending.msg.ocpp,
|
|
205
|
-
response: true,
|
|
206
|
-
error: isError,
|
|
207
|
-
receivedAt: Date.now(),
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
node.send([null, responseMsg]);
|
|
212
|
-
return true;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
266
|
// Initial status
|
|
216
|
-
node.status({ fill: 'grey', shape: 'ring', text: '
|
|
217
|
-
|
|
218
|
-
// Update status when Redis is ready
|
|
219
|
-
if (redis) {
|
|
220
|
-
redis.once('ready', () => {
|
|
221
|
-
node.status({ fill: 'green', shape: 'ring', text: 'connected' });
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
if (redis.status === 'ready') {
|
|
225
|
-
node.status({ fill: 'green', shape: 'ring', text: 'connected' });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
267
|
+
node.status({ fill: 'grey', shape: 'ring', text: 'initializing' });
|
|
228
268
|
|
|
229
269
|
// Cleanup on close
|
|
230
|
-
node.on('close', (done) => {
|
|
270
|
+
node.on('close', async (done) => {
|
|
231
271
|
// Clear all pending timeouts
|
|
232
272
|
for (const [, pending] of pendingCommands) {
|
|
233
273
|
clearTimeout(pending.timeoutId);
|
|
234
274
|
}
|
|
235
275
|
pendingCommands.clear();
|
|
276
|
+
|
|
277
|
+
// Cleanup subscriber
|
|
278
|
+
if (subRedis) {
|
|
279
|
+
try {
|
|
280
|
+
await subRedis.unsubscribe();
|
|
281
|
+
await subRedis.quit();
|
|
282
|
+
} catch (e) {
|
|
283
|
+
// Ignore cleanup errors
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
236
287
|
done();
|
|
237
288
|
});
|
|
238
289
|
}
|
package/nodes/ocpp-response.js
CHANGED
|
@@ -81,16 +81,32 @@ module.exports = function (RED) {
|
|
|
81
81
|
|
|
82
82
|
// Find and notify all ocpp-command nodes
|
|
83
83
|
let handled = false;
|
|
84
|
+
|
|
85
|
+
// Get all flows and find ocpp-command nodes
|
|
86
|
+
const allNodes = [];
|
|
84
87
|
RED.nodes.eachNode((n) => {
|
|
85
|
-
if (n.type === 'ocpp-command'
|
|
86
|
-
|
|
87
|
-
const wasHandled = n.handleResponse(messageId, msg.payload, isError);
|
|
88
|
-
if (wasHandled) {
|
|
89
|
-
handled = true;
|
|
90
|
-
}
|
|
88
|
+
if (n.type === 'ocpp-command') {
|
|
89
|
+
allNodes.push(n.id);
|
|
91
90
|
}
|
|
92
91
|
});
|
|
93
92
|
|
|
93
|
+
// Try to get the runtime node instance and call handleResponse
|
|
94
|
+
for (const nodeId of allNodes) {
|
|
95
|
+
try {
|
|
96
|
+
const commandNode = RED.nodes.getNode(nodeId);
|
|
97
|
+
if (commandNode && typeof commandNode.handleResponse === 'function') {
|
|
98
|
+
const wasHandled = commandNode.handleResponse(messageId, msg.payload, isError);
|
|
99
|
+
if (wasHandled) {
|
|
100
|
+
handled = true;
|
|
101
|
+
node.log(`Response ${messageId} handled by ocpp-command node ${nodeId}`);
|
|
102
|
+
break; // Only one node should handle each response
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
node.warn(`Error calling handleResponse on node ${nodeId}: ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
// Update status
|
|
95
111
|
if (handled) {
|
|
96
112
|
node.status({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blokcert/node-red-contrib-ocpp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Node-RED nodes for OCPP (Open Charge Point Protocol) message handling via Redis Streams",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"ocpp-in": "nodes/ocpp-in.js",
|
|
23
23
|
"ocpp-out": "nodes/ocpp-out.js",
|
|
24
24
|
"ocpp-command": "nodes/ocpp-command.js",
|
|
25
|
-
"ocpp-response": "nodes/ocpp-response.js",
|
|
26
25
|
"ocpp-config": "nodes/ocpp-config.js"
|
|
27
26
|
}
|
|
28
27
|
},
|