@blokcert/node-red-contrib-ocpp 1.0.2 → 1.1.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 +41 -0
- package/README.md +46 -2
- package/lib/ocpp-normalizer.js +18 -3
- package/nodes/ocpp-in.html +17 -5
- package/nodes/ocpp-in.js +40 -7
- package/nodes/ocpp-response.html +98 -0
- package/nodes/ocpp-response.js +161 -0
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.1.0] - 2026-02-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **New `ocpp-response` node** - Routes CALLRESULT/CALLERROR responses back to the originating ocpp-command nodes
|
|
9
|
+
- Response message support in `ocpp-in` node with `messageType` and `ocppMessageType` fields
|
|
10
|
+
- New filter options in `ocpp-in`:
|
|
11
|
+
- `_CallResult` - Filter for only CALLRESULT responses
|
|
12
|
+
- `_CallError` - Filter for only CALLERROR responses
|
|
13
|
+
- `_Response` - Filter for both response types
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Fixed parsing of CALLRESULT/CALLERROR messages (previously showed as "[Unknown]")
|
|
17
|
+
- Response messages now correctly set `action` to `_CallResult` or `_CallError` for routing
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- `ocpp-in` status now shows descriptive text for response messages (e.g., "Response abc123 from test:charger001")
|
|
21
|
+
|
|
22
|
+
## [1.0.2] - 2026-02-10
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Minor bug fixes
|
|
26
|
+
|
|
27
|
+
## [1.0.1] - 2026-02-08
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Initial bug fixes after release
|
|
31
|
+
|
|
32
|
+
## [1.0.0] - 2026-02-05
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Initial release
|
|
36
|
+
- `ocpp-in` node for receiving OCPP messages via Redis Streams
|
|
37
|
+
- `ocpp-out` node for sending OCPP responses
|
|
38
|
+
- `ocpp-command` node for sending CSMS-initiated commands
|
|
39
|
+
- `ocpp-config` node for Redis connection configuration
|
|
40
|
+
- OCPP 1.6 and 2.0.1 support with automatic normalization/denormalization
|
|
41
|
+
- Consumer group support for horizontal scaling
|
package/README.md
CHANGED
|
@@ -34,16 +34,25 @@ Receives OCPP messages from charging stations via Redis Streams.
|
|
|
34
34
|
**Features:**
|
|
35
35
|
- Consumer group support for load balancing
|
|
36
36
|
- Optional payload normalization (OCPP 1.6/2.0.1 → unified format)
|
|
37
|
-
- Filter by action type
|
|
37
|
+
- Filter by action type or response type
|
|
38
38
|
- Auto-acknowledge messages
|
|
39
39
|
|
|
40
|
+
**Filter Options:**
|
|
41
|
+
- Empty: Receive all messages (CALL requests + responses)
|
|
42
|
+
- Action name (e.g., `BootNotification`): Only CALL requests with this action
|
|
43
|
+
- `_CallResult`: Only CALLRESULT responses
|
|
44
|
+
- `_CallError`: Only CALLERROR responses
|
|
45
|
+
- `_Response`: Both CALLRESULT and CALLERROR responses
|
|
46
|
+
|
|
40
47
|
**Output:**
|
|
41
48
|
```javascript
|
|
42
49
|
{
|
|
43
50
|
payload: { /* OCPP message payload */ },
|
|
44
51
|
ocpp: {
|
|
45
52
|
messageId: "abc123",
|
|
46
|
-
action: "BootNotification",
|
|
53
|
+
action: "BootNotification", // or "_CallResult" / "_CallError" for responses
|
|
54
|
+
messageType: "call", // "call", "result", or "error"
|
|
55
|
+
ocppMessageType: 2, // 2=CALL, 3=CALLRESULT, 4=CALLERROR
|
|
47
56
|
version: "1.6",
|
|
48
57
|
tenant: "operator1",
|
|
49
58
|
deviceId: "charger001",
|
|
@@ -94,6 +103,10 @@ Sends CSMS-initiated commands to charging stations.
|
|
|
94
103
|
- ClearCache
|
|
95
104
|
- And more...
|
|
96
105
|
|
|
106
|
+
**Outputs:**
|
|
107
|
+
- Output 1: Command sent confirmation
|
|
108
|
+
- Output 2: Device response or timeout error (when "Wait for Response" is enabled)
|
|
109
|
+
|
|
97
110
|
**Example:**
|
|
98
111
|
```javascript
|
|
99
112
|
msg.ocpp = {
|
|
@@ -107,6 +120,37 @@ msg.payload = {
|
|
|
107
120
|
return msg;
|
|
108
121
|
```
|
|
109
122
|
|
|
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
|
+
|
|
110
154
|
## Example Flow
|
|
111
155
|
|
|
112
156
|
```
|
package/lib/ocpp-normalizer.js
CHANGED
|
@@ -22,7 +22,7 @@ const MessageType = {
|
|
|
22
22
|
function parseStreamMessage(streamData) {
|
|
23
23
|
const { tenant, deviceId, timestamp } = streamData;
|
|
24
24
|
|
|
25
|
-
// Parse metadata (contains action, version, serverID)
|
|
25
|
+
// Parse metadata (contains action, version, serverID, type)
|
|
26
26
|
let metadata = {};
|
|
27
27
|
if (streamData.metadata) {
|
|
28
28
|
try {
|
|
@@ -47,8 +47,21 @@ function parseStreamMessage(streamData) {
|
|
|
47
47
|
// Extract messageId from payload.uniqueID or streamData.id
|
|
48
48
|
const messageId = parsedPayload?.uniqueID || streamData.id;
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
// Determine message type from metadata (call, result, error)
|
|
51
|
+
// messageType in payload contains the OCPP numeric type (2=CALL, 3=CALLRESULT, 4=CALLERROR)
|
|
52
|
+
const msgType = metadata.type || 'call';
|
|
53
|
+
const ocppMessageType = parsedPayload?.messageType || MessageType.CALL;
|
|
54
|
+
|
|
55
|
+
// Extract action from metadata or payload (only for CALL messages)
|
|
56
|
+
let action = metadata.action || parsedPayload?.action;
|
|
57
|
+
|
|
58
|
+
// For result/error messages, action is not present in the message
|
|
59
|
+
// We mark them with special action names for routing
|
|
60
|
+
if (msgType === 'result' || ocppMessageType === MessageType.CALLRESULT) {
|
|
61
|
+
action = action || '_CallResult';
|
|
62
|
+
} else if (msgType === 'error' || ocppMessageType === MessageType.CALLERROR) {
|
|
63
|
+
action = action || '_CallError';
|
|
64
|
+
}
|
|
52
65
|
|
|
53
66
|
// Extract actual payload content
|
|
54
67
|
const actualPayload = parsedPayload?.payload || parsedPayload;
|
|
@@ -62,6 +75,8 @@ function parseStreamMessage(streamData) {
|
|
|
62
75
|
tenant,
|
|
63
76
|
deviceId,
|
|
64
77
|
action,
|
|
78
|
+
messageType: msgType, // 'call', 'result', or 'error'
|
|
79
|
+
ocppMessageType, // 2, 3, or 4
|
|
65
80
|
payload: actualPayload,
|
|
66
81
|
version,
|
|
67
82
|
protocol,
|
package/nodes/ocpp-in.html
CHANGED
|
@@ -72,7 +72,9 @@
|
|
|
72
72
|
<dd>OCPP message metadata:
|
|
73
73
|
<ul>
|
|
74
74
|
<li><code>messageId</code> - Unique message ID</li>
|
|
75
|
-
<li><code>action</code> - OCPP action (e.g., BootNotification
|
|
75
|
+
<li><code>action</code> - OCPP action (e.g., BootNotification) or <code>_CallResult</code>/<code>_CallError</code> for responses</li>
|
|
76
|
+
<li><code>messageType</code> - "call", "result", or "error"</li>
|
|
77
|
+
<li><code>ocppMessageType</code> - Numeric OCPP message type (2=CALL, 3=CALLRESULT, 4=CALLERROR)</li>
|
|
76
78
|
<li><code>version</code> - OCPP version (1.6 or 2.0.1)</li>
|
|
77
79
|
<li><code>tenant</code> - Operator/tenant identifier</li>
|
|
78
80
|
<li><code>deviceId</code> - Charger/station ID</li>
|
|
@@ -104,16 +106,26 @@
|
|
|
104
106
|
This allows your flow to handle both versions with the same logic.</dd>
|
|
105
107
|
|
|
106
108
|
<dt>Filter Action <span class="property-type">string</span></dt>
|
|
107
|
-
<dd>Optional. Only output messages
|
|
108
|
-
Leave empty to receive all
|
|
109
|
+
<dd>Optional. Only output messages matching this filter.
|
|
110
|
+
Leave empty to receive all messages.</dd>
|
|
109
111
|
</dl>
|
|
110
112
|
|
|
111
113
|
<h3>Details</h3>
|
|
112
114
|
<p>This node reads from the <code>ws:inbound</code> Redis Stream using consumer groups,
|
|
113
115
|
allowing multiple Node-RED instances to share the message load.</p>
|
|
114
116
|
|
|
115
|
-
<h3>
|
|
116
|
-
<p>
|
|
117
|
+
<h3>Filter Options</h3>
|
|
118
|
+
<p>The Filter Action field supports these values:</p>
|
|
119
|
+
<ul>
|
|
120
|
+
<li><strong>Empty</strong> - Receive all messages (CALL requests + responses)</li>
|
|
121
|
+
<li><strong>Action name</strong> (e.g., <code>BootNotification</code>) - Only receive CALL requests with this action</li>
|
|
122
|
+
<li><code>_CallResult</code> - Only receive CALLRESULT responses</li>
|
|
123
|
+
<li><code>_CallError</code> - Only receive CALLERROR responses</li>
|
|
124
|
+
<li><code>_Response</code> - Receive both CALLRESULT and CALLERROR responses</li>
|
|
125
|
+
</ul>
|
|
126
|
+
|
|
127
|
+
<h3>Common OCPP Actions</h3>
|
|
128
|
+
<p>Actions that can be filtered:</p>
|
|
117
129
|
<ul>
|
|
118
130
|
<li><code>BootNotification</code></li>
|
|
119
131
|
<li><code>Heartbeat</code></li>
|
package/nodes/ocpp-in.js
CHANGED
|
@@ -112,12 +112,35 @@ module.exports = function (RED) {
|
|
|
112
112
|
msg.streamId = streamId;
|
|
113
113
|
|
|
114
114
|
// Apply action filter if configured
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
// Special filter values:
|
|
116
|
+
// - "_CallResult" - only CALLRESULT responses
|
|
117
|
+
// - "_CallError" - only CALLERROR responses
|
|
118
|
+
// - "_Response" - both CALLRESULT and CALLERROR
|
|
119
|
+
// - Any other value filters CALL messages by action name
|
|
120
|
+
if (node.filterAction) {
|
|
121
|
+
const filter = node.filterAction;
|
|
122
|
+
const isResult = msg.messageType === 'result';
|
|
123
|
+
const isError = msg.messageType === 'error';
|
|
124
|
+
|
|
125
|
+
let shouldPass = false;
|
|
126
|
+
if (filter === '_CallResult') {
|
|
127
|
+
shouldPass = isResult;
|
|
128
|
+
} else if (filter === '_CallError') {
|
|
129
|
+
shouldPass = isError;
|
|
130
|
+
} else if (filter === '_Response') {
|
|
131
|
+
shouldPass = isResult || isError;
|
|
132
|
+
} else {
|
|
133
|
+
// Filter CALL messages by action, pass through responses
|
|
134
|
+
shouldPass = msg.action === filter;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!shouldPass) {
|
|
138
|
+
// Auto-ack filtered messages if autoAck is enabled
|
|
139
|
+
if (node.autoAck) {
|
|
140
|
+
await redis.xack(streamKey, consumerGroup, streamId);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
119
143
|
}
|
|
120
|
-
return;
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
// Normalize payload if enabled
|
|
@@ -131,6 +154,8 @@ module.exports = function (RED) {
|
|
|
131
154
|
ocpp: {
|
|
132
155
|
messageId: msg.messageId,
|
|
133
156
|
action: msg.action,
|
|
157
|
+
messageType: msg.messageType, // 'call', 'result', or 'error'
|
|
158
|
+
ocppMessageType: msg.ocppMessageType, // 2, 3, or 4
|
|
134
159
|
version: msg.version,
|
|
135
160
|
protocol: msg.protocol,
|
|
136
161
|
tenant: msg.tenant,
|
|
@@ -143,11 +168,19 @@ module.exports = function (RED) {
|
|
|
143
168
|
_msgid: RED.util.generateId(),
|
|
144
169
|
};
|
|
145
170
|
|
|
146
|
-
// Update status
|
|
171
|
+
// Update status - show different text for call vs result/error
|
|
172
|
+
let statusText;
|
|
173
|
+
if (msg.messageType === 'result') {
|
|
174
|
+
statusText = `Response ${msg.messageId?.slice(0, 8)} from ${msg.identity}`;
|
|
175
|
+
} else if (msg.messageType === 'error') {
|
|
176
|
+
statusText = `Error ${msg.messageId?.slice(0, 8)} from ${msg.identity}`;
|
|
177
|
+
} else {
|
|
178
|
+
statusText = `${msg.action} from ${msg.identity}`;
|
|
179
|
+
}
|
|
147
180
|
node.status({
|
|
148
181
|
fill: 'green',
|
|
149
182
|
shape: 'dot',
|
|
150
|
-
text:
|
|
183
|
+
text: statusText,
|
|
151
184
|
});
|
|
152
185
|
|
|
153
186
|
// Send to output
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('ocpp-response', {
|
|
3
|
+
category: 'OCPP',
|
|
4
|
+
color: '#89D689',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
server: { value: '', type: 'ocpp-config', required: true },
|
|
8
|
+
},
|
|
9
|
+
inputs: 1,
|
|
10
|
+
outputs: 1,
|
|
11
|
+
icon: 'font-awesome/fa-reply',
|
|
12
|
+
label: function () {
|
|
13
|
+
return this.name || 'ocpp response';
|
|
14
|
+
},
|
|
15
|
+
paletteLabel: 'ocpp response',
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script type="text/html" data-template-name="ocpp-response">
|
|
20
|
+
<div class="form-row">
|
|
21
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
22
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
23
|
+
</div>
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-server"><i class="fa fa-server"></i> Server</label>
|
|
26
|
+
<input type="text" id="node-input-server">
|
|
27
|
+
</div>
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script type="text/html" data-help-name="ocpp-response">
|
|
31
|
+
<p>Routes CALLRESULT/CALLERROR responses back to the originating ocpp-command nodes.</p>
|
|
32
|
+
|
|
33
|
+
<h3>Overview</h3>
|
|
34
|
+
<p>When you send a command to a charging station using <code>ocpp-command</code>,
|
|
35
|
+
the station will respond with a CALLRESULT (success) or CALLERROR (failure).</p>
|
|
36
|
+
<p>This node routes those responses back to the correct <code>ocpp-command</code> node
|
|
37
|
+
that originated the request.</p>
|
|
38
|
+
|
|
39
|
+
<h3>Setup</h3>
|
|
40
|
+
<p>Connect this node to an <code>ocpp-in</code> node. When a response message arrives,
|
|
41
|
+
this node will:</p>
|
|
42
|
+
<ol>
|
|
43
|
+
<li>Check if it's a CALLRESULT or CALLERROR message</li>
|
|
44
|
+
<li>Look up the pending command in Redis</li>
|
|
45
|
+
<li>Find and notify the originating <code>ocpp-command</code> node</li>
|
|
46
|
+
<li>Pass through the response for additional processing</li>
|
|
47
|
+
</ol>
|
|
48
|
+
|
|
49
|
+
<h3>Inputs</h3>
|
|
50
|
+
<dl class="message-properties">
|
|
51
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
52
|
+
<dd>The response payload from the charging station.</dd>
|
|
53
|
+
|
|
54
|
+
<dt>ocpp.messageId <span class="property-type">string</span></dt>
|
|
55
|
+
<dd>The unique message ID matching the original command.</dd>
|
|
56
|
+
|
|
57
|
+
<dt>ocpp.messageType <span class="property-type">string</span></dt>
|
|
58
|
+
<dd>"result" for CALLRESULT, "error" for CALLERROR.</dd>
|
|
59
|
+
</dl>
|
|
60
|
+
|
|
61
|
+
<h3>Outputs</h3>
|
|
62
|
+
<dl class="message-properties">
|
|
63
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
64
|
+
<dd>The response payload (unchanged).</dd>
|
|
65
|
+
|
|
66
|
+
<dt>ocpp.action <span class="property-type">string</span></dt>
|
|
67
|
+
<dd>The original command action (e.g., "GetConfiguration").</dd>
|
|
68
|
+
|
|
69
|
+
<dt>ocpp.response <span class="property-type">boolean</span></dt>
|
|
70
|
+
<dd>Always true for responses.</dd>
|
|
71
|
+
|
|
72
|
+
<dt>ocpp.error <span class="property-type">boolean</span></dt>
|
|
73
|
+
<dd>True if this was a CALLERROR response.</dd>
|
|
74
|
+
|
|
75
|
+
<dt>ocpp.handled <span class="property-type">boolean</span></dt>
|
|
76
|
+
<dd>True if an ocpp-command node was found and notified.</dd>
|
|
77
|
+
|
|
78
|
+
<dt>ocpp.latency <span class="property-type">number</span></dt>
|
|
79
|
+
<dd>Round-trip time in milliseconds.</dd>
|
|
80
|
+
</dl>
|
|
81
|
+
|
|
82
|
+
<h3>Typical Flow</h3>
|
|
83
|
+
<pre>
|
|
84
|
+
[inject] → [ocpp-command] → (sent to device)
|
|
85
|
+
↘ [debug: command sent]
|
|
86
|
+
|
|
87
|
+
[ocpp-in] → [ocpp-response] → [debug: response received]
|
|
88
|
+
↓
|
|
89
|
+
(notifies ocpp-command)
|
|
90
|
+
↓
|
|
91
|
+
[ocpp-command second output] → [debug: command response]
|
|
92
|
+
</pre>
|
|
93
|
+
|
|
94
|
+
<h3>References</h3>
|
|
95
|
+
<ul>
|
|
96
|
+
<li><a href="https://www.openchargealliance.org/">Open Charge Alliance</a></li>
|
|
97
|
+
</ul>
|
|
98
|
+
</script>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OCPP Response Router Node
|
|
3
|
+
* Routes CALLRESULT/CALLERROR responses back to the originating ocpp-command nodes
|
|
4
|
+
*
|
|
5
|
+
* Connect this node's input to an ocpp-in node that receives response messages.
|
|
6
|
+
* It will automatically find and notify the corresponding ocpp-command node.
|
|
7
|
+
*/
|
|
8
|
+
const { Keys } = require('../lib/redis-client');
|
|
9
|
+
const { MessageType } = require('../lib/ocpp-normalizer');
|
|
10
|
+
|
|
11
|
+
module.exports = function (RED) {
|
|
12
|
+
function OCPPResponseNode(config) {
|
|
13
|
+
RED.nodes.createNode(this, config);
|
|
14
|
+
const node = this;
|
|
15
|
+
|
|
16
|
+
// Get config node
|
|
17
|
+
node.server = RED.nodes.getNode(config.server);
|
|
18
|
+
if (!node.server) {
|
|
19
|
+
node.error('No OCPP config node configured');
|
|
20
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no config' });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const redis = node.server.redis;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Handle incoming response messages from ocpp-in
|
|
28
|
+
*/
|
|
29
|
+
node.on('input', async (msg, send, done) => {
|
|
30
|
+
try {
|
|
31
|
+
const ocpp = msg.ocpp || {};
|
|
32
|
+
const messageId = ocpp.messageId;
|
|
33
|
+
const tenant = ocpp.tenant;
|
|
34
|
+
const deviceId = ocpp.deviceId;
|
|
35
|
+
const messageType = ocpp.messageType; // 'call', 'result', 'error'
|
|
36
|
+
const ocppMessageType = ocpp.ocppMessageType; // 2, 3, 4
|
|
37
|
+
|
|
38
|
+
// Only process result and error messages
|
|
39
|
+
const isResult = messageType === 'result' || ocppMessageType === MessageType.CALLRESULT;
|
|
40
|
+
const isError = messageType === 'error' || ocppMessageType === MessageType.CALLERROR;
|
|
41
|
+
|
|
42
|
+
if (!isResult && !isError) {
|
|
43
|
+
// Not a response message, pass through
|
|
44
|
+
if (send) send(msg);
|
|
45
|
+
if (done) done();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!messageId || !tenant || !deviceId) {
|
|
50
|
+
throw new Error('Missing messageId, tenant, or deviceId in response');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Look up the pending command in Redis
|
|
54
|
+
const pendingKey = Keys.pending(tenant, deviceId);
|
|
55
|
+
const pendingData = await redis.hget(pendingKey, messageId);
|
|
56
|
+
|
|
57
|
+
if (!pendingData) {
|
|
58
|
+
// No pending command found - might have timed out or been handled elsewhere
|
|
59
|
+
node.log(`No pending command found for ${messageId}`);
|
|
60
|
+
node.status({
|
|
61
|
+
fill: 'yellow',
|
|
62
|
+
shape: 'ring',
|
|
63
|
+
text: `orphan: ${messageId.slice(0, 8)}`,
|
|
64
|
+
});
|
|
65
|
+
// Still pass through the message
|
|
66
|
+
if (send) send(msg);
|
|
67
|
+
if (done) done();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse pending command data
|
|
72
|
+
let pendingInfo;
|
|
73
|
+
try {
|
|
74
|
+
pendingInfo = JSON.parse(pendingData);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
pendingInfo = {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Remove from pending
|
|
80
|
+
await redis.hdel(pendingKey, messageId);
|
|
81
|
+
|
|
82
|
+
// Find and notify all ocpp-command nodes
|
|
83
|
+
let handled = false;
|
|
84
|
+
RED.nodes.eachNode((n) => {
|
|
85
|
+
if (n.type === 'ocpp-command' && n.handleResponse) {
|
|
86
|
+
// Try to handle this response
|
|
87
|
+
const wasHandled = n.handleResponse(messageId, msg.payload, isError);
|
|
88
|
+
if (wasHandled) {
|
|
89
|
+
handled = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Update status
|
|
95
|
+
if (handled) {
|
|
96
|
+
node.status({
|
|
97
|
+
fill: isError ? 'red' : 'green',
|
|
98
|
+
shape: 'dot',
|
|
99
|
+
text: `${pendingInfo.action || 'response'}: ${isError ? 'error' : 'ok'}`,
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
// No ocpp-command node claimed this response
|
|
103
|
+
// This could happen if the node was restarted or if using a different flow pattern
|
|
104
|
+
node.status({
|
|
105
|
+
fill: 'yellow',
|
|
106
|
+
shape: 'ring',
|
|
107
|
+
text: `unhandled: ${messageId.slice(0, 8)}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Build output message with response info
|
|
112
|
+
const outMsg = {
|
|
113
|
+
...msg,
|
|
114
|
+
ocpp: {
|
|
115
|
+
...ocpp,
|
|
116
|
+
action: pendingInfo.action,
|
|
117
|
+
response: true,
|
|
118
|
+
error: isError,
|
|
119
|
+
handled,
|
|
120
|
+
sentAt: pendingInfo.sentAt,
|
|
121
|
+
receivedAt: Date.now(),
|
|
122
|
+
latency: pendingInfo.sentAt ? Date.now() - pendingInfo.sentAt : undefined,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Pass through the response (for additional processing or logging)
|
|
127
|
+
if (send) {
|
|
128
|
+
send(outMsg);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (done) {
|
|
132
|
+
done();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
} catch (err) {
|
|
136
|
+
node.error(`Response routing error: ${err.message}`, msg);
|
|
137
|
+
node.status({ fill: 'red', shape: 'dot', text: err.message });
|
|
138
|
+
|
|
139
|
+
if (done) {
|
|
140
|
+
done(err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Initial status
|
|
146
|
+
node.status({ fill: 'grey', shape: 'ring', text: 'ready' });
|
|
147
|
+
|
|
148
|
+
// Update status when Redis is ready
|
|
149
|
+
if (redis) {
|
|
150
|
+
redis.once('ready', () => {
|
|
151
|
+
node.status({ fill: 'green', shape: 'ring', text: 'connected' });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (redis.status === 'ready') {
|
|
155
|
+
node.status({ fill: 'green', shape: 'ring', text: 'connected' });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
RED.nodes.registerType('ocpp-response', OCPPResponseNode);
|
|
161
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blokcert/node-red-contrib-ocpp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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,6 +22,7 @@
|
|
|
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",
|
|
25
26
|
"ocpp-config": "nodes/ocpp-config.js"
|
|
26
27
|
}
|
|
27
28
|
},
|