@blokcert/node-red-contrib-ocpp 1.1.1 → 1.2.1
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 +13 -0
- package/README.md +24 -35
- package/nodes/ocpp-command.html +28 -60
- package/nodes/ocpp-command.js +182 -122
- package/nodes/ocpp-in.html +15 -0
- package/nodes/ocpp-in.js +16 -0
- package/package.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
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
|
+
|
|
5
18
|
## [1.1.1] - 2026-02-12
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ msg.ocpp.errorDescription = "Action not supported";
|
|
|
89
89
|
|
|
90
90
|
### ocpp command
|
|
91
91
|
|
|
92
|
-
Sends CSMS-initiated commands to charging stations.
|
|
92
|
+
Sends CSMS-initiated commands to charging stations and receives responses.
|
|
93
93
|
|
|
94
94
|
**Supported Commands:**
|
|
95
95
|
- Reset
|
|
@@ -103,9 +103,29 @@ Sends CSMS-initiated commands to charging stations.
|
|
|
103
103
|
- ClearCache
|
|
104
104
|
- And more...
|
|
105
105
|
|
|
106
|
-
**
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
**Output:**
|
|
107
|
+
Single output that emits when the device responds (or when a timeout occurs).
|
|
108
|
+
|
|
109
|
+
**Response Message:**
|
|
110
|
+
```javascript
|
|
111
|
+
{
|
|
112
|
+
payload: { /* device response payload */ },
|
|
113
|
+
ocpp: {
|
|
114
|
+
messageId: "abc123",
|
|
115
|
+
action: "GetConfiguration", // Original command
|
|
116
|
+
tenant: "operator1",
|
|
117
|
+
deviceId: "charger001",
|
|
118
|
+
identity: "operator1:charger001",
|
|
119
|
+
response: true,
|
|
120
|
+
error: false, // true for CALLERROR or timeout
|
|
121
|
+
errorCode: null, // Set for errors
|
|
122
|
+
errorDescription: null, // Set for errors
|
|
123
|
+
sentAt: 1699999999999,
|
|
124
|
+
receivedAt: 1699999999150,
|
|
125
|
+
latency: 150 // Round-trip time in ms
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
109
129
|
|
|
110
130
|
**Example:**
|
|
111
131
|
```javascript
|
|
@@ -120,37 +140,6 @@ msg.payload = {
|
|
|
120
140
|
return msg;
|
|
121
141
|
```
|
|
122
142
|
|
|
123
|
-
### ocpp response
|
|
124
|
-
|
|
125
|
-
Routes CALLRESULT/CALLERROR responses back to the originating ocpp-command nodes.
|
|
126
|
-
|
|
127
|
-
**Features:**
|
|
128
|
-
- Automatically matches responses to pending commands
|
|
129
|
-
- Notifies ocpp-command nodes of responses
|
|
130
|
-
- Calculates round-trip latency
|
|
131
|
-
|
|
132
|
-
**Setup:**
|
|
133
|
-
Connect this node to an `ocpp in` node (optionally filtered by `_Response`) to receive device responses:
|
|
134
|
-
|
|
135
|
-
```
|
|
136
|
-
[ocpp in (filter: _Response)] → [ocpp response] → [debug]
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
**Output:**
|
|
140
|
-
```javascript
|
|
141
|
-
{
|
|
142
|
-
payload: { /* device response */ },
|
|
143
|
-
ocpp: {
|
|
144
|
-
messageId: "abc123",
|
|
145
|
-
action: "GetConfiguration", // Original command
|
|
146
|
-
response: true,
|
|
147
|
-
error: false,
|
|
148
|
-
latency: 150, // Round-trip time in ms
|
|
149
|
-
handled: true // Whether an ocpp-command node was notified
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
143
|
## Example Flow
|
|
155
144
|
|
|
156
145
|
```
|
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,110 @@ 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
|
+
subRedis.on('subscribe', (channel, count) => {
|
|
63
|
+
node.log(`Subscribe confirmed: ${channel} (total: ${count})`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Handle incoming messages
|
|
67
|
+
subRedis.on('message', (channel, message) => {
|
|
68
|
+
node.log(`Pub/Sub message received on ${channel}`);
|
|
69
|
+
try {
|
|
70
|
+
const data = JSON.parse(message);
|
|
71
|
+
const messageId = data.messageId || data.uniqueID;
|
|
72
|
+
|
|
73
|
+
if (!messageId) {
|
|
74
|
+
node.warn('Received response without messageId');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
node.log(`Looking up pending for messageId: ${messageId}`);
|
|
79
|
+
const pending = pendingCommands.get(messageId);
|
|
80
|
+
if (!pending) {
|
|
81
|
+
node.log(`No pending command found for ${messageId}, pendingCommands size: ${pendingCommands.size}`);
|
|
82
|
+
// Not our command, ignore
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Clear timeout
|
|
87
|
+
clearTimeout(pending.timeoutId);
|
|
88
|
+
pendingCommands.delete(messageId);
|
|
89
|
+
|
|
90
|
+
// Remove from Redis pending hash
|
|
91
|
+
const pendingKey = Keys.pending(pending.tenant, pending.deviceId);
|
|
92
|
+
redis.hdel(pendingKey, messageId).catch(() => {});
|
|
93
|
+
|
|
94
|
+
// Determine if error
|
|
95
|
+
const isError = data.messageType === 4 || data.error === true;
|
|
96
|
+
|
|
97
|
+
// Update status
|
|
98
|
+
node.status({
|
|
99
|
+
fill: isError ? 'red' : 'green',
|
|
100
|
+
shape: 'dot',
|
|
101
|
+
text: `${pending.action}: ${isError ? 'error' : 'ok'}`,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
node.log(`Found pending command for ${messageId}, sending to output`);
|
|
105
|
+
|
|
106
|
+
// Build output message with device response
|
|
107
|
+
const responseMsg = {
|
|
108
|
+
payload: data.payload || data,
|
|
109
|
+
ocpp: {
|
|
110
|
+
messageId,
|
|
111
|
+
action: pending.action,
|
|
112
|
+
tenant: pending.tenant,
|
|
113
|
+
deviceId: pending.deviceId,
|
|
114
|
+
identity: `${pending.tenant}:${pending.deviceId}`,
|
|
115
|
+
response: true,
|
|
116
|
+
error: isError,
|
|
117
|
+
errorCode: data.errorCode,
|
|
118
|
+
errorDescription: data.errorDescription,
|
|
119
|
+
sentAt: pending.sentAt,
|
|
120
|
+
receivedAt: Date.now(),
|
|
121
|
+
latency: Date.now() - pending.sentAt,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
node.send(responseMsg);
|
|
126
|
+
|
|
127
|
+
} catch (err) {
|
|
128
|
+
node.error(`Error processing response: ${err.message}`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
node.status({ fill: 'green', shape: 'ring', text: 'ready' });
|
|
133
|
+
|
|
134
|
+
} catch (err) {
|
|
135
|
+
node.error(`Failed to initialize subscriber: ${err.message}`);
|
|
136
|
+
node.status({ fill: 'red', shape: 'ring', text: 'sub error' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Initialize subscriber when Redis is ready
|
|
141
|
+
if (redis.status === 'ready') {
|
|
142
|
+
initSubscriber();
|
|
143
|
+
} else {
|
|
144
|
+
redis.once('ready', () => {
|
|
145
|
+
initSubscriber();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
40
149
|
/**
|
|
41
150
|
* Handle incoming messages
|
|
42
151
|
*/
|
|
@@ -65,6 +174,22 @@ module.exports = function (RED) {
|
|
|
65
174
|
// Build OCPP CALL message
|
|
66
175
|
const callMessage = buildCall(messageId, action, payload);
|
|
67
176
|
|
|
177
|
+
// Store pending command for response matching
|
|
178
|
+
const pendingKey = Keys.pending(tenant, deviceId);
|
|
179
|
+
const sentAt = Date.now();
|
|
180
|
+
|
|
181
|
+
// Store command info in Redis for response matching
|
|
182
|
+
// Include our serverId so WS Server knows where to send response
|
|
183
|
+
await redis.hset(pendingKey,
|
|
184
|
+
messageId, JSON.stringify({
|
|
185
|
+
action,
|
|
186
|
+
sentAt,
|
|
187
|
+
timeout: node.timeout,
|
|
188
|
+
serverId: nodeServerId, // This tells WS Server where to send response
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
await redis.expire(pendingKey, Math.ceil(node.timeout / 1000) + 10);
|
|
192
|
+
|
|
68
193
|
// Send to device
|
|
69
194
|
const outboundKey = Keys.outbound(tenant, deviceId);
|
|
70
195
|
await redis.xadd(
|
|
@@ -73,10 +198,11 @@ module.exports = function (RED) {
|
|
|
73
198
|
'*',
|
|
74
199
|
'id', messageId,
|
|
75
200
|
'payload', callMessage,
|
|
76
|
-
'timestamp',
|
|
201
|
+
'timestamp', sentAt.toString(),
|
|
202
|
+
'serverId', nodeServerId // Include serverId in outbound message
|
|
77
203
|
);
|
|
78
204
|
|
|
79
|
-
node.log(`Sent
|
|
205
|
+
node.log(`Sent ${action} to ${tenant}:${deviceId} (${messageId})`);
|
|
80
206
|
|
|
81
207
|
// Update status
|
|
82
208
|
node.status({
|
|
@@ -85,82 +211,52 @@ module.exports = function (RED) {
|
|
|
85
211
|
text: `${action} → ${tenant}:${deviceId}`,
|
|
86
212
|
});
|
|
87
213
|
|
|
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
|
-
});
|
|
128
|
-
|
|
129
|
-
// Send timeout error on second output
|
|
130
|
-
const timeoutMsg = {
|
|
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
|
-
};
|
|
214
|
+
// Set up timeout
|
|
215
|
+
const timeoutId = setTimeout(async () => {
|
|
216
|
+
pendingCommands.delete(messageId);
|
|
139
217
|
|
|
140
|
-
|
|
141
|
-
|
|
218
|
+
// Remove from Redis
|
|
219
|
+
await redis.hdel(pendingKey, messageId).catch(() => {});
|
|
142
220
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
action,
|
|
221
|
+
node.status({
|
|
222
|
+
fill: 'yellow',
|
|
223
|
+
shape: 'dot',
|
|
224
|
+
text: `timeout: ${action}`,
|
|
148
225
|
});
|
|
149
226
|
|
|
150
|
-
// Send
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
227
|
+
// Send timeout error
|
|
228
|
+
const timeoutMsg = {
|
|
229
|
+
payload: null,
|
|
230
|
+
ocpp: {
|
|
231
|
+
messageId,
|
|
232
|
+
action,
|
|
233
|
+
tenant,
|
|
234
|
+
deviceId,
|
|
235
|
+
identity: `${tenant}:${deviceId}`,
|
|
236
|
+
response: true,
|
|
237
|
+
error: true,
|
|
238
|
+
errorCode: 'Timeout',
|
|
239
|
+
errorDescription: `Command ${action} timed out after ${node.timeout}ms`,
|
|
240
|
+
sentAt,
|
|
241
|
+
receivedAt: Date.now(),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
158
245
|
if (send) {
|
|
159
|
-
send(
|
|
246
|
+
send(timeoutMsg);
|
|
160
247
|
} else {
|
|
161
|
-
node.send(
|
|
248
|
+
node.send(timeoutMsg);
|
|
162
249
|
}
|
|
163
|
-
}
|
|
250
|
+
}, node.timeout);
|
|
251
|
+
|
|
252
|
+
// Store for response matching
|
|
253
|
+
pendingCommands.set(messageId, {
|
|
254
|
+
timeoutId,
|
|
255
|
+
action,
|
|
256
|
+
tenant,
|
|
257
|
+
deviceId,
|
|
258
|
+
sentAt,
|
|
259
|
+
});
|
|
164
260
|
|
|
165
261
|
if (done) {
|
|
166
262
|
done();
|
|
@@ -176,63 +272,27 @@ module.exports = function (RED) {
|
|
|
176
272
|
}
|
|
177
273
|
});
|
|
178
274
|
|
|
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
275
|
// 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
|
-
}
|
|
276
|
+
node.status({ fill: 'grey', shape: 'ring', text: 'initializing' });
|
|
228
277
|
|
|
229
278
|
// Cleanup on close
|
|
230
|
-
node.on('close', (done) => {
|
|
279
|
+
node.on('close', async (done) => {
|
|
231
280
|
// Clear all pending timeouts
|
|
232
281
|
for (const [, pending] of pendingCommands) {
|
|
233
282
|
clearTimeout(pending.timeoutId);
|
|
234
283
|
}
|
|
235
284
|
pendingCommands.clear();
|
|
285
|
+
|
|
286
|
+
// Cleanup subscriber
|
|
287
|
+
if (subRedis) {
|
|
288
|
+
try {
|
|
289
|
+
await subRedis.unsubscribe();
|
|
290
|
+
await subRedis.quit();
|
|
291
|
+
} catch (e) {
|
|
292
|
+
// Ignore cleanup errors
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
236
296
|
done();
|
|
237
297
|
});
|
|
238
298
|
}
|
package/nodes/ocpp-in.html
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
autoAck: { value: true },
|
|
11
11
|
normalizePayload: { value: true },
|
|
12
12
|
filterAction: { value: '' },
|
|
13
|
+
includeResponses: { value: false },
|
|
13
14
|
},
|
|
14
15
|
inputs: 0,
|
|
15
16
|
outputs: 1,
|
|
@@ -58,6 +59,11 @@
|
|
|
58
59
|
<label for="node-input-filterAction"><i class="fa fa-filter"></i> Filter Action</label>
|
|
59
60
|
<input type="text" id="node-input-filterAction" placeholder="(all actions)">
|
|
60
61
|
</div>
|
|
62
|
+
<div class="form-row">
|
|
63
|
+
<label for="node-input-includeResponses"><i class="fa fa-reply"></i> Include Responses</label>
|
|
64
|
+
<input type="checkbox" id="node-input-includeResponses" style="width: auto; margin-left: 0;">
|
|
65
|
+
<span style="margin-left: 10px;">Include CallResult/CallError (normally handled by ocpp-command)</span>
|
|
66
|
+
</div>
|
|
61
67
|
</script>
|
|
62
68
|
|
|
63
69
|
<script type="text/html" data-help-name="ocpp-in">
|
|
@@ -108,12 +114,21 @@
|
|
|
108
114
|
<dt>Filter Action <span class="property-type">string</span></dt>
|
|
109
115
|
<dd>Optional. Only output messages matching this filter.
|
|
110
116
|
Leave empty to receive all messages.</dd>
|
|
117
|
+
|
|
118
|
+
<dt>Include Responses <span class="property-type">boolean</span></dt>
|
|
119
|
+
<dd>If enabled, includes CallResult (3) and CallError (4) messages.
|
|
120
|
+
By default, these are skipped because <code>ocpp-command</code> nodes handle responses via Pub/Sub.
|
|
121
|
+
Enable this only if you need to monitor or log all response traffic.</dd>
|
|
111
122
|
</dl>
|
|
112
123
|
|
|
113
124
|
<h3>Details</h3>
|
|
114
125
|
<p>This node reads from the <code>ws:inbound</code> Redis Stream using consumer groups,
|
|
115
126
|
allowing multiple Node-RED instances to share the message load.</p>
|
|
116
127
|
|
|
128
|
+
<p><strong>Note:</strong> By default, this node only outputs CALL messages (requests from charging stations).
|
|
129
|
+
Response messages (CallResult/CallError) are handled by <code>ocpp-command</code> nodes via Redis Pub/Sub.
|
|
130
|
+
Enable "Include Responses" only if you need to monitor or log all traffic.</p>
|
|
131
|
+
|
|
117
132
|
<h3>Filter Options</h3>
|
|
118
133
|
<p>The Filter Action field supports these values:</p>
|
|
119
134
|
<ul>
|
package/nodes/ocpp-in.js
CHANGED
|
@@ -24,6 +24,7 @@ module.exports = function (RED) {
|
|
|
24
24
|
node.autoAck = config.autoAck !== false; // Default true
|
|
25
25
|
node.normalizePayload = config.normalizePayload !== false; // Default true
|
|
26
26
|
node.filterAction = config.filterAction || ''; // Optional action filter
|
|
27
|
+
node.includeResponses = config.includeResponses === true; // Default false - skip CallResult/CallError
|
|
27
28
|
|
|
28
29
|
// State
|
|
29
30
|
let running = false;
|
|
@@ -111,6 +112,21 @@ module.exports = function (RED) {
|
|
|
111
112
|
let msg = parseStreamMessage(data);
|
|
112
113
|
msg.streamId = streamId;
|
|
113
114
|
|
|
115
|
+
// Skip CallResult (3) and CallError (4) messages by default
|
|
116
|
+
// These responses should be handled by ocpp-command nodes via Pub/Sub
|
|
117
|
+
// Only allow them through if explicitly configured with includeResponses option
|
|
118
|
+
// Check both: ocppMessageType (numeric: 3, 4) and messageType (string: 'result', 'error')
|
|
119
|
+
const isResponse = msg.ocppMessageType === 3 || msg.ocppMessageType === 4 ||
|
|
120
|
+
msg.messageType === 'result' || msg.messageType === 'error';
|
|
121
|
+
|
|
122
|
+
if (isResponse && !node.includeResponses) {
|
|
123
|
+
// Auto-ack response messages silently
|
|
124
|
+
if (node.autoAck) {
|
|
125
|
+
await redis.xack(streamKey, consumerGroup, streamId);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
// Apply action filter if configured
|
|
115
131
|
// Special filter values:
|
|
116
132
|
// - "_CallResult" - only CALLRESULT responses
|
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.1",
|
|
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
|
},
|