@blokcert/node-red-contrib-ocpp 1.0.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/README.md +188 -0
- package/icons/ocpp.svg +4 -0
- package/lib/ocpp-normalizer.js +368 -0
- package/lib/redis-client.js +97 -0
- package/nodes/ocpp-command.html +201 -0
- package/nodes/ocpp-command.js +241 -0
- package/nodes/ocpp-config.html +85 -0
- package/nodes/ocpp-config.js +54 -0
- package/nodes/ocpp-in.html +131 -0
- package/nodes/ocpp-in.js +213 -0
- package/nodes/ocpp-out.html +129 -0
- package/nodes/ocpp-out.js +153 -0
- package/package.json +43 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('ocpp-command', {
|
|
3
|
+
category: 'OCPP',
|
|
4
|
+
color: '#FF9800',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
server: { value: '', type: 'ocpp-config', required: true },
|
|
8
|
+
command: { value: '' },
|
|
9
|
+
timeout: { value: 30000, validate: RED.validators.number() },
|
|
10
|
+
streamMaxLen: { value: 1000, validate: RED.validators.number() },
|
|
11
|
+
waitForResponse: { value: true },
|
|
12
|
+
},
|
|
13
|
+
inputs: 1,
|
|
14
|
+
outputs: 2,
|
|
15
|
+
outputLabels: ['sent', 'response/error'],
|
|
16
|
+
icon: 'font-awesome/fa-bolt',
|
|
17
|
+
paletteLabel: 'ocpp command',
|
|
18
|
+
label: function () {
|
|
19
|
+
if (this.command) {
|
|
20
|
+
return this.name || `ocpp ${this.command}`;
|
|
21
|
+
}
|
|
22
|
+
return this.name || 'ocpp command';
|
|
23
|
+
},
|
|
24
|
+
labelStyle: function () {
|
|
25
|
+
return this.name ? 'node_label_italic' : '';
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script type="text/html" data-template-name="ocpp-command">
|
|
31
|
+
<div class="form-row">
|
|
32
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
33
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
34
|
+
</div>
|
|
35
|
+
<div class="form-row">
|
|
36
|
+
<label for="node-input-server"><i class="fa fa-server"></i> Server</label>
|
|
37
|
+
<input type="text" id="node-input-server">
|
|
38
|
+
</div>
|
|
39
|
+
<div class="form-row">
|
|
40
|
+
<label for="node-input-command"><i class="fa fa-terminal"></i> Command</label>
|
|
41
|
+
<select id="node-input-command">
|
|
42
|
+
<option value="">(set via msg.ocpp.command)</option>
|
|
43
|
+
<optgroup label="OCPP 1.6 & 2.0.1">
|
|
44
|
+
<option value="Reset">Reset</option>
|
|
45
|
+
<option value="UnlockConnector">UnlockConnector</option>
|
|
46
|
+
<option value="TriggerMessage">TriggerMessage</option>
|
|
47
|
+
<option value="ChangeAvailability">ChangeAvailability</option>
|
|
48
|
+
<option value="ClearCache">ClearCache</option>
|
|
49
|
+
<option value="GetConfiguration">GetConfiguration / GetVariables</option>
|
|
50
|
+
<option value="ChangeConfiguration">ChangeConfiguration / SetVariables</option>
|
|
51
|
+
</optgroup>
|
|
52
|
+
<optgroup label="OCPP 1.6">
|
|
53
|
+
<option value="RemoteStartTransaction">RemoteStartTransaction</option>
|
|
54
|
+
<option value="RemoteStopTransaction">RemoteStopTransaction</option>
|
|
55
|
+
<option value="ReserveNow">ReserveNow</option>
|
|
56
|
+
<option value="CancelReservation">CancelReservation</option>
|
|
57
|
+
<option value="UpdateFirmware">UpdateFirmware</option>
|
|
58
|
+
<option value="GetDiagnostics">GetDiagnostics</option>
|
|
59
|
+
<option value="SendLocalList">SendLocalList</option>
|
|
60
|
+
<option value="GetLocalListVersion">GetLocalListVersion</option>
|
|
61
|
+
</optgroup>
|
|
62
|
+
<optgroup label="OCPP 2.0.1">
|
|
63
|
+
<option value="RequestStartTransaction">RequestStartTransaction</option>
|
|
64
|
+
<option value="RequestStopTransaction">RequestStopTransaction</option>
|
|
65
|
+
<option value="SetVariables">SetVariables</option>
|
|
66
|
+
<option value="GetVariables">GetVariables</option>
|
|
67
|
+
<option value="GetBaseReport">GetBaseReport</option>
|
|
68
|
+
<option value="SetChargingProfile">SetChargingProfile</option>
|
|
69
|
+
<option value="ClearChargingProfile">ClearChargingProfile</option>
|
|
70
|
+
<option value="GetCompositeSchedule">GetCompositeSchedule</option>
|
|
71
|
+
</optgroup>
|
|
72
|
+
</select>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="form-row">
|
|
75
|
+
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout (ms)</label>
|
|
76
|
+
<input type="number" id="node-input-timeout" placeholder="30000">
|
|
77
|
+
</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
|
+
<div class="form-row">
|
|
84
|
+
<label for="node-input-streamMaxLen"><i class="fa fa-list-ol"></i> Stream Max Len</label>
|
|
85
|
+
<input type="number" id="node-input-streamMaxLen" placeholder="1000">
|
|
86
|
+
</div>
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<script type="text/html" data-help-name="ocpp-command">
|
|
90
|
+
<p>Sends CSMS-initiated commands to charging stations.</p>
|
|
91
|
+
|
|
92
|
+
<h3>Inputs</h3>
|
|
93
|
+
<dl class="message-properties">
|
|
94
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
95
|
+
<dd>The command payload to send to the device.</dd>
|
|
96
|
+
|
|
97
|
+
<dt>ocpp.tenant <span class="property-type">string</span></dt>
|
|
98
|
+
<dd>Target operator/tenant identifier (required).</dd>
|
|
99
|
+
|
|
100
|
+
<dt>ocpp.deviceId <span class="property-type">string</span></dt>
|
|
101
|
+
<dd>Target charger/station ID (required).</dd>
|
|
102
|
+
|
|
103
|
+
<dt>ocpp.command <span class="property-type">string</span></dt>
|
|
104
|
+
<dd>Command action (overrides node config).</dd>
|
|
105
|
+
|
|
106
|
+
<dt>ocpp.messageId <span class="property-type">string</span></dt>
|
|
107
|
+
<dd>Custom message ID (auto-generated if not provided).</dd>
|
|
108
|
+
</dl>
|
|
109
|
+
|
|
110
|
+
<h3>Outputs</h3>
|
|
111
|
+
<ol class="node-ports">
|
|
112
|
+
<li>Command Sent
|
|
113
|
+
<dl class="message-properties">
|
|
114
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
115
|
+
<dd>Original payload</dd>
|
|
116
|
+
<dt>ocpp.messageId <span class="property-type">string</span></dt>
|
|
117
|
+
<dd>Generated/provided message ID</dd>
|
|
118
|
+
<dt>ocpp.sentAt <span class="property-type">number</span></dt>
|
|
119
|
+
<dd>Timestamp when command was sent</dd>
|
|
120
|
+
</dl>
|
|
121
|
+
</li>
|
|
122
|
+
<li>Response/Error (when Wait Response is enabled)
|
|
123
|
+
<dl class="message-properties">
|
|
124
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
125
|
+
<dd>Response payload from device</dd>
|
|
126
|
+
<dt>ocpp.response <span class="property-type">boolean</span></dt>
|
|
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>
|
|
135
|
+
|
|
136
|
+
<h3>Properties</h3>
|
|
137
|
+
<dl class="message-properties">
|
|
138
|
+
<dt>Command <span class="property-type">select</span></dt>
|
|
139
|
+
<dd>OCPP command to send. Can be overridden via <code>msg.ocpp.command</code>.</dd>
|
|
140
|
+
|
|
141
|
+
<dt>Timeout <span class="property-type">number</span></dt>
|
|
142
|
+
<dd>How long to wait for device response (ms).</dd>
|
|
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>
|
|
146
|
+
</dl>
|
|
147
|
+
|
|
148
|
+
<h3>Common Commands</h3>
|
|
149
|
+
<table>
|
|
150
|
+
<tr><th>Command</th><th>Description</th></tr>
|
|
151
|
+
<tr><td>Reset</td><td>Reboot the charging station</td></tr>
|
|
152
|
+
<tr><td>RemoteStartTransaction</td><td>Start charging remotely (1.6)</td></tr>
|
|
153
|
+
<tr><td>RequestStartTransaction</td><td>Start charging remotely (2.0.1)</td></tr>
|
|
154
|
+
<tr><td>UnlockConnector</td><td>Unlock a connector</td></tr>
|
|
155
|
+
<tr><td>TriggerMessage</td><td>Request status update</td></tr>
|
|
156
|
+
<tr><td>ChangeAvailability</td><td>Set connector availability</td></tr>
|
|
157
|
+
</table>
|
|
158
|
+
|
|
159
|
+
<h3>Example: Remote Start</h3>
|
|
160
|
+
<pre>
|
|
161
|
+
// For OCPP 1.6
|
|
162
|
+
msg.ocpp = {
|
|
163
|
+
tenant: "operator1",
|
|
164
|
+
deviceId: "charger001",
|
|
165
|
+
command: "RemoteStartTransaction"
|
|
166
|
+
};
|
|
167
|
+
msg.payload = {
|
|
168
|
+
connectorId: 1,
|
|
169
|
+
idTag: "USER123"
|
|
170
|
+
};
|
|
171
|
+
return msg;
|
|
172
|
+
|
|
173
|
+
// For OCPP 2.0.1
|
|
174
|
+
msg.ocpp = {
|
|
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;
|
|
187
|
+
</pre>
|
|
188
|
+
|
|
189
|
+
<h3>Example: Reset</h3>
|
|
190
|
+
<pre>
|
|
191
|
+
msg.ocpp = {
|
|
192
|
+
tenant: "operator1",
|
|
193
|
+
deviceId: "charger001",
|
|
194
|
+
command: "Reset"
|
|
195
|
+
};
|
|
196
|
+
msg.payload = {
|
|
197
|
+
type: "Soft" // or "Hard"
|
|
198
|
+
};
|
|
199
|
+
return msg;
|
|
200
|
+
</pre>
|
|
201
|
+
</script>
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OCPP Command Node
|
|
3
|
+
* Sends CSMS-initiated commands to charging stations
|
|
4
|
+
*/
|
|
5
|
+
const { Keys } = require('../lib/redis-client');
|
|
6
|
+
const { buildCall } = require('../lib/ocpp-normalizer');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
module.exports = function (RED) {
|
|
10
|
+
function OCPPCommandNode(config) {
|
|
11
|
+
RED.nodes.createNode(this, config);
|
|
12
|
+
const node = this;
|
|
13
|
+
|
|
14
|
+
// Get config node
|
|
15
|
+
node.server = RED.nodes.getNode(config.server);
|
|
16
|
+
if (!node.server) {
|
|
17
|
+
node.error('No OCPP config node configured');
|
|
18
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no config' });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Node configuration
|
|
23
|
+
node.command = config.command || ''; // Can be overridden by msg
|
|
24
|
+
node.timeout = parseInt(config.timeout, 10) || 30000;
|
|
25
|
+
node.streamMaxLen = parseInt(config.streamMaxLen, 10) || 1000;
|
|
26
|
+
node.waitForResponse = config.waitForResponse !== false; // Default true
|
|
27
|
+
|
|
28
|
+
const redis = node.server.redis;
|
|
29
|
+
|
|
30
|
+
// Pending commands waiting for response
|
|
31
|
+
const pendingCommands = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate unique message ID
|
|
35
|
+
*/
|
|
36
|
+
function generateMessageId() {
|
|
37
|
+
return crypto.randomUUID();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handle incoming messages
|
|
42
|
+
*/
|
|
43
|
+
node.on('input', async (msg, send, done) => {
|
|
44
|
+
try {
|
|
45
|
+
// Determine target device
|
|
46
|
+
const tenant = msg.ocpp?.tenant || msg.tenant;
|
|
47
|
+
const deviceId = msg.ocpp?.deviceId || msg.deviceId;
|
|
48
|
+
|
|
49
|
+
if (!tenant || !deviceId) {
|
|
50
|
+
throw new Error('Missing tenant or deviceId. Set msg.ocpp.tenant and msg.ocpp.deviceId');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Determine command action
|
|
54
|
+
const action = msg.ocpp?.command || msg.command || node.command;
|
|
55
|
+
if (!action) {
|
|
56
|
+
throw new Error('Missing command action. Set msg.ocpp.command or configure in node');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Generate message ID
|
|
60
|
+
const messageId = msg.ocpp?.messageId || generateMessageId();
|
|
61
|
+
|
|
62
|
+
// Build command payload
|
|
63
|
+
const payload = msg.payload || {};
|
|
64
|
+
|
|
65
|
+
// Build OCPP CALL message
|
|
66
|
+
const callMessage = buildCall(messageId, action, payload);
|
|
67
|
+
|
|
68
|
+
// Send to device
|
|
69
|
+
const outboundKey = Keys.outbound(tenant, deviceId);
|
|
70
|
+
await redis.xadd(
|
|
71
|
+
outboundKey,
|
|
72
|
+
'MAXLEN', '~', node.streamMaxLen,
|
|
73
|
+
'*',
|
|
74
|
+
'messageId', messageId,
|
|
75
|
+
'data', callMessage,
|
|
76
|
+
'timestamp', Date.now().toString()
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
node.log(`Sent command ${action} to ${tenant}:${deviceId} (${messageId})`);
|
|
80
|
+
|
|
81
|
+
// Update status
|
|
82
|
+
node.status({
|
|
83
|
+
fill: 'blue',
|
|
84
|
+
shape: 'dot',
|
|
85
|
+
text: `${action} → ${tenant}:${deviceId}`,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Prepare output message
|
|
89
|
+
const outMsg = {
|
|
90
|
+
...msg,
|
|
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
|
+
};
|
|
139
|
+
|
|
140
|
+
node.send([null, timeoutMsg]);
|
|
141
|
+
}, node.timeout);
|
|
142
|
+
|
|
143
|
+
// Store for cleanup
|
|
144
|
+
pendingCommands.set(messageId, {
|
|
145
|
+
timeoutId,
|
|
146
|
+
msg: outMsg,
|
|
147
|
+
action,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Send on first output (command sent)
|
|
151
|
+
if (send) {
|
|
152
|
+
send([outMsg, null]);
|
|
153
|
+
} else {
|
|
154
|
+
node.send([outMsg, null]);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Don't wait for response, just send confirmation
|
|
158
|
+
if (send) {
|
|
159
|
+
send(outMsg);
|
|
160
|
+
} else {
|
|
161
|
+
node.send(outMsg);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (done) {
|
|
166
|
+
done();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} catch (err) {
|
|
170
|
+
node.error(`Command error: ${err.message}`, msg);
|
|
171
|
+
node.status({ fill: 'red', shape: 'dot', text: err.message });
|
|
172
|
+
|
|
173
|
+
if (done) {
|
|
174
|
+
done(err);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
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
|
+
// Initial status
|
|
216
|
+
node.status({ fill: 'grey', shape: 'ring', text: 'ready' });
|
|
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
|
+
}
|
|
228
|
+
|
|
229
|
+
// Cleanup on close
|
|
230
|
+
node.on('close', (done) => {
|
|
231
|
+
// Clear all pending timeouts
|
|
232
|
+
for (const [, pending] of pendingCommands) {
|
|
233
|
+
clearTimeout(pending.timeoutId);
|
|
234
|
+
}
|
|
235
|
+
pendingCommands.clear();
|
|
236
|
+
done();
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
RED.nodes.registerType('ocpp-command', OCPPCommandNode);
|
|
241
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('ocpp-config', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
name: { value: '' },
|
|
6
|
+
host: { value: 'localhost', required: true },
|
|
7
|
+
port: { value: 6379, required: true, validate: RED.validators.number() },
|
|
8
|
+
db: { value: 0, validate: RED.validators.number() },
|
|
9
|
+
consumerGroup: { value: 'biz-workers', required: true },
|
|
10
|
+
consumerName: { value: '' },
|
|
11
|
+
},
|
|
12
|
+
credentials: {
|
|
13
|
+
password: { type: 'password' },
|
|
14
|
+
},
|
|
15
|
+
label: function () {
|
|
16
|
+
return this.name || `${this.host}:${this.port}`;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script type="text/html" data-template-name="ocpp-config">
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
24
|
+
<input type="text" id="node-config-input-name" placeholder="Name">
|
|
25
|
+
</div>
|
|
26
|
+
<div class="form-row">
|
|
27
|
+
<label for="node-config-input-host"><i class="fa fa-server"></i> Host</label>
|
|
28
|
+
<input type="text" id="node-config-input-host" placeholder="localhost">
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<label for="node-config-input-port"><i class="fa fa-plug"></i> Port</label>
|
|
32
|
+
<input type="number" id="node-config-input-port" placeholder="6379">
|
|
33
|
+
</div>
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-config-input-password"><i class="fa fa-lock"></i> Password</label>
|
|
36
|
+
<input type="password" id="node-config-input-password" placeholder="(optional)">
|
|
37
|
+
</div>
|
|
38
|
+
<div class="form-row">
|
|
39
|
+
<label for="node-config-input-db"><i class="fa fa-database"></i> DB</label>
|
|
40
|
+
<input type="number" id="node-config-input-db" placeholder="0">
|
|
41
|
+
</div>
|
|
42
|
+
<div class="form-row">
|
|
43
|
+
<label for="node-config-input-consumerGroup"><i class="fa fa-users"></i> Consumer Group</label>
|
|
44
|
+
<input type="text" id="node-config-input-consumerGroup" placeholder="biz-workers">
|
|
45
|
+
</div>
|
|
46
|
+
<div class="form-row">
|
|
47
|
+
<label for="node-config-input-consumerName"><i class="fa fa-user"></i> Consumer Name</label>
|
|
48
|
+
<input type="text" id="node-config-input-consumerName" placeholder="(auto-generated)">
|
|
49
|
+
</div>
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<script type="text/html" data-help-name="ocpp-config">
|
|
53
|
+
<p>Configuration node for OCPP Redis connection.</p>
|
|
54
|
+
|
|
55
|
+
<h3>Properties</h3>
|
|
56
|
+
<dl class="message-properties">
|
|
57
|
+
<dt>Host <span class="property-type">string</span></dt>
|
|
58
|
+
<dd>Redis server hostname or IP address.</dd>
|
|
59
|
+
|
|
60
|
+
<dt>Port <span class="property-type">number</span></dt>
|
|
61
|
+
<dd>Redis server port (default: 6379).</dd>
|
|
62
|
+
|
|
63
|
+
<dt>Password <span class="property-type">string</span></dt>
|
|
64
|
+
<dd>Optional Redis password for authentication.</dd>
|
|
65
|
+
|
|
66
|
+
<dt>DB <span class="property-type">number</span></dt>
|
|
67
|
+
<dd>Redis database number (default: 0).</dd>
|
|
68
|
+
|
|
69
|
+
<dt>Consumer Group <span class="property-type">string</span></dt>
|
|
70
|
+
<dd>Redis Streams consumer group name (default: biz-workers).</dd>
|
|
71
|
+
|
|
72
|
+
<dt>Consumer Name <span class="property-type">string</span></dt>
|
|
73
|
+
<dd>Consumer name within the group. Auto-generated if empty.</dd>
|
|
74
|
+
</dl>
|
|
75
|
+
|
|
76
|
+
<h3>Details</h3>
|
|
77
|
+
<p>This configuration node is shared by OCPP input and output nodes.
|
|
78
|
+
It manages the Redis connection used for OCPP message streams.</p>
|
|
79
|
+
|
|
80
|
+
<h3>Redis Streams</h3>
|
|
81
|
+
<ul>
|
|
82
|
+
<li><code>ws:inbound</code> - Inbound messages from charging stations</li>
|
|
83
|
+
<li><code>ws:out:{tenant}:{deviceId}</code> - Outbound messages to devices</li>
|
|
84
|
+
</ul>
|
|
85
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OCPP Config Node
|
|
3
|
+
* Shared configuration for Redis connection and OCPP settings
|
|
4
|
+
*/
|
|
5
|
+
const { getConnection, closeConnection } = require('../lib/redis-client');
|
|
6
|
+
|
|
7
|
+
module.exports = function (RED) {
|
|
8
|
+
function OCPPConfigNode(config) {
|
|
9
|
+
RED.nodes.createNode(this, config);
|
|
10
|
+
const node = this;
|
|
11
|
+
|
|
12
|
+
// Store configuration
|
|
13
|
+
node.host = config.host || 'localhost';
|
|
14
|
+
node.port = parseInt(config.port, 10) || 6379;
|
|
15
|
+
node.db = parseInt(config.db, 10) || 0;
|
|
16
|
+
node.consumerGroup = config.consumerGroup || 'biz-workers';
|
|
17
|
+
node.consumerName = config.consumerName || `node-red-${process.pid}`;
|
|
18
|
+
|
|
19
|
+
// Password from credentials (secure storage)
|
|
20
|
+
node.password = node.credentials?.password || '';
|
|
21
|
+
|
|
22
|
+
// Get or create Redis connection
|
|
23
|
+
try {
|
|
24
|
+
node.redis = getConnection(node.id, {
|
|
25
|
+
host: node.host,
|
|
26
|
+
port: node.port,
|
|
27
|
+
password: node.password,
|
|
28
|
+
db: node.db,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
node.redis.on('ready', () => {
|
|
32
|
+
node.log(`Redis connected: ${node.host}:${node.port}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
node.redis.on('error', (err) => {
|
|
36
|
+
node.error(`Redis error: ${err.message}`);
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
node.error(`Failed to create Redis connection: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Cleanup on close
|
|
43
|
+
node.on('close', (done) => {
|
|
44
|
+
closeConnection(node.id);
|
|
45
|
+
done();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
RED.nodes.registerType('ocpp-config', OCPPConfigNode, {
|
|
50
|
+
credentials: {
|
|
51
|
+
password: { type: 'password' },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
};
|