@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,131 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('ocpp-in', {
3
+ category: 'OCPP',
4
+ color: '#4CAF50',
5
+ defaults: {
6
+ name: { value: '' },
7
+ server: { value: '', type: 'ocpp-config', required: true },
8
+ blockTimeout: { value: 5000, validate: RED.validators.number() },
9
+ batchSize: { value: 10, validate: RED.validators.number() },
10
+ autoAck: { value: true },
11
+ normalizePayload: { value: true },
12
+ filterAction: { value: '' },
13
+ },
14
+ inputs: 0,
15
+ outputs: 1,
16
+ icon: 'font-awesome/fa-sign-in',
17
+ paletteLabel: 'ocpp in',
18
+ label: function () {
19
+ if (this.filterAction) {
20
+ return this.name || `ocpp in (${this.filterAction})`;
21
+ }
22
+ return this.name || 'ocpp in';
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-in">
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-blockTimeout"><i class="fa fa-clock-o"></i> Block Timeout (ms)</label>
41
+ <input type="number" id="node-input-blockTimeout" placeholder="5000">
42
+ </div>
43
+ <div class="form-row">
44
+ <label for="node-input-batchSize"><i class="fa fa-list"></i> Batch Size</label>
45
+ <input type="number" id="node-input-batchSize" placeholder="10">
46
+ </div>
47
+ <div class="form-row">
48
+ <label for="node-input-autoAck"><i class="fa fa-check"></i> Auto Acknowledge</label>
49
+ <input type="checkbox" id="node-input-autoAck" style="width: auto; margin-left: 0;">
50
+ <span style="margin-left: 10px;">Automatically ACK messages after processing</span>
51
+ </div>
52
+ <div class="form-row">
53
+ <label for="node-input-normalizePayload"><i class="fa fa-exchange"></i> Normalize</label>
54
+ <input type="checkbox" id="node-input-normalizePayload" style="width: auto; margin-left: 0;">
55
+ <span style="margin-left: 10px;">Convert OCPP 1.6/2.0.1 to unified format</span>
56
+ </div>
57
+ <div class="form-row">
58
+ <label for="node-input-filterAction"><i class="fa fa-filter"></i> Filter Action</label>
59
+ <input type="text" id="node-input-filterAction" placeholder="(all actions)">
60
+ </div>
61
+ </script>
62
+
63
+ <script type="text/html" data-help-name="ocpp-in">
64
+ <p>Receives OCPP messages from charging stations via Redis Streams.</p>
65
+
66
+ <h3>Outputs</h3>
67
+ <dl class="message-properties">
68
+ <dt>payload <span class="property-type">object</span></dt>
69
+ <dd>The OCPP message payload (normalized if enabled).</dd>
70
+
71
+ <dt>ocpp <span class="property-type">object</span></dt>
72
+ <dd>OCPP message metadata:
73
+ <ul>
74
+ <li><code>messageId</code> - Unique message ID</li>
75
+ <li><code>action</code> - OCPP action (e.g., BootNotification, Heartbeat)</li>
76
+ <li><code>version</code> - OCPP version (1.6 or 2.0.1)</li>
77
+ <li><code>tenant</code> - Operator/tenant identifier</li>
78
+ <li><code>deviceId</code> - Charger/station ID</li>
79
+ <li><code>identity</code> - Combined tenant:deviceId</li>
80
+ <li><code>timestamp</code> - Message timestamp</li>
81
+ <li><code>streamId</code> - Redis Stream message ID</li>
82
+ <li><code>_original</code> - Original payload before normalization</li>
83
+ </ul>
84
+ </dd>
85
+ </dl>
86
+
87
+ <h3>Properties</h3>
88
+ <dl class="message-properties">
89
+ <dt>Server <span class="property-type">ocpp-config</span></dt>
90
+ <dd>The OCPP configuration node with Redis connection settings.</dd>
91
+
92
+ <dt>Block Timeout <span class="property-type">number</span></dt>
93
+ <dd>How long to wait (ms) for new messages before checking again.</dd>
94
+
95
+ <dt>Batch Size <span class="property-type">number</span></dt>
96
+ <dd>Maximum number of messages to read per batch.</dd>
97
+
98
+ <dt>Auto Acknowledge <span class="property-type">boolean</span></dt>
99
+ <dd>If enabled, messages are automatically acknowledged after being sent.
100
+ Disable if you need manual control over acknowledgment.</dd>
101
+
102
+ <dt>Normalize <span class="property-type">boolean</span></dt>
103
+ <dd>If enabled, converts OCPP 1.6 and 2.0.1 messages to a unified format.
104
+ This allows your flow to handle both versions with the same logic.</dd>
105
+
106
+ <dt>Filter Action <span class="property-type">string</span></dt>
107
+ <dd>Optional. Only output messages with this action name.
108
+ Leave empty to receive all actions.</dd>
109
+ </dl>
110
+
111
+ <h3>Details</h3>
112
+ <p>This node reads from the <code>ws:inbound</code> Redis Stream using consumer groups,
113
+ allowing multiple Node-RED instances to share the message load.</p>
114
+
115
+ <h3>Supported Actions</h3>
116
+ <p>Common OCPP actions that can be filtered:</p>
117
+ <ul>
118
+ <li><code>BootNotification</code></li>
119
+ <li><code>Heartbeat</code></li>
120
+ <li><code>StatusNotification</code></li>
121
+ <li><code>Authorize</code></li>
122
+ <li><code>StartTransaction</code> / <code>TransactionEvent</code></li>
123
+ <li><code>StopTransaction</code></li>
124
+ <li><code>MeterValues</code></li>
125
+ </ul>
126
+
127
+ <h3>Example Flow</h3>
128
+ <pre>
129
+ [ocpp in] → [switch (by action)] → [handler] → [ocpp out]
130
+ </pre>
131
+ </script>
@@ -0,0 +1,213 @@
1
+ /**
2
+ * OCPP Input Node
3
+ * Reads OCPP messages from Redis Stream (ws:inbound) using Consumer Groups
4
+ */
5
+ const { Keys } = require('../lib/redis-client');
6
+ const { parseStreamMessage, normalize } = require('../lib/ocpp-normalizer');
7
+
8
+ module.exports = function (RED) {
9
+ function OCPPInNode(config) {
10
+ RED.nodes.createNode(this, config);
11
+ const node = this;
12
+
13
+ // Get config node
14
+ node.server = RED.nodes.getNode(config.server);
15
+ if (!node.server) {
16
+ node.error('No OCPP config node configured');
17
+ node.status({ fill: 'red', shape: 'ring', text: 'no config' });
18
+ return;
19
+ }
20
+
21
+ // Node configuration
22
+ node.blockTimeout = parseInt(config.blockTimeout, 10) || 5000;
23
+ node.batchSize = parseInt(config.batchSize, 10) || 10;
24
+ node.autoAck = config.autoAck !== false; // Default true
25
+ node.normalizePayload = config.normalizePayload !== false; // Default true
26
+ node.filterAction = config.filterAction || ''; // Optional action filter
27
+
28
+ // State
29
+ let running = false;
30
+ let consumerGroupCreated = false;
31
+
32
+ const redis = node.server.redis;
33
+ const consumerGroup = node.server.consumerGroup;
34
+ const consumerName = node.server.consumerName;
35
+ const streamKey = Keys.inbound();
36
+
37
+ /**
38
+ * Ensure consumer group exists
39
+ */
40
+ async function ensureConsumerGroup() {
41
+ if (consumerGroupCreated) return;
42
+
43
+ try {
44
+ await redis.xgroup('CREATE', streamKey, consumerGroup, '0', 'MKSTREAM');
45
+ node.log(`Created consumer group: ${consumerGroup}`);
46
+ } catch (err) {
47
+ if (err.message.includes('BUSYGROUP')) {
48
+ // Group already exists, that's fine
49
+ node.log(`Consumer group already exists: ${consumerGroup}`);
50
+ } else {
51
+ throw err;
52
+ }
53
+ }
54
+ consumerGroupCreated = true;
55
+ }
56
+
57
+ /**
58
+ * Read messages from stream
59
+ */
60
+ async function readMessages() {
61
+ if (!running) return;
62
+
63
+ try {
64
+ // XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key id
65
+ const results = await redis.xreadgroup(
66
+ 'GROUP', consumerGroup, consumerName,
67
+ 'COUNT', node.batchSize,
68
+ 'BLOCK', node.blockTimeout,
69
+ 'STREAMS', streamKey, '>'
70
+ );
71
+
72
+ if (results && results.length > 0) {
73
+ const [, messages] = results[0]; // [streamKey, [[id, fields], ...]]
74
+
75
+ for (const [streamId, fields] of messages) {
76
+ await processMessage(streamId, fields);
77
+ }
78
+ }
79
+ } catch (err) {
80
+ if (err.message.includes('NOGROUP')) {
81
+ // Consumer group was deleted, recreate it
82
+ consumerGroupCreated = false;
83
+ await ensureConsumerGroup();
84
+ } else {
85
+ node.error(`Read error: ${err.message}`);
86
+ node.status({ fill: 'red', shape: 'dot', text: 'read error' });
87
+ // Wait before retry
88
+ await new Promise(resolve => setTimeout(resolve, 1000));
89
+ }
90
+ }
91
+
92
+ // Continue reading
93
+ if (running) {
94
+ setImmediate(readMessages);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Process a single message from the stream
100
+ */
101
+ async function processMessage(streamId, fields) {
102
+ try {
103
+ // Convert Redis fields array to object
104
+ // fields = ['key1', 'val1', 'key2', 'val2', ...]
105
+ const data = {};
106
+ for (let i = 0; i < fields.length; i += 2) {
107
+ data[fields[i]] = fields[i + 1];
108
+ }
109
+
110
+ // Parse the stream message
111
+ let msg = parseStreamMessage(data);
112
+ msg.streamId = streamId;
113
+
114
+ // Apply action filter if configured
115
+ if (node.filterAction && msg.action !== node.filterAction) {
116
+ // Auto-ack filtered messages if autoAck is enabled
117
+ if (node.autoAck) {
118
+ await redis.xack(streamKey, consumerGroup, streamId);
119
+ }
120
+ return;
121
+ }
122
+
123
+ // Normalize payload if enabled
124
+ if (node.normalizePayload) {
125
+ msg = normalize(msg);
126
+ }
127
+
128
+ // Build Node-RED message
129
+ const nodeMsg = {
130
+ payload: msg.payload,
131
+ ocpp: {
132
+ messageId: msg.messageId,
133
+ action: msg.action,
134
+ version: msg.version,
135
+ protocol: msg.protocol,
136
+ tenant: msg.tenant,
137
+ deviceId: msg.deviceId,
138
+ identity: msg.identity,
139
+ timestamp: msg.timestamp,
140
+ streamId: streamId,
141
+ _original: msg._original,
142
+ },
143
+ _msgid: RED.util.generateId(),
144
+ };
145
+
146
+ // Update status
147
+ node.status({
148
+ fill: 'green',
149
+ shape: 'dot',
150
+ text: `${msg.action} from ${msg.identity}`,
151
+ });
152
+
153
+ // Send to output
154
+ node.send(nodeMsg);
155
+
156
+ // Auto-acknowledge if enabled
157
+ if (node.autoAck) {
158
+ await redis.xack(streamKey, consumerGroup, streamId);
159
+ }
160
+
161
+ } catch (err) {
162
+ node.error(`Process error: ${err.message}`, { streamId, fields });
163
+ node.status({ fill: 'red', shape: 'dot', text: 'process error' });
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Start reading from stream
169
+ */
170
+ async function start() {
171
+ if (running) return;
172
+
173
+ try {
174
+ await ensureConsumerGroup();
175
+ running = true;
176
+ node.status({ fill: 'green', shape: 'ring', text: 'listening' });
177
+ node.log(`Started listening on ${streamKey}`);
178
+ readMessages();
179
+ } catch (err) {
180
+ node.error(`Start error: ${err.message}`);
181
+ node.status({ fill: 'red', shape: 'ring', text: 'start failed' });
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Stop reading
187
+ */
188
+ function stop() {
189
+ running = false;
190
+ node.status({ fill: 'grey', shape: 'ring', text: 'stopped' });
191
+ }
192
+
193
+ // Start when Node-RED is ready
194
+ if (redis) {
195
+ redis.once('ready', () => {
196
+ start();
197
+ });
198
+
199
+ // If already connected
200
+ if (redis.status === 'ready') {
201
+ start();
202
+ }
203
+ }
204
+
205
+ // Cleanup on node removal
206
+ node.on('close', (done) => {
207
+ stop();
208
+ done();
209
+ });
210
+ }
211
+
212
+ RED.nodes.registerType('ocpp-in', OCPPInNode);
213
+ };
@@ -0,0 +1,129 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('ocpp-out', {
3
+ category: 'OCPP',
4
+ color: '#2196F3',
5
+ defaults: {
6
+ name: { value: '' },
7
+ server: { value: '', type: 'ocpp-config', required: true },
8
+ denormalizePayload: { value: true },
9
+ autoAck: { value: true },
10
+ streamMaxLen: { value: 1000, validate: RED.validators.number() },
11
+ },
12
+ inputs: 1,
13
+ outputs: 1,
14
+ icon: 'font-awesome/fa-sign-out',
15
+ paletteLabel: 'ocpp out',
16
+ label: function () {
17
+ return this.name || 'ocpp out';
18
+ },
19
+ labelStyle: function () {
20
+ return this.name ? 'node_label_italic' : '';
21
+ },
22
+ align: 'right',
23
+ });
24
+ </script>
25
+
26
+ <script type="text/html" data-template-name="ocpp-out">
27
+ <div class="form-row">
28
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
29
+ <input type="text" id="node-input-name" placeholder="Name">
30
+ </div>
31
+ <div class="form-row">
32
+ <label for="node-input-server"><i class="fa fa-server"></i> Server</label>
33
+ <input type="text" id="node-input-server">
34
+ </div>
35
+ <div class="form-row">
36
+ <label for="node-input-denormalizePayload"><i class="fa fa-exchange"></i> Denormalize</label>
37
+ <input type="checkbox" id="node-input-denormalizePayload" style="width: auto; margin-left: 0;">
38
+ <span style="margin-left: 10px;">Convert unified format back to OCPP version</span>
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-input-autoAck"><i class="fa fa-check"></i> Auto ACK</label>
42
+ <input type="checkbox" id="node-input-autoAck" style="width: auto; margin-left: 0;">
43
+ <span style="margin-left: 10px;">Acknowledge inbound message after response</span>
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-streamMaxLen"><i class="fa fa-list-ol"></i> Stream Max Len</label>
47
+ <input type="number" id="node-input-streamMaxLen" placeholder="1000">
48
+ </div>
49
+ </script>
50
+
51
+ <script type="text/html" data-help-name="ocpp-out">
52
+ <p>Sends OCPP responses back to charging stations via Redis Streams.</p>
53
+
54
+ <h3>Inputs</h3>
55
+ <dl class="message-properties">
56
+ <dt>payload <span class="property-type">object</span></dt>
57
+ <dd>The response payload to send back to the device.</dd>
58
+
59
+ <dt>ocpp <span class="property-type">object</span></dt>
60
+ <dd>OCPP metadata from the input node (required):
61
+ <ul>
62
+ <li><code>messageId</code> - Original message ID (required)</li>
63
+ <li><code>action</code> - OCPP action for denormalization</li>
64
+ <li><code>version</code> - OCPP version for denormalization</li>
65
+ <li><code>tenant</code> - Operator/tenant identifier (required)</li>
66
+ <li><code>deviceId</code> - Charger/station ID (required)</li>
67
+ <li><code>streamId</code> - For auto-acknowledge</li>
68
+ </ul>
69
+ </dd>
70
+
71
+ <dt>ocpp.error <span class="property-type">boolean</span></dt>
72
+ <dd>If true, sends an error response instead of a normal response.</dd>
73
+
74
+ <dt>ocpp.errorCode <span class="property-type">string</span></dt>
75
+ <dd>OCPP error code (e.g., InternalError, NotImplemented).</dd>
76
+
77
+ <dt>ocpp.errorDescription <span class="property-type">string</span></dt>
78
+ <dd>Human-readable error description.</dd>
79
+ </dl>
80
+
81
+ <h3>Outputs</h3>
82
+ <p>The message is passed through unchanged after sending the response.</p>
83
+
84
+ <h3>Properties</h3>
85
+ <dl class="message-properties">
86
+ <dt>Server <span class="property-type">ocpp-config</span></dt>
87
+ <dd>The OCPP configuration node with Redis connection settings.</dd>
88
+
89
+ <dt>Denormalize <span class="property-type">boolean</span></dt>
90
+ <dd>If enabled, converts unified format back to OCPP 1.6 or 2.0.1
91
+ based on the original message version.</dd>
92
+
93
+ <dt>Auto ACK <span class="property-type">boolean</span></dt>
94
+ <dd>If enabled and streamId is present, acknowledges the original
95
+ inbound message after sending the response.</dd>
96
+
97
+ <dt>Stream Max Len <span class="property-type">number</span></dt>
98
+ <dd>Maximum length of the outbound stream (older messages are trimmed).</dd>
99
+ </dl>
100
+
101
+ <h3>Details</h3>
102
+ <p>This node sends responses to the device-specific outbound stream:
103
+ <code>ws:out:{tenant}:{deviceId}</code></p>
104
+
105
+ <p>The WebSocket server reads from this stream and forwards the response
106
+ to the connected charging station.</p>
107
+
108
+ <h3>Error Responses</h3>
109
+ <p>To send an error response, set <code>msg.ocpp.error = true</code>:</p>
110
+ <pre>
111
+ msg.ocpp.error = true;
112
+ msg.ocpp.errorCode = "NotImplemented";
113
+ msg.ocpp.errorDescription = "This action is not supported";
114
+ return msg;
115
+ </pre>
116
+
117
+ <h3>Example Flow</h3>
118
+ <pre>
119
+ [ocpp in] → [handler function] → [ocpp out]
120
+
121
+ // Handler function example:
122
+ msg.payload = {
123
+ status: "Accepted",
124
+ currentTime: new Date().toISOString(),
125
+ interval: 300
126
+ };
127
+ return msg;
128
+ </pre>
129
+ </script>
@@ -0,0 +1,153 @@
1
+ /**
2
+ * OCPP Output Node
3
+ * Sends OCPP responses back to charging stations via Redis Stream
4
+ */
5
+ const { Keys } = require('../lib/redis-client');
6
+ const { denormalize, buildResponse, buildError } = require('../lib/ocpp-normalizer');
7
+
8
+ module.exports = function (RED) {
9
+ function OCPPOutNode(config) {
10
+ RED.nodes.createNode(this, config);
11
+ const node = this;
12
+
13
+ // Get config node
14
+ node.server = RED.nodes.getNode(config.server);
15
+ if (!node.server) {
16
+ node.error('No OCPP config node configured');
17
+ node.status({ fill: 'red', shape: 'ring', text: 'no config' });
18
+ return;
19
+ }
20
+
21
+ // Node configuration
22
+ node.denormalizePayload = config.denormalizePayload !== false; // Default true
23
+ node.autoAck = config.autoAck !== false; // Default true
24
+ node.streamMaxLen = parseInt(config.streamMaxLen, 10) || 1000;
25
+
26
+ const redis = node.server.redis;
27
+ const consumerGroup = node.server.consumerGroup;
28
+
29
+ /**
30
+ * Handle incoming messages
31
+ */
32
+ node.on('input', async (msg, send, done) => {
33
+ try {
34
+ // Extract OCPP metadata
35
+ const ocpp = msg.ocpp || {};
36
+ const {
37
+ messageId,
38
+ action,
39
+ version,
40
+ tenant,
41
+ deviceId,
42
+ identity,
43
+ streamId,
44
+ } = ocpp;
45
+
46
+ // Validate required fields
47
+ if (!messageId) {
48
+ throw new Error('Missing ocpp.messageId');
49
+ }
50
+ if (!tenant || !deviceId) {
51
+ throw new Error('Missing ocpp.tenant or ocpp.deviceId');
52
+ }
53
+
54
+ let responsePayload = msg.payload;
55
+
56
+ // Check if this is an error response
57
+ const isError = msg.ocpp?.error || msg.error;
58
+
59
+ if (isError) {
60
+ // Build error response
61
+ const errorCode = msg.ocpp?.errorCode || 'InternalError';
62
+ const errorDescription = msg.ocpp?.errorDescription || msg.error?.message || 'Unknown error';
63
+ const errorDetails = msg.ocpp?.errorDetails || {};
64
+
65
+ const response = buildError(messageId, errorCode, errorDescription, errorDetails);
66
+
67
+ await sendToDevice(tenant, deviceId, response, messageId);
68
+
69
+ } else {
70
+ // Denormalize payload if enabled
71
+ if (node.denormalizePayload && action) {
72
+ const denormalized = denormalize({
73
+ action,
74
+ payload: responsePayload,
75
+ version,
76
+ }, version);
77
+ responsePayload = denormalized.payload;
78
+ }
79
+
80
+ // Build OCPP response
81
+ const response = buildResponse(messageId, responsePayload);
82
+
83
+ await sendToDevice(tenant, deviceId, response, messageId);
84
+ }
85
+
86
+ // Auto-acknowledge the original message if streamId is present
87
+ if (node.autoAck && streamId) {
88
+ const inboundKey = Keys.inbound();
89
+ await redis.xack(inboundKey, consumerGroup, streamId);
90
+ }
91
+
92
+ // Update status
93
+ node.status({
94
+ fill: 'green',
95
+ shape: 'dot',
96
+ text: `${action || 'response'} → ${identity}`,
97
+ });
98
+
99
+ // Forward message if needed
100
+ if (send) {
101
+ send(msg);
102
+ }
103
+
104
+ if (done) {
105
+ done();
106
+ }
107
+
108
+ } catch (err) {
109
+ node.error(`Output error: ${err.message}`, msg);
110
+ node.status({ fill: 'red', shape: 'dot', text: err.message });
111
+
112
+ if (done) {
113
+ done(err);
114
+ }
115
+ }
116
+ });
117
+
118
+ /**
119
+ * Send response to device via Redis Stream
120
+ */
121
+ async function sendToDevice(tenant, deviceId, response, messageId) {
122
+ const outboundKey = Keys.outbound(tenant, deviceId);
123
+
124
+ // XADD with MAXLEN to prevent unbounded growth
125
+ await redis.xadd(
126
+ outboundKey,
127
+ 'MAXLEN', '~', node.streamMaxLen,
128
+ '*',
129
+ 'messageId', messageId,
130
+ 'data', response,
131
+ 'timestamp', Date.now().toString()
132
+ );
133
+
134
+ node.log(`Sent response to ${outboundKey}: ${messageId}`);
135
+ }
136
+
137
+ // Initial status
138
+ node.status({ fill: 'grey', shape: 'ring', text: 'ready' });
139
+
140
+ // Update status when Redis is ready
141
+ if (redis) {
142
+ redis.once('ready', () => {
143
+ node.status({ fill: 'green', shape: 'ring', text: 'connected' });
144
+ });
145
+
146
+ if (redis.status === 'ready') {
147
+ node.status({ fill: 'green', shape: 'ring', text: 'connected' });
148
+ }
149
+ }
150
+ }
151
+
152
+ RED.nodes.registerType('ocpp-out', OCPPOutNode);
153
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@blokcert/node-red-contrib-ocpp",
3
+ "version": "1.0.0",
4
+ "description": "Node-RED nodes for OCPP (Open Charge Point Protocol) message handling via Redis Streams",
5
+ "keywords": [
6
+ "node-red",
7
+ "ocpp",
8
+ "ocpp1.6",
9
+ "ocpp2.0.1",
10
+ "ev-charging",
11
+ "redis-streams"
12
+ ],
13
+ "author": "BLOKcert",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/MingShyanWei/node-red-contrib-ocpp"
18
+ },
19
+ "node-red": {
20
+ "version": ">=2.0.0",
21
+ "nodes": {
22
+ "ocpp-in": "nodes/ocpp-in.js",
23
+ "ocpp-out": "nodes/ocpp-out.js",
24
+ "ocpp-command": "nodes/ocpp-command.js",
25
+ "ocpp-config": "nodes/ocpp-config.js"
26
+ }
27
+ },
28
+ "dependencies": {
29
+ "ioredis": "^5.3.2"
30
+ },
31
+ "devDependencies": {
32
+ "node-red": "^3.0.0",
33
+ "node-red-node-test-helper": "^0.3.2",
34
+ "mocha": "^10.2.0",
35
+ "chai": "^4.3.7"
36
+ },
37
+ "scripts": {
38
+ "test": "mocha 'test/**/*_spec.js' --timeout 10000"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ }
43
+ }