@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.
@@ -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
+ };