node-red-contrib-scorp-io 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SCorp-io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # node-red-contrib-scorp-io
2
+
3
+ Node-RED nodes for SCorp-io MQTTS integration.
4
+
5
+ The package exposes a shared SCorp-io configuration node and a multi-device publishing node for:
6
+
7
+ - `DBIRTH` messages: device metric declarations;
8
+ - `DDATA` messages: device metric values with timestamps.
9
+
10
+ ## Requirements
11
+
12
+ - Node.js `>=18.0.0`
13
+ - Node-RED `>=3.0.0`
14
+ - SCorp-io MQTT credentials for production mode
15
+
16
+ ## Installation
17
+
18
+ ### From the Node-RED editor
19
+
20
+ Open **Menu → Manage palette → Install**, then search for:
21
+
22
+ ```text
23
+ node-red-contrib-scorp-io
24
+ ```
25
+
26
+ ### From npm
27
+
28
+ Run from the Node-RED user directory, usually `~/.node-red`:
29
+
30
+ ```bash
31
+ cd ~/.node-red
32
+ npm install node-red-contrib-scorp-io
33
+ ```
34
+
35
+ Restart Node-RED after installation.
36
+
37
+ ## Nodes
38
+
39
+ ### `scorp-io-config`
40
+
41
+ Shared configuration node for the SCorp-io MQTT connection.
42
+
43
+ | Field | Description |
44
+ | --- | --- |
45
+ | Mode | `test` disables MQTT publishing; `production` publishes to the broker |
46
+ | Client ID | MQTT client identifier |
47
+ | Login | MQTT username |
48
+ | Password | MQTT password, stored as a Node-RED credential |
49
+ | Project ID | SCorp-io project identifier used in MQTT topics |
50
+ | Node ID | Edge node identifier used in MQTT topics |
51
+
52
+ Production broker endpoint:
53
+
54
+ ```text
55
+ mqtts://broker-public-prod.scorp-io.com:8883
56
+ ```
57
+
58
+ ### `scorp-io-device`
59
+
60
+ Multi-device node that builds and publishes SCorp-io `DBIRTH` and `DDATA` payloads.
61
+
62
+ #### Input
63
+
64
+ The node has one input.
65
+
66
+ | Message | Behavior |
67
+ | --- | --- |
68
+ | `msg.topic === "birth"` | Emits a `DBIRTH` for all configured devices |
69
+ | `msg.deviceId` set | Emits `DDATA` for the matching configured device |
70
+ | regular payload | Emits `DDATA` for every device where at least one metric path resolves |
71
+
72
+ #### Output
73
+
74
+ The node has one debug output containing the generated message:
75
+
76
+ | Property | Description |
77
+ | --- | --- |
78
+ | `msg.topic` | MQTT topic that was/would be published |
79
+ | `msg.payload` | Generated SCorp-io payload |
80
+ | `msg._scorp.type` | `DBIRTH` or `DDATA` |
81
+ | `msg._scorp.device` | Target device id |
82
+ | `msg._scorp.simulated` | `true` in test mode |
83
+
84
+ ## Metric configuration
85
+
86
+ Each device has a list of metrics.
87
+
88
+ | Metric field | Description | Example |
89
+ | --- | --- | --- |
90
+ | `name` | Metric name sent to SCorp-io | `pompe-1/etats` |
91
+ | `dataType` | Metric type | `Integer`, `Boolean`, `Float` |
92
+ | `valuePath` | Path resolved from incoming `msg` for `DDATA` | `msg.payload.pompe1.etats` |
93
+
94
+ Supported value paths:
95
+
96
+ ```text
97
+ msg.payload.pompe1.etats
98
+ msg.pompe.etat
99
+ pompe1.etats
100
+ ```
101
+
102
+ Paths without the `msg.` prefix are resolved from `msg.payload`.
103
+
104
+ ## Topic format
105
+
106
+ ```text
107
+ mqtts/{PROJECT_ID}/{MESSAGE_TYPE}/{EDGE_NODE_ID}/{DEVICE_ID}
108
+ ```
109
+
110
+ Examples:
111
+
112
+ ```text
113
+ mqtts/my-project/DBIRTH/edge-01/pompe-1
114
+ mqtts/my-project/DDATA/edge-01/pompe-1
115
+ ```
116
+
117
+ ## Payload examples
118
+
119
+ ### DBIRTH
120
+
121
+ ```json
122
+ {
123
+ "metrics": [
124
+ { "name": "pompe-1/etats", "dataType": "Integer" },
125
+ { "name": "pompe-1/defaut", "dataType": "Boolean" }
126
+ ]
127
+ }
128
+ ```
129
+
130
+ ### DDATA
131
+
132
+ ```json
133
+ {
134
+ "metrics": [
135
+ {
136
+ "name": "pompe-1/etats",
137
+ "timestamp": 1710000000000,
138
+ "dataType": "Integer",
139
+ "value": 1
140
+ },
141
+ {
142
+ "name": "pompe-1/defaut",
143
+ "timestamp": 1710000000000,
144
+ "dataType": "Boolean",
145
+ "value": false
146
+ }
147
+ ]
148
+ }
149
+ ```
150
+
151
+ ## Example flow
152
+
153
+ An importable example is provided in:
154
+
155
+ ```text
156
+ examples/scorp-io-basic-flow.json
157
+ ```
158
+
159
+ It runs in `test` mode by default, so it does not publish to the production MQTT broker.
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ npm install
165
+ npm test
166
+ npm run pack:dry-run
167
+ ```
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,124 @@
1
+ [
2
+ {
3
+ "id": "f1f2f3f4f5f6f7f8",
4
+ "type": "tab",
5
+ "label": "SCorp-io basic example",
6
+ "disabled": false,
7
+ "info": "Example flow for @scorp-io/node-red-contrib-scorp-io. Runs in test mode by default."
8
+ },
9
+ {
10
+ "id": "cfg-scorp-io-example",
11
+ "type": "scorp-io-config",
12
+ "name": "SCorp-io test",
13
+ "clientId": "example-client",
14
+ "projectId": "my-project",
15
+ "edgeNodeId": "edge-01",
16
+ "mode": "test",
17
+ "login": ""
18
+ },
19
+ {
20
+ "id": "inject-pompe-1",
21
+ "type": "inject",
22
+ "z": "f1f2f3f4f5f6f7f8",
23
+ "name": "Sample DDATA pompe-1",
24
+ "props": [
25
+ {
26
+ "p": "payload"
27
+ }
28
+ ],
29
+ "repeat": "",
30
+ "crontab": "",
31
+ "once": false,
32
+ "onceDelay": 0.1,
33
+ "topic": "",
34
+ "payload": "{\"pompe1\":{\"etats\":1,\"defaut\":false,\"status\":3.14}}",
35
+ "payloadType": "json",
36
+ "x": 180,
37
+ "y": 120,
38
+ "wires": [
39
+ [
40
+ "scorp-device-example"
41
+ ]
42
+ ]
43
+ },
44
+ {
45
+ "id": "inject-birth",
46
+ "type": "inject",
47
+ "z": "f1f2f3f4f5f6f7f8",
48
+ "name": "Force DBIRTH",
49
+ "props": [
50
+ {
51
+ "p": "topic",
52
+ "vt": "str"
53
+ }
54
+ ],
55
+ "repeat": "",
56
+ "crontab": "",
57
+ "once": false,
58
+ "onceDelay": 0.1,
59
+ "topic": "birth",
60
+ "x": 160,
61
+ "y": 180,
62
+ "wires": [
63
+ [
64
+ "scorp-device-example"
65
+ ]
66
+ ]
67
+ },
68
+ {
69
+ "id": "scorp-device-example",
70
+ "type": "scorp-io-device",
71
+ "z": "f1f2f3f4f5f6f7f8",
72
+ "name": "Example pumps",
73
+ "config": "cfg-scorp-io-example",
74
+ "devices": [
75
+ {
76
+ "deviceId": "pompe-1",
77
+ "metrics": [
78
+ {
79
+ "name": "pompe-1/etats",
80
+ "dataType": "Integer",
81
+ "valuePath": "msg.payload.pompe1.etats"
82
+ },
83
+ {
84
+ "name": "pompe-1/defaut",
85
+ "dataType": "Boolean",
86
+ "valuePath": "msg.payload.pompe1.defaut"
87
+ },
88
+ {
89
+ "name": "pompe-1/status",
90
+ "dataType": "Float",
91
+ "valuePath": "msg.payload.pompe1.status"
92
+ }
93
+ ]
94
+ }
95
+ ],
96
+ "birthOnDeploy": true,
97
+ "birthPeriodic": false,
98
+ "birthInterval": "24h",
99
+ "x": 430,
100
+ "y": 140,
101
+ "wires": [
102
+ [
103
+ "debug-scorp-output"
104
+ ]
105
+ ]
106
+ },
107
+ {
108
+ "id": "debug-scorp-output",
109
+ "type": "debug",
110
+ "z": "f1f2f3f4f5f6f7f8",
111
+ "name": "SCorp-io generated message",
112
+ "active": true,
113
+ "tosidebar": true,
114
+ "console": false,
115
+ "tostatus": false,
116
+ "complete": "true",
117
+ "targetType": "full",
118
+ "statusVal": "",
119
+ "statusType": "auto",
120
+ "x": 710,
121
+ "y": 140,
122
+ "wires": []
123
+ }
124
+ ]
@@ -0,0 +1,43 @@
1
+ {
2
+ "scorp-io-config": {
3
+ "label": {
4
+ "clientid": "Client ID",
5
+ "login": "Login",
6
+ "password": "Password",
7
+ "projectid": "Project ID",
8
+ "edgenodeid": "Edge Node ID"
9
+ },
10
+ "tooltip": {
11
+ "clientid": "Unique MQTT connection identifier",
12
+ "projectid": "SCorp-io project identifier",
13
+ "edgenodeid": "Edge node identifier"
14
+ },
15
+ "status": {
16
+ "connected": "Connected",
17
+ "disconnected": "Disconnected",
18
+ "connecting": "Connecting...",
19
+ "error": "Connection error"
20
+ }
21
+ },
22
+ "scorp-io-device": {
23
+ "label": {
24
+ "deviceid": "Device ID",
25
+ "config": "SCorp-io Config",
26
+ "metrics": "Metrics",
27
+ "metricname": "Name",
28
+ "metricdatatype": "Data Type",
29
+ "metricvalue": "Value (msg.payload path)"
30
+ },
31
+ "placeholder": {
32
+ "deviceid": "e.g. pompe-1",
33
+ "metricname": "e.g. pompe-1/etats",
34
+ "metricvalue": "e.g. pompe1.etats"
35
+ },
36
+ "status": {
37
+ "connected": "Connected",
38
+ "disconnected": "Disconnected",
39
+ "birth_sent": "DBIRTH sent",
40
+ "data_sent": "DDATA sent"
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "scorp-io-config": {
3
+ "label": {
4
+ "clientid": "Client ID",
5
+ "login": "Login",
6
+ "password": "Mot de passe",
7
+ "projectid": "Project ID",
8
+ "edgenodeid": "Edge Node ID"
9
+ },
10
+ "tooltip": {
11
+ "clientid": "Identifiant unique de connexion MQTT",
12
+ "projectid": "Identifiant du projet SCorp-io",
13
+ "edgenodeid": "Identifiant du nœud edge"
14
+ },
15
+ "status": {
16
+ "connected": "Connecté",
17
+ "disconnected": "Déconnecté",
18
+ "connecting": "Connexion...",
19
+ "error": "Erreur de connexion"
20
+ }
21
+ },
22
+ "scorp-io-device": {
23
+ "label": {
24
+ "deviceid": "Device ID",
25
+ "config": "Config SCorp-io",
26
+ "metrics": "Métriques",
27
+ "metricname": "Nom",
28
+ "metricdatatype": "Type de données",
29
+ "metricvalue": "Valeur (chemin msg.payload)"
30
+ },
31
+ "placeholder": {
32
+ "deviceid": "ex: pompe-1",
33
+ "metricname": "ex: pompe-1/etats",
34
+ "metricvalue": "ex: pompe1.etats"
35
+ },
36
+ "status": {
37
+ "connected": "Connecté",
38
+ "disconnected": "Déconnecté",
39
+ "birth_sent": "DBIRTH envoyé",
40
+ "data_sent": "DDATA envoyé"
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,96 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('scorp-io-config', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: '' },
6
+ clientId: { value: '', required: true },
7
+ projectId: { value: '', required: true },
8
+ edgeNodeId: { value: '', required: true },
9
+ mode: { value: 'test' },
10
+ login: { value: '' }
11
+ },
12
+ credentials: {
13
+ password: { type: 'password' }
14
+ },
15
+ label: function() {
16
+ const modeTag = this.mode === 'production' ? '🟢 PROD' : '🧪 TEST';
17
+ return this.name || ('SCorp-io [' + (this.projectId || '?') + '] ' + modeTag);
18
+ },
19
+ oneditprepare: function() {
20
+ // Pré-remplir le mot de passe depuis credentials
21
+ if (this.credentials && this.credentials.password) {
22
+ $('#node-config-input-password').val(this.credentials.password);
23
+ }
24
+ // Afficher/masquer avertissement selon mode
25
+ $('#node-config-input-mode').on('change', function() {
26
+ if ($(this).val() === 'production') {
27
+ $('#scorp-mode-warning').show();
28
+ } else {
29
+ $('#scorp-mode-warning').hide();
30
+ }
31
+ }).trigger('change');
32
+ },
33
+ oneditsave: function() {
34
+ // Sauvegarder le mot de passe dans credentials
35
+ this.credentials = this.credentials || {};
36
+ this.credentials.password = $('#node-config-input-password').val();
37
+ }
38
+ });
39
+ </script>
40
+
41
+ <script type="text/html" data-template-name="scorp-io-config">
42
+
43
+ <div class="form-row">
44
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Nom</label>
45
+ <input type="text" id="node-config-input-name" placeholder="ex: SCorp-io Production" style="width:70%">
46
+ </div>
47
+
48
+ <!-- MODE -->
49
+ <div class="form-row">
50
+ <label for="node-config-input-mode"><i class="fa fa-toggle-on"></i> Mode</label>
51
+ <select id="node-config-input-mode" style="width:70%">
52
+ <option value="test">🧪 Test (simulation, pas d'envoi MQTT)</option>
53
+ <option value="production">🟢 Production (envoi MQTT réel)</option>
54
+ </select>
55
+ </div>
56
+ <div id="scorp-mode-warning" class="form-tips"
57
+ style="display:none; background:#fff3cd; border-left:4px solid #ffc107; padding:8px; margin-bottom:10px;">
58
+ ⚠️ Mode <b>Production</b> activé — les messages MQTT seront envoyés au broker SCorp-io.
59
+ </div>
60
+
61
+ <hr/>
62
+ <div class="form-row">
63
+ <label style="width:100%;font-weight:bold;"><i class="fa fa-plug"></i> Connexion MQTT</label>
64
+ </div>
65
+ <div class="form-tips" style="margin-bottom:12px;">
66
+ Broker fixe : <b>broker-public-prod.scorp-io.com : 8883</b> (TLS automatique)
67
+ </div>
68
+
69
+ <div class="form-row">
70
+ <label for="node-config-input-clientId"><i class="fa fa-id-card"></i> Client ID</label>
71
+ <input type="text" id="node-config-input-clientId" placeholder="ex: my-edge-client-01" style="width:70%">
72
+ </div>
73
+ <div class="form-row">
74
+ <label for="node-config-input-login"><i class="fa fa-user"></i> Login</label>
75
+ <input type="text" id="node-config-input-login" placeholder="Nom d'utilisateur MQTT" style="width:70%" autocomplete="off">
76
+ </div>
77
+ <div class="form-row">
78
+ <label for="node-config-input-password"><i class="fa fa-lock"></i> Mot de passe</label>
79
+ <input type="password" id="node-config-input-password" placeholder="Mot de passe MQTT" style="width:70%" autocomplete="new-password">
80
+ </div>
81
+
82
+ <hr/>
83
+ <div class="form-row">
84
+ <label style="width:100%;font-weight:bold;"><i class="fa fa-sitemap"></i> Identifiants SCorp-io</label>
85
+ </div>
86
+
87
+ <div class="form-row">
88
+ <label for="node-config-input-projectId"><i class="fa fa-folder"></i> Project ID</label>
89
+ <input type="text" id="node-config-input-projectId" placeholder="ex: my-project" style="width:70%">
90
+ </div>
91
+ <div class="form-row">
92
+ <label for="node-config-input-edgeNodeId"><i class="fa fa-microchip"></i> Node ID</label>
93
+ <input type="text" id="node-config-input-edgeNodeId" placeholder="ex: edge-node-01" style="width:70%">
94
+ </div>
95
+
96
+ </script>
@@ -0,0 +1,104 @@
1
+ module.exports = function(RED) {
2
+ const mqtt = require('mqtt');
3
+
4
+ function ScorpIoConfigNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+
7
+ this.clientId = config.clientId;
8
+ this.projectId = config.projectId;
9
+ this.edgeNodeId = config.edgeNodeId;
10
+ this.mode = config.mode || 'test';
11
+ this.login = config.login;
12
+ this.password = this.credentials && this.credentials.password;
13
+ this.brokerUrl = 'mqtts://broker-public-prod.scorp-io.com:8883';
14
+
15
+ this._devices = {};
16
+ this.connected = false;
17
+
18
+ const node = this;
19
+
20
+ // ── API partagée — toujours définie, mode test ou production ──────────
21
+
22
+ node.register = function(deviceNode) {
23
+ node._devices[deviceNode.id] = deviceNode;
24
+ };
25
+
26
+ node.deregister = function(deviceNode) {
27
+ delete node._devices[deviceNode.id];
28
+ };
29
+
30
+ node.publish = function(topic, payload, options, callback) {
31
+ // Compatibilité : publish(topic, payload, callback)
32
+ if (typeof options === 'function') { callback = options; options = {}; }
33
+ options = options || {};
34
+
35
+ if (node.mode === 'test') {
36
+ if (callback) callback(null);
37
+ return;
38
+ }
39
+ if (node.client && node.client.connected) {
40
+ const mqttOpts = { qos: 1, retain: !!options.retain };
41
+ node.client.publish(topic, JSON.stringify(payload), mqttOpts, callback);
42
+ } else {
43
+ node.warn('SCorp-io : tentative de publication hors connexion');
44
+ if (callback) callback(new Error('Non connecté'));
45
+ }
46
+ };
47
+
48
+ // ── Connexion MQTT — uniquement en mode production ────────────────────
49
+
50
+ if (node.mode === 'test') {
51
+ node.log('[MODE TEST] Connexion MQTT désactivée');
52
+ return;
53
+ }
54
+
55
+ const options = {
56
+ clientId: node.clientId,
57
+ username: node.login,
58
+ password: node.password,
59
+ rejectUnauthorized: true,
60
+ reconnectPeriod: 5000,
61
+ connectTimeout: 30000
62
+ };
63
+
64
+ node.client = mqtt.connect(node.brokerUrl, options);
65
+
66
+ node.client.on('connect', function() {
67
+ node.connected = true;
68
+ node.log('SCorp-io connecté à ' + node.brokerUrl);
69
+ Object.values(node._devices).forEach(d => d.onConnected());
70
+ });
71
+
72
+ node.client.on('reconnect', function() {
73
+ node.connected = false;
74
+ node.log('SCorp-io reconnexion en cours...');
75
+ Object.values(node._devices).forEach(d => d.onConnecting());
76
+ });
77
+
78
+ node.client.on('error', function(err) {
79
+ node.connected = false;
80
+ node.error('SCorp-io erreur MQTT : ' + err.message);
81
+ Object.values(node._devices).forEach(d => d.onError(err.message));
82
+ });
83
+
84
+ node.client.on('offline', function() {
85
+ node.connected = false;
86
+ node.log('SCorp-io déconnecté');
87
+ Object.values(node._devices).forEach(d => d.onDisconnected());
88
+ });
89
+
90
+ node.on('close', function(done) {
91
+ if (node.client) {
92
+ node.client.end(true, {}, done);
93
+ } else {
94
+ done();
95
+ }
96
+ });
97
+ }
98
+
99
+ RED.nodes.registerType('scorp-io-config', ScorpIoConfigNode, {
100
+ credentials: {
101
+ password: { type: 'password' }
102
+ }
103
+ });
104
+ };
@@ -0,0 +1,300 @@
1
+ <script type="text/javascript">
2
+
3
+ const SCORP_DATA_TYPES = [
4
+ 'Int8','Int16','Int32','Int64',
5
+ 'UInt8','UInt16','UInt32','UInt64',
6
+ 'Float','Double','Boolean','String','DateTime'
7
+ ];
8
+
9
+ function scorpMakeMetricRow(container, metric) {
10
+ metric = metric || { name: '', dataType: 'String', valuePath: '' };
11
+ const row = $('<div/>').css({
12
+ display: 'flex', gap: '4px', alignItems: 'center', width: '100%', padding: '2px 0'
13
+ }).appendTo(container);
14
+
15
+ $('<input/>', { type: 'text', placeholder: 'ex: pompe-1/etats', class: 'scorp-metric-name' })
16
+ .css({ flex: '2', minWidth: 0 }).val(metric.name || '').appendTo(row);
17
+
18
+ const sel = $('<select/>', { class: 'scorp-metric-datatype' })
19
+ .css({ flex: '1.3', minWidth: 0 }).appendTo(row);
20
+ SCORP_DATA_TYPES.forEach(function(t) {
21
+ $('<option/>', { value: t, text: t }).appendTo(sel);
22
+ });
23
+ sel.val(metric.dataType || 'String');
24
+
25
+ $('<input/>', { type: 'text', placeholder: 'msg.payload.pompe1.etats', class: 'scorp-metric-valuepath' })
26
+ .css({ flex: '2.5', minWidth: 0 }).val(metric.valuePath || '').appendTo(row);
27
+ }
28
+
29
+ function scorpMakeDeviceBlock(container, device) {
30
+ device = device || { deviceId: '', metrics: [] };
31
+
32
+ const block = $('<div/>').addClass('scorp-device-block').css({
33
+ border: '1px solid #ccc', borderRadius: '4px', marginBottom: '8px', background: '#f9f9f9'
34
+ }).appendTo(container);
35
+
36
+ // En-tête
37
+ const header = $('<div/>').css({
38
+ display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 8px',
39
+ background: '#e8f4f8', borderBottom: '1px solid #ccc',
40
+ borderRadius: '4px 4px 0 0', cursor: 'pointer'
41
+ }).appendTo(block);
42
+
43
+ const toggle = $('<span/>').css({
44
+ fontFamily: 'monospace', fontWeight: 'bold', minWidth: '12px', color: '#555'
45
+ }).text('▼').appendTo(header);
46
+
47
+ $('<label/>').css({ fontWeight: 'bold', marginBottom: 0, flex: '0 0 auto' })
48
+ .text('Device ID :').appendTo(header);
49
+
50
+ $('<input/>', { type: 'text', placeholder: 'ex: pompe-1', class: 'scorp-device-id' })
51
+ .css({ flex: '1', minWidth: 0 }).val(device.deviceId || '').appendTo(header);
52
+
53
+ $('<button/>', { type: 'button' })
54
+ .addClass('red-ui-button red-ui-button-small')
55
+ .css({ marginLeft: 'auto', flexShrink: 0 })
56
+ .html('<i class="fa fa-trash"></i>')
57
+ .on('click', function(e) { e.stopPropagation(); block.remove(); })
58
+ .appendTo(header);
59
+
60
+ // Corps
61
+ const body = $('<div/>').css({ padding: '8px' }).appendTo(block);
62
+
63
+ // En-têtes colonnes
64
+ $('<div/>').css({
65
+ display: 'flex', gap: '4px',
66
+ fontSize: '11px', color: '#888', fontWeight: 'bold', padding: '0 0 4px 0'
67
+ }).html(
68
+ '<span style="flex:2">Nom métrique</span>' +
69
+ '<span style="flex:1.3">Type</span>' +
70
+ '<span style="flex:2.5">Chemin (depuis msg)</span>'
71
+ ).appendTo(body);
72
+
73
+ // editableList SANS bouton natif
74
+ const metricList = $('<ol/>').css({ paddingLeft: 0, marginBottom: '6px' }).appendTo(body);
75
+ metricList.editableList({
76
+ addItem: function(rowContainer, i, opt) {
77
+ const m = (opt && opt.name !== undefined) ? opt : { name: '', dataType: 'String', valuePath: '' };
78
+ scorpMakeMetricRow(rowContainer, m);
79
+ },
80
+ removable: true,
81
+ sortable: true,
82
+ addButton: false // ← supprime le bouton natif dupliqué
83
+ });
84
+
85
+ (device.metrics || []).forEach(function(m) { metricList.editableList('addItem', m); });
86
+
87
+ // Bouton unique "Ajouter une métrique"
88
+ $('<button/>', { type: 'button' })
89
+ .addClass('red-ui-button red-ui-button-small')
90
+ .html('<i class="fa fa-plus"></i> Ajouter une métrique')
91
+ .on('click', function() { metricList.editableList('addItem', {}); })
92
+ .appendTo(body);
93
+
94
+ // Toggle collapse
95
+ header.on('click', function(e) {
96
+ if ($(e.target).is('input, button, i')) return;
97
+ if (body.is(':visible')) { body.hide(); toggle.text('▶'); }
98
+ else { body.show(); toggle.text('▼'); }
99
+ });
100
+
101
+ block.data('metricList', metricList);
102
+ return block;
103
+ }
104
+
105
+ RED.nodes.registerType('scorp-io-device', {
106
+ category: 'SCorp-io',
107
+ color: '#00AEEF',
108
+ defaults: {
109
+ name: { value: '' },
110
+ config: { value: '', type: 'scorp-io-config', required: true },
111
+ devices: { value: [] },
112
+ birthOnDeploy: { value: true },
113
+ birthPeriodic: { value: false },
114
+ birthInterval: { value: '24h' }
115
+ },
116
+ inputs: 1,
117
+ outputs: 1,
118
+ icon: 'bridge.svg',
119
+ inputLabels: ['data (msg.topic="birth" pour forcer DBIRTH)'],
120
+ outputLabels: ['debug (topic + payload)'],
121
+ label: function() {
122
+ const n = (this.devices || []).length;
123
+ return this.name || ('SCorp-io — ' + n + ' device' + (n > 1 ? 's' : ''));
124
+ },
125
+ labelStyle: function() { return this.name ? 'node_label_italic' : ''; },
126
+
127
+ oneditprepare: function() {
128
+ const node = this;
129
+
130
+ // ── Devices ──
131
+ const container = $('#scorp-devices-container');
132
+ (node.devices || []).forEach(function(d) { scorpMakeDeviceBlock(container, d); });
133
+ $('#scorp-add-device').on('click', function() {
134
+ scorpMakeDeviceBlock(container, { deviceId: '', metrics: [] });
135
+ });
136
+
137
+ // ── DBIRTH au déploiement ──
138
+ $('#scorp-birth-on-deploy').prop('checked', node.birthOnDeploy !== false);
139
+
140
+ // ── DBIRTH périodique ──
141
+ const periodicChk = $('#scorp-birth-periodic');
142
+ const intervalSel = $('#scorp-birth-interval');
143
+
144
+ periodicChk.prop('checked', !!node.birthPeriodic);
145
+ intervalSel.val(node.birthInterval || '24h');
146
+ intervalSel.prop('disabled', !node.birthPeriodic);
147
+
148
+ periodicChk.on('change', function() {
149
+ intervalSel.prop('disabled', !$(this).prop('checked'));
150
+ });
151
+
152
+ // ── Bouton forcer DBIRTH ──
153
+ const btn = $('#scorp-btn-dbirth');
154
+ const status = $('#scorp-btn-dbirth-status');
155
+
156
+ if (!node.id) { btn.prop('disabled', true); }
157
+
158
+ btn.on('click', function() {
159
+ btn.prop('disabled', true);
160
+ status.text('Envoi…').css('color', '#aaa');
161
+ $.ajax({
162
+ method: 'POST',
163
+ url: 'scorp-io/dbirth/' + node.id,
164
+ success: function(data) {
165
+ status.text('✅ DBIRTH envoyé : ' + data.devices.join(', ')).css('color', 'green');
166
+ btn.prop('disabled', false);
167
+ },
168
+ error: function(xhr) {
169
+ const msg = xhr.responseJSON ? xhr.responseJSON.error : xhr.statusText;
170
+ status.text('❌ ' + msg).css('color', 'red');
171
+ btn.prop('disabled', false);
172
+ }
173
+ });
174
+ });
175
+ },
176
+
177
+ oneditsave: function() {
178
+ // Devices
179
+ const devices = [];
180
+ $('#scorp-devices-container .scorp-device-block').each(function() {
181
+ const block = $(this);
182
+ const deviceId = block.find('.scorp-device-id').val().trim();
183
+ if (!deviceId) return;
184
+ const metrics = [];
185
+ const metricList = block.data('metricList');
186
+ if (metricList) {
187
+ metricList.editableList('items').each(function() {
188
+ const name = $(this).find('.scorp-metric-name').val().trim();
189
+ const dataType = $(this).find('.scorp-metric-datatype').val();
190
+ const valuePath = $(this).find('.scorp-metric-valuepath').val().trim();
191
+ if (name) metrics.push({ name, dataType, valuePath });
192
+ });
193
+ }
194
+ devices.push({ deviceId, metrics });
195
+ });
196
+ this.devices = devices;
197
+
198
+ // DBIRTH config
199
+ this.birthOnDeploy = $('#scorp-birth-on-deploy').prop('checked');
200
+ this.birthPeriodic = $('#scorp-birth-periodic').prop('checked');
201
+ this.birthInterval = $('#scorp-birth-interval').val();
202
+ }
203
+ });
204
+ </script>
205
+
206
+ <script type="text/html" data-template-name="scorp-io-device">
207
+
208
+ <div class="form-row">
209
+ <label for="node-input-name"><i class="fa fa-tag"></i> Nom</label>
210
+ <input type="text" id="node-input-name" placeholder="ex: Mes pompes">
211
+ </div>
212
+ <div class="form-row">
213
+ <label for="node-input-config"><i class="fa fa-cog"></i> Config</label>
214
+ <input type="text" id="node-input-config">
215
+ </div>
216
+
217
+ <hr/>
218
+
219
+ <!-- DEVICES -->
220
+ <div class="form-row">
221
+ <label style="width:100%; font-weight:bold; margin-bottom:6px;">
222
+ <i class="fa fa-microchip"></i> Devices
223
+ </label>
224
+ <div id="scorp-devices-container" style="width:100%;"></div>
225
+ <button type="button" id="scorp-add-device" class="red-ui-button" style="margin-top:6px;">
226
+ <i class="fa fa-plus"></i> Ajouter un device
227
+ </button>
228
+ </div>
229
+
230
+ <hr/>
231
+
232
+ <!-- STRATÉGIE DBIRTH -->
233
+ <div class="form-row" style="width:100%;">
234
+ <label style="width:100%; font-weight:bold; margin-bottom:8px;">
235
+ <i class="fa fa-refresh"></i> Émission DBIRTH
236
+ </label>
237
+
238
+ <!-- Option A : au déploiement -->
239
+ <div style="display:flex; align-items:center; gap:8px; margin-bottom:10px; width:100%;">
240
+ <input type="checkbox" id="scorp-birth-on-deploy" style="width:auto; margin:0; flex-shrink:0;">
241
+ <label for="scorp-birth-on-deploy" style="margin:0; font-weight:normal; flex:1;">
242
+ Envoyer un DBIRTH automatiquement après déploiement / redémarrage
243
+ </label>
244
+ </div>
245
+
246
+ <!-- Option B : périodique -->
247
+ <div style="display:flex; align-items:center; gap:8px; width:100%;">
248
+ <input type="checkbox" id="scorp-birth-periodic" style="width:auto; margin:0; flex-shrink:0;">
249
+ <label for="scorp-birth-periodic" style="margin:0; font-weight:normal;">
250
+ Envoyer un DBIRTH périodiquement toutes les
251
+ </label>
252
+ <select id="scorp-birth-interval" style="width:auto; flex-shrink:0;">
253
+ <option value="1h">1 heure</option>
254
+ <option value="6h">6 heures</option>
255
+ <option value="12h">12 heures</option>
256
+ <option value="24h">24 heures</option>
257
+ <option value="48h">48 heures</option>
258
+ </select>
259
+ </div>
260
+ </div>
261
+
262
+ <hr/>
263
+
264
+ <!-- BOUTON FORCER DBIRTH -->
265
+ <div class="form-row">
266
+ <label style="width:100%; font-weight:bold; margin-bottom:6px;">
267
+ <i class="fa fa-bolt"></i> Actions
268
+ </label>
269
+ <button type="button" id="scorp-btn-dbirth" class="red-ui-button">
270
+ <i class="fa fa-refresh"></i> Forcer DBIRTH maintenant
271
+ </button>
272
+ <span id="scorp-btn-dbirth-status" style="margin-left:10px; font-size:12px;"></span>
273
+ </div>
274
+
275
+ <hr/>
276
+
277
+ <div class="form-tips">
278
+ <b>Entrée :</b> message normal → <b>DDATA</b> auto &nbsp;|&nbsp;
279
+ <code>msg.topic = "birth"</code> → <b>DBIRTH</b><br/>
280
+ <b>Chemins :</b> <code>msg.payload.pompe1.etats</code> ou <code>msg.pompe.etat</code>
281
+ </div>
282
+
283
+ </script>
284
+
285
+ <script type="text/html" data-help-name="scorp-io-device">
286
+ <p>Nœud SCorp-io multi-devices. Publie <b>DBIRTH</b> et <b>DDATA</b> via MQTTS.</p>
287
+ <h3>Entrée</h3>
288
+ <dl class="message-properties">
289
+ <dt>msg.topic <span class="property-type">string</span></dt>
290
+ <dd><code>"birth"</code> → force DBIRTH pour tous les devices.</dd>
291
+ <dt>msg.payload <span class="property-type">object</span></dt>
292
+ <dd>Routage automatique : DDATA publié pour chaque device dont une métrique est résolvable.</dd>
293
+ </dl>
294
+ <h3>Sortie — debug</h3>
295
+ <dl class="message-properties">
296
+ <dt>msg.topic</dt><dd>Topic MQTT publié.</dd>
297
+ <dt>msg.payload</dt><dd>Trame publiée.</dd>
298
+ <dt>msg._scorp</dt><dd>type, mode, device, simulated.</dd>
299
+ </dl>
300
+ </script>
@@ -0,0 +1,281 @@
1
+ module.exports = function(RED) {
2
+
3
+ // ── resolvePath ───────────────────────────────────────────────────────────
4
+ // Résout un chemin depuis la racine msg
5
+ // "msg.payload.pompe1.etats" → msg.payload.pompe1.etats
6
+ // "msg.pompe.etat" → msg.pompe.etat
7
+ // "pompe1.etats" → fallback sur msg.payload.pompe1.etats
8
+
9
+ function resolvePath(msg, rawPath) {
10
+ if (!rawPath || !msg) return undefined;
11
+ let path = rawPath.trim();
12
+ if (path.startsWith('msg.')) {
13
+ path = path.slice(4);
14
+ } else {
15
+ path = 'payload.' + path;
16
+ }
17
+ try {
18
+ return path.split('.').reduce(function(acc, key) {
19
+ if (acc === undefined || acc === null) return undefined;
20
+ return acc[key];
21
+ }, msg);
22
+ } catch(e) { return undefined; }
23
+ }
24
+
25
+ // ── deviceIsRelevant ──────────────────────────────────────────────────────
26
+ // Retourne true si au moins une métrique du device est résolvable dans msg
27
+
28
+ function deviceIsRelevant(device, msg) {
29
+ if (msg && msg.deviceId && msg.deviceId === device.deviceId) {
30
+ return true;
31
+ }
32
+ return device.metrics.some(function(m) {
33
+ return m.valuePath && resolvePath(msg, m.valuePath) !== undefined;
34
+ });
35
+ }
36
+
37
+ // ── buildTopic ────────────────────────────────────────────────────────────
38
+
39
+ function buildTopic(projectId, edgeNodeId, deviceId, msgType) {
40
+ return ['mqtts', projectId, msgType, edgeNodeId, deviceId].join('/');
41
+ }
42
+
43
+ // ── ScorpIoDeviceNode ─────────────────────────────────────────────────────
44
+
45
+ function ScorpIoDeviceNode(config) {
46
+ RED.nodes.createNode(this, config);
47
+
48
+ this.devices = config.devices || [];
49
+ this.birthOnDeploy = config.birthOnDeploy !== false; // true par défaut
50
+ this.birthPeriodic = !!config.birthPeriodic;
51
+ this.birthInterval = config.birthInterval || '24h';
52
+ this.configId = config.config;
53
+ this.config = RED.nodes.getNode(this.configId);
54
+
55
+ const node = this;
56
+ const isTest = !node.config || node.config.mode === 'test';
57
+
58
+ if (!node.config) {
59
+ node.status({ fill: 'red', shape: 'ring', text: 'Config manquante' });
60
+ node.error('Nœud de configuration SCorp-io introuvable');
61
+ return;
62
+ }
63
+
64
+ // ── Constructeurs de payload ──────────────────────────────────────────
65
+
66
+ function buildDBirth(device) {
67
+ return {
68
+ metrics: device.metrics.map(m => ({
69
+ name: m.name,
70
+ dataType: m.dataType
71
+ }))
72
+ };
73
+ }
74
+
75
+ function buildDData(device, msg) {
76
+ return {
77
+ metrics: device.metrics.map(function(m) {
78
+ const value = resolvePath(msg, m.valuePath);
79
+ const timestamp = Date.now();
80
+ if (value === undefined && m.valuePath) {
81
+ node.warn(
82
+ '[' + device.deviceId + '] Métrique "' + m.name + '" : ' +
83
+ 'chemin "' + m.valuePath + '" introuvable dans msg.'
84
+ );
85
+ }
86
+ return { name: m.name, timestamp: timestamp, dataType: m.dataType, value: value };
87
+ })
88
+ };
89
+ }
90
+
91
+ // ── Debug output ──────────────────────────────────────────────────────
92
+
93
+ function emitDebug(msgType, deviceId, topic, payload) {
94
+ node.send({
95
+ topic: topic,
96
+ payload: payload,
97
+ _scorp: {
98
+ type: msgType,
99
+ mode: node.config.mode,
100
+ device: deviceId,
101
+ simulated: isTest
102
+ }
103
+ });
104
+ }
105
+
106
+ // ── Publication DBIRTH (1 device) ─────────────────────────────────────
107
+
108
+ function sendBirthForDevice(device) {
109
+ const topic = buildTopic(node.config.projectId, node.config.edgeNodeId, device.deviceId, 'DBIRTH');
110
+ const payload = buildDBirth(device);
111
+
112
+ node.log('DBIRTH [' + device.deviceId + '] → ' + topic);
113
+
114
+ if (isTest) {
115
+ emitDebug('DBIRTH', device.deviceId, topic, payload);
116
+ return;
117
+ }
118
+
119
+ node.config.publish(topic, payload, { retain: true }, function(err) {
120
+ if (err) {
121
+ node.error('Erreur DBIRTH [' + device.deviceId + '] : ' + err.message);
122
+ } else {
123
+ node.log('DBIRTH [' + device.deviceId + '] publié');
124
+ emitDebug('DBIRTH', device.deviceId, topic, payload);
125
+ }
126
+ });
127
+ }
128
+
129
+ // ── Publication DBIRTH (tous les devices) ─────────────────────────────
130
+
131
+ node.sendAllBirths = function() {
132
+ if (node.devices.length === 0) {
133
+ node.warn('Aucun device configuré, DBIRTH ignoré');
134
+ return;
135
+ }
136
+ node.devices.forEach(sendBirthForDevice);
137
+
138
+ if (isTest) {
139
+ node.status({ fill: 'blue', shape: 'ring', text: '🧪 DBIRTH x' + node.devices.length + ' simulé' });
140
+ } else {
141
+ node.status({ fill: 'green', shape: 'dot', text: 'DBIRTH x' + node.devices.length + ' envoyé' });
142
+ }
143
+ };
144
+
145
+ // ── Publication DDATA — routage automatique par correspondance ─────────
146
+ // Pour chaque device, on vérifie si au moins une métrique est résolvable
147
+ // dans le msg. Si oui → DDATA publié pour ce device.
148
+ // Si aucun device matche → warn.
149
+
150
+ node.sendDataAuto = function(msg) {
151
+ const matched = node.devices.filter(d => deviceIsRelevant(d, msg));
152
+
153
+ if (matched.length === 0) {
154
+ node.warn(
155
+ 'Aucun device ne correspond au msg reçu. ' +
156
+ 'Vérifier les chemins de métriques configurés. ' +
157
+ 'msg.payload : ' + JSON.stringify(msg.payload)
158
+ );
159
+ return;
160
+ }
161
+
162
+ matched.forEach(function(device) {
163
+ const topic = buildTopic(node.config.projectId, node.config.edgeNodeId, device.deviceId, 'DDATA');
164
+ const data = buildDData(device, msg);
165
+
166
+ node.log('DDATA [' + device.deviceId + '] → ' + topic);
167
+
168
+ if (isTest) {
169
+ node.status({ fill: 'blue', shape: 'dot', text: '🧪 DDATA [' + device.deviceId + '] simulé' });
170
+ emitDebug('DDATA', device.deviceId, topic, data);
171
+ return;
172
+ }
173
+
174
+ node.config.publish(topic, data, { retain: false }, function(err) {
175
+ if (err) {
176
+ node.error('Erreur DDATA [' + device.deviceId + '] : ' + err.message);
177
+ node.status({ fill: 'red', shape: 'dot', text: 'Erreur DDATA' });
178
+ } else {
179
+ node.status({ fill: 'green', shape: 'dot', text: 'DDATA [' + device.deviceId + '] envoyé' });
180
+ emitDebug('DDATA', device.deviceId, topic, data);
181
+ }
182
+ });
183
+ });
184
+ };
185
+
186
+ // ── États connexion ───────────────────────────────────────────────────
187
+
188
+ node.onConnected = function() {
189
+ node.status({ fill: 'green', shape: 'ring', text: 'Connecté' });
190
+ if (node.birthOnDeploy) node.sendAllBirths();
191
+ };
192
+
193
+ node.onConnecting = function() {
194
+ node.status({ fill: 'yellow', shape: 'ring', text: 'Connexion...' });
195
+ };
196
+
197
+ node.onDisconnected = function() {
198
+ node.status({ fill: 'red', shape: 'ring', text: 'Déconnecté' });
199
+ };
200
+
201
+ node.onError = function(errMsg) {
202
+ node.status({ fill: 'red', shape: 'dot', text: 'Erreur: ' + errMsg });
203
+ };
204
+
205
+ // ── Init ──────────────────────────────────────────────────────────────
206
+
207
+ node.config.register(node);
208
+
209
+ if (isTest) {
210
+ const deviceLabel = node.devices.length + ' device' + (node.devices.length > 1 ? 's' : '');
211
+ node.status({ fill: 'blue', shape: 'ring', text: '🧪 Test — ' + deviceLabel });
212
+ if (node.birthOnDeploy) node.sendAllBirths();
213
+ } else if (node.config.client && node.config.client.connected) {
214
+ node.onConnected();
215
+ } else {
216
+ node.status({ fill: 'yellow', shape: 'ring', text: 'Connexion...' });
217
+ }
218
+
219
+ // ── DBIRTH périodique ─────────────────────────────────────────────────
220
+
221
+ const INTERVAL_MAP = {
222
+ '1h': 3600000,
223
+ '6h': 21600000,
224
+ '12h': 43200000,
225
+ '24h': 86400000,
226
+ '48h': 172800000
227
+ };
228
+
229
+ if (node.birthPeriodic) {
230
+ const ms = INTERVAL_MAP[node.birthInterval] || INTERVAL_MAP['24h'];
231
+ node._birthTimer = setInterval(function() {
232
+ node.log('DBIRTH périodique (' + node.birthInterval + ')');
233
+ node.sendAllBirths();
234
+ }, ms);
235
+ node.log('DBIRTH périodique activé : toutes les ' + node.birthInterval);
236
+ }
237
+
238
+ // ── Entrée ────────────────────────────────────────────────────────────
239
+ // msg.topic === 'birth' → DBIRTH pour tous les devices
240
+ // sinon → DDATA routage automatique
241
+
242
+ node.on('input', function(msg, send, done) {
243
+ if (msg.topic === 'birth' || msg._inputPort === 1) {
244
+ node.sendAllBirths();
245
+ } else {
246
+ node.sendDataAuto(msg);
247
+ }
248
+ done();
249
+ });
250
+
251
+ node.on('close', function(done) {
252
+ if (node._birthTimer) {
253
+ clearInterval(node._birthTimer);
254
+ node._birthTimer = null;
255
+ }
256
+ node.config.deregister(node);
257
+ done();
258
+ });
259
+ }
260
+
261
+ RED.nodes.registerType('scorp-io-device', ScorpIoDeviceNode);
262
+
263
+ // ── Endpoint HTTP admin — bouton DBIRTH depuis l'UI ───────────────────────
264
+ // POST /scorp-io/dbirth/:nodeId
265
+
266
+ RED.httpAdmin.post('/scorp-io/dbirth/:nodeId', function(req, res) {
267
+ const node = RED.nodes.getNode(req.params.nodeId);
268
+ if (!node) {
269
+ return res.status(404).json({ error: 'Nœud introuvable : ' + req.params.nodeId });
270
+ }
271
+ if (typeof node.sendAllBirths !== 'function') {
272
+ return res.status(400).json({ error: 'Ce nœud ne supporte pas sendAllBirths' });
273
+ }
274
+ try {
275
+ node.sendAllBirths();
276
+ res.json({ ok: true, devices: (node.devices || []).map(d => d.deviceId) });
277
+ } catch(e) {
278
+ res.status(500).json({ error: e.message });
279
+ }
280
+ });
281
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "node-red-contrib-scorp-io",
3
+ "version": "0.1.0",
4
+ "description": "Node-RED nodes for SCorp-io MQTTS integration with DBIRTH and DDATA messages.",
5
+ "keywords": [
6
+ "node-red",
7
+ "node-red-contrib",
8
+ "scorp-io",
9
+ "mqtt",
10
+ "mqtts",
11
+ "iot",
12
+ "industrial"
13
+ ],
14
+ "author": "SCorp-io",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+ssh://git@github.com/SCORPIO-JRB/node-red-contrib-scorp-io.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/SCORPIO-JRB/node-red-contrib-scorp-io/issues"
22
+ },
23
+ "homepage": "https://github.com/SCORPIO-JRB/node-red-contrib-scorp-io#readme",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "nodes/",
29
+ "locales/",
30
+ "examples/",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "node-red": {
35
+ "version": ">=3.0.0",
36
+ "nodes": {
37
+ "scorp-io-config": "nodes/scorp-io-config.js",
38
+ "scorp-io-device": "nodes/scorp-io-device.js"
39
+ }
40
+ },
41
+ "dependencies": {
42
+ "mqtt": "^5.3.4"
43
+ },
44
+ "devDependencies": {
45
+ "mocha": "^10.4.0",
46
+ "node-red": "^3.1.15",
47
+ "node-red-node-test-helper": "^0.3.4",
48
+ "should": "^13.2.3"
49
+ },
50
+ "scripts": {
51
+ "test": "mocha test/**/*_spec.js --timeout 5000",
52
+ "test:helpers": "mocha test/scorp-io-helpers_spec.js --timeout 5000",
53
+ "test:config": "mocha test/scorp-io-config_spec.js --timeout 5000",
54
+ "test:device": "mocha test/scorp-io-device_spec.js --timeout 5000",
55
+ "pack:dry-run": "npm pack --dry-run"
56
+ },
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ }
60
+ }