node-red-contrib-ta-cmi-coe 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +28 -17
  2. package/README.de.md +225 -0
  3. package/README.md +134 -187
  4. package/coe/coe-input.js +49 -36
  5. package/coe/coe-monitor.html +1 -1
  6. package/coe/coe-monitor.js +32 -41
  7. package/coe/coe-output.html +67 -28
  8. package/coe/coe-output.js +159 -24
  9. package/coe/config.js +14 -7
  10. package/coe/locales/de/coe-input.html +2 -2
  11. package/coe/locales/de/coe-input.json +8 -2
  12. package/coe/locales/de/coe-monitor.html +1 -2
  13. package/coe/locales/de/coe-monitor.json +9 -4
  14. package/coe/locales/de/coe-output.html +8 -2
  15. package/coe/locales/de/coe-output.json +11 -5
  16. package/coe/locales/de/config.html +3 -3
  17. package/coe/locales/de/config.json +1 -1
  18. package/coe/locales/en-US/coe-input.html +2 -2
  19. package/coe/locales/en-US/coe-input.json +8 -2
  20. package/coe/locales/en-US/coe-monitor.html +1 -2
  21. package/coe/locales/en-US/coe-monitor.json +9 -4
  22. package/coe/locales/en-US/coe-output.html +8 -2
  23. package/coe/locales/en-US/coe-output.json +10 -4
  24. package/coe/locales/en-US/config.html +3 -3
  25. package/coe/locales/en-US/config.json +1 -1
  26. package/coe/units-config.js +2 -1
  27. package/examples/Example Flow.json +56 -0
  28. package/lib/coe-v1.js +152 -0
  29. package/lib/coe-v2.js +78 -116
  30. package/lib/old.js +133 -0
  31. package/lib/protocol.js +19 -0
  32. package/lib/queueing.js +92 -137
  33. package/lib/units.js +7 -7
  34. package/lib/utils.js +71 -66
  35. package/package.json +14 -2
  36. package/__tests__/blockinfo.test.js +0 -24
  37. package/__tests__/conversion.test.js +0 -22
  38. package/__tests__/udp.test.js +0 -46
  39. package/lib/coe.js +0 -109
package/lib/old.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * CoE V2 Protocol Support Module
3
+ *
4
+ * Copyright 2025 Florian Mayrhofer
5
+ * Licensed under the Apache License, Version 2.0
6
+ *
7
+ */
8
+
9
+ // Convert V2 Data into Uniform format
10
+ function convertV2ToUniformFormat(v2Data) {
11
+ // Group Outputs by block
12
+ const blockMap = {};
13
+
14
+ v2Data.blocks.forEach(block => {
15
+ const isDigital = block.outputNumber <= 254;
16
+ const actualOutput = isDigital ? block.outputNumber : (block.outputNumber - 255);
17
+
18
+ // Determine block number and position
19
+ let blockNumber, position;
20
+
21
+ if (isDigital) {
22
+ // Digital: Output 1-16 → Block 0, Output 17-32 → Block 9
23
+ if (actualOutput <= 16) {
24
+ blockNumber = 0;
25
+ position = actualOutput - 1;
26
+ } else {
27
+ blockNumber = 9;
28
+ position = actualOutput - 17;
29
+ }
30
+ } else {
31
+ // Analog: Output 1-4 → Block 1, 5-8 → Block 2, etc.
32
+ blockNumber = Math.floor((actualOutput - 1) / 4) + 1;
33
+ position = (actualOutput - 1) % 4;
34
+ }
35
+
36
+ const key = `${block.nodeNumber}-${blockNumber}`;
37
+
38
+ if (!blockMap[key]) {
39
+ blockMap[key] = {
40
+ nodeNumber: block.nodeNumber,
41
+ blockNumber: blockNumber,
42
+ dataType: isDigital ? 'digital' : 'analog',
43
+ values: isDigital ? new Array(16).fill(undefined) : new Array(4).fill(undefined),
44
+ units: isDigital ? new Array(16).fill(undefined) : new Array(4).fill(undefined)
45
+ };
46
+ }
47
+
48
+ // Convert value & insert (V2 uses other decimals)
49
+ const convertedValue = convertRawToValue(block.value, block.unitId, 2);
50
+ blockMap[key].values[position] = isDigital ? (block.value ? 1 : 0) : convertedValue;
51
+
52
+ if (blockMap[key].units) {
53
+ blockMap[key].units[position] = block.unitId;
54
+ }
55
+ });
56
+ return Object.values(blockMap);
57
+ }
58
+
59
+ // Create CoE V2 Packet
60
+ function createPacket(nodeNumber, dataType, outputs) {
61
+ // outputState: Array von {outputNumber, unit, value}
62
+ // Max 16 value blocks
63
+ const outputState = convertUniformToV2Format(dataType, outputs);
64
+
65
+ const blockCount = Math.min(outputState.length, 16);
66
+ const messageLength = 4 + (blockCount * 8);
67
+
68
+ const buffer = Buffer.alloc(messageLength);
69
+
70
+ // Write header
71
+ buffer.writeUInt8(0x02, 0); // Version Low
72
+ buffer.writeUInt8(0x00, 1); // Version High
73
+ buffer.writeUInt8(messageLength, 2); // Message Length
74
+ buffer.writeUInt8(blockCount, 3); // Block Count
75
+
76
+ // Write value blocks
77
+ for (let i = 0; i < blockCount; i++) {
78
+ const offset = 4 + (i * 8);
79
+ const output = outputState[i];
80
+
81
+ buffer.writeUInt8(nodeNumber, offset);
82
+
83
+ // Output Number (Little Endian, 2 Bytes)
84
+ buffer.writeUInt8(output.outputNumber & 0xFF, offset + 1);
85
+ buffer.writeUInt8((output.outputNumber >> 8) & 0xFF, offset + 2);
86
+
87
+ buffer.writeUInt8(output.unitId || 0, offset + 3); // Unit ID
88
+ buffer.writeInt32LE(output.value, offset + 4); // Value (Int32 LE)
89
+ }
90
+ return buffer;
91
+ }
92
+
93
+ // Convert Uniform format to V2 outputs
94
+ // function convertUniformToV2Format(blockNumber, values, units, dataType) {
95
+ function convertUniformToV2Format(dataType, outputs) {
96
+ let outputState = [];
97
+
98
+ if (dataType === 'digital') {
99
+ // Digital: 16 Bits
100
+ const baseOutput = blockNumber === 0 ? 1 : 17;
101
+ for (let i = 0; i < values.length; i++) {
102
+ if (values[i] !== undefined) {
103
+ const unitId = units && units[i] !== undefined ? units[i] : 0;
104
+
105
+ outputState.push({
106
+ outputNumber: baseOutput + i,
107
+ unitId: unitId,
108
+ value: values[i] ? 1 : 0
109
+ });
110
+ }
111
+ }
112
+ } else {
113
+ // Analog: 4 Values
114
+ const baseOutput = (blockNumber - 1) * 4 + 1;
115
+ for (let i = 0; i < 4; i++) {
116
+ if (values[i] !== undefined) {
117
+ const unitId = units ? units[i] : 0;
118
+ const rawValue = convertValueToRaw(values[i], unitId, 2); // V2 uses other decimals
119
+
120
+ // Output > 255 = analog
121
+ const outputNumber = baseOutput + i + 255;
122
+
123
+ outputState.push({
124
+ outputNumber: outputNumber,
125
+ unitId: unitId,
126
+ value: rawValue
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ return outputState;
133
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * CoE Protocol Parsing and Creation Module
3
+ *
4
+ * Copyright 2025 Florian Mayrhofer
5
+ * Licensed under the Apache License, Version 2.0
6
+ *
7
+ */
8
+
9
+ module.exports = {
10
+ // Parsing functions for CoE V1 and V2
11
+ createPacket: {
12
+ 1: require('./coe-v1.js').createPacket,
13
+ 2: require('./coe-v2.js').createPacket
14
+ },
15
+ parsePacket: {
16
+ 1: require('./coe-v1.js').parsePacket,
17
+ 2: require('./coe-v2.js').parsePacket
18
+ }
19
+ };
package/lib/queueing.js CHANGED
@@ -6,115 +6,69 @@
6
6
  *
7
7
  */
8
8
 
9
- const { createCoEPacket } = require('../lib/coe');
9
+ const { createPacket } = require('./protocol');
10
10
 
11
- const blockStateCache = {};
12
- const blockUnitsCache = {}; // Cache for units per block
13
- const blockQueues = {};
14
11
  const blockTimers = {};
15
12
  const DEBOUNCE_DELAY = 50; // ms (Time slot for message collection)
13
+ const outputStateCache = {};
14
+ const queuedState = {};
16
15
 
17
- // Returns a copy of the current block state (values) for the given block
18
- function getBlockState(nodeNumber, blockNumber, dataType) {
19
- const key = `${nodeNumber}-${blockNumber}-${dataType}`;
20
- if (!blockStateCache[key]) {
21
- if (dataType === 'analog') {
22
- blockStateCache[key] = [0, 0, 0, 0];
23
- } else {
24
- blockStateCache[key] = new Array(16).fill(0);
25
- }
26
- }
27
- // Return a copy to avoid accidental external mutation
28
- return Array.isArray(blockStateCache[key]) ? [...blockStateCache[key]] : blockStateCache[key];
29
- }
30
-
31
- // Sets the current block state (values) for the given block
32
- function setBlockState(nodeNumber, blockNumber, dataType, values) {
33
- const key = `${nodeNumber}-${blockNumber}-${dataType}`;
34
- blockStateCache[key] = [...values];
16
+ // Persist output state for a specific block
17
+ function setOutputState(queueKey, output) {
18
+ outputStateCache[queueKey] = {
19
+ ...outputStateCache[queueKey],
20
+ outputs: output
21
+ };
35
22
  }
36
23
 
37
- // Returns a copy of the current block units for the given block (analog only)
38
- function getBlockUnits(nodeNumber, blockNumber, dataType) {
39
- const key = `${nodeNumber}-${blockNumber}-${dataType}`;
40
- if (!blockUnitsCache[key]) {
41
- // Only analog blocks have units
42
- if (dataType === 'analog') {
43
- blockUnitsCache[key] = [0, 0, 0, 0];
44
- } else {
45
- blockUnitsCache[key] = null;
24
+ // Return a copy of the current message state
25
+ function getOutputState(node, queueKey) {
26
+ if (!outputStateCache[queueKey]) {
27
+ outputStateCache[queueKey] = {
28
+ nodeNumber: node.nodeNumber,
29
+ dataType: node.dataType,
30
+ outputs: {}
46
31
  }
47
32
  }
48
- return blockUnitsCache[key] ? [...blockUnitsCache[key]] : null;
33
+ return { ...outputStateCache[queueKey] };
49
34
  }
50
35
 
51
- // Sets the current block units for the given block (analog only)
52
- function setBlockUnits(nodeNumber, blockNumber, dataType, units) {
53
- const key = `${nodeNumber}-${blockNumber}-${dataType}`;
54
- if (dataType === 'analog') {
55
- blockUnitsCache[key] = units ? [...units] : [0,0,0,0];
56
- } else {
57
- blockUnitsCache[key] = null;
58
- }
59
- }
60
-
61
- // Generates a unique key for the queue based on node, block, and data type
62
- function getQueueKey(nodeNumber, blockNumber, dataType) {
63
- return `${nodeNumber}-${blockNumber}-${dataType}`;
64
- }
65
36
 
66
37
  // Queues and debounces messages for a specific block
67
- function queueAndSend(node, translate, nodeNumber, blockNumber, values, units, dataType, version, cmiConfig, cmiAddress, origMsg) {
68
- const queueKey = getQueueKey(nodeNumber, blockNumber, dataType);
38
+ function queueAndSend(node) {
39
+ const valueState = {output: node.outputNumber, value: node.lastReceivedValue, unit: node.lastReceivedUnit};
40
+ const origMsg = node.lastReceivedMsg;
41
+ const queueKey = node.queueKey;
42
+ const coeVersion = node.coeVersion;
43
+ const cmiConfig = node.cmiConfig;
44
+ const cmiAddress = node.cmiAddress;
69
45
 
70
46
  // New queueing logic
71
- let baseValues;
72
- let baseUnits;
47
+ let initialOutputState;
48
+ let participatingNodes = new Set();
73
49
 
74
- if (blockQueues[queueKey]) {
75
- baseValues = [...blockQueues[queueKey].values];
76
- baseUnits = blockQueues[queueKey].units ? [...blockQueues[queueKey].units] : null;
50
+ if (queuedState[queueKey]) {
51
+ participatingNodes = queuedState[queueKey].participating.add(node);
77
52
  } else {
78
- baseValues = getBlockState(nodeNumber, blockNumber, dataType);
79
- baseUnits = (dataType === 'analog') ? getBlockUnits(nodeNumber, blockNumber, dataType) : null;
53
+ queuedState[queueKey] = {
54
+ timestamp: Date.now(),
55
+ participating: participatingNodes.add(node),
56
+ origMsg: origMsg || null
57
+ }
80
58
  }
81
59
 
60
+ initialOutputState = getOutputState(node, queueKey).outputs;
61
+
82
62
  // Merge incoming values/units with existing block state
83
- let mergedValues = baseValues;
84
- let mergedUnits = baseUnits;
85
-
86
- if (dataType === 'analog') {
87
- for (let i = 0; i < 4; i++) {
88
- if (values[i] !== undefined && values[i] !== null) {
89
- mergedValues[i] = values[i];
90
- }
91
- if (units && units[i] !== undefined) {
92
- mergedUnits[i] = units[i];
93
- }
94
- }
95
- } else {
96
- for (let i = 0; i < 16; i++) {
97
- if (values[i] !== undefined) {
98
- mergedValues[i] = values[i];
99
- }
100
- }
63
+ let mergedOutputState = initialOutputState;
64
+
65
+ if (valueState.output !== undefined && valueState.output !== null) {
66
+ mergedOutputState[valueState.output] = {
67
+ value: valueState.value,
68
+ unit: valueState.unit
69
+ };
101
70
  }
102
71
 
103
- if (!blockQueues[queueKey]) { // Create queue, if none
104
- blockQueues[queueKey] = {
105
- values: mergedValues,
106
- units: mergedUnits,
107
- node: node,
108
- timestamp: Date.now(),
109
- origMsg: origMsg || null
110
- };
111
- } else { // Overwrite state, if queue exists
112
- const q = blockQueues[queueKey];
113
- q.values = mergedValues;
114
- q.units = mergedUnits;
115
- q.origMsg = origMsg || q.origMsg;
116
- }
117
-
118
72
  // Delete existing timer if any
119
73
  if (blockTimers[queueKey]) {
120
74
  clearTimeout(blockTimers[queueKey]);
@@ -122,67 +76,68 @@ function queueAndSend(node, translate, nodeNumber, blockNumber, values, units, d
122
76
 
123
77
  // Start a new timer to send the queued message after the debounce delay
124
78
  blockTimers[queueKey] = setTimeout(() => {
125
- const queued = blockQueues[queueKey];
126
- if (queued) {
127
- const packet = createCoEPacket(
79
+ const nodeNumber = outputStateCache[queueKey].nodeNumber;
80
+ const dataType = outputStateCache[queueKey].dataType;
81
+ const queued = mergedOutputState;
82
+ if (outputStateCache[queueKey]) {
83
+ const packet = createPacket[coeVersion](
128
84
  nodeNumber,
129
- blockNumber,
130
- queued.values,
131
- queued.units,
132
85
  dataType,
133
- version
86
+ queued
134
87
  );
135
88
 
136
- // Persist both values and units for the block
137
- setBlockState(nodeNumber, blockNumber, dataType, queued.values);
138
- if (dataType === 'analog') {
139
- setBlockUnits(nodeNumber, blockNumber, dataType, queued.units);
140
- }
89
+ // Persist the merged output state for the queue
90
+ setOutputState(queueKey, queued);
91
+
92
+ cmiConfig.send(cmiAddress, packet);
93
+
94
+ const now = Date.now();
95
+ const mergedText = node._("coe-output.status.merged");
96
+ const readyText = node._("coe-output.status.ready");
97
+
98
+ queuedState[queueKey].participating.forEach(participatingNode => {
99
+ const outputNumber = participatingNode.outputNumber;
100
+
101
+ // Guard against missing sparse entries
102
+ participatingNode.lastSentValue = (queued[outputNumber] && queued[outputNumber].value !== undefined) ? queued[outputNumber].value : undefined;
103
+ participatingNode.lastSentTime = now;
141
104
 
142
- // Send debug output on the node outputs: [original msg, debug info]
143
- try {
144
- const debugPayload = {
145
- debug: {
146
- hex: packet.toString('hex').toUpperCase(),
147
- node: nodeNumber,
148
- block: blockNumber,
149
- dataType: dataType,
150
- version: version,
151
- blockState: queued.values,
152
- units: queued.units
153
- }
154
- };
155
- // If node has outputs, send original msg on first output and debug on second
156
- queued.node.send([queued.origMsg || null, { payload: debugPayload }]);
157
- } catch (err) {
158
- // Do not break sending on debug failure
159
- queued.node.warn(`Failed to send debug msg: ${err.message}`);
160
- }
161
- const mergedText = translate("coe-output.status.merged");
162
- const readyText = translate("coe-output.status.ready");
163
-
164
- cmiConfig.send(cmiAddress, packet);
165
-
166
- queued.node.status({
105
+ participatingNode.status({
167
106
  fill: "green",
168
107
  shape: "dot",
169
- text: `${mergedText} [${version}]`
108
+ text: `${mergedText} [v${coeVersion}]`
170
109
  });
171
-
172
110
  setTimeout(() => {
173
- queued.node.status({fill: "grey", shape: "ring", text: `${readyText} [v${version}]`});
174
- }, 2000);
111
+ participatingNode.status({fill: "grey", shape: "ring", text: `${readyText} [v${coeVersion}]`});
112
+ }, 5000);
113
+
114
+ // Send debug output on the node outputs: [original msg, debug info]
115
+ try {
116
+ const debugPayload = {
117
+ debug: {
118
+ node: nodeNumber,
119
+ dataType: dataType,
120
+ output: outputNumber,
121
+ value: queued[outputNumber] ? queued[outputNumber].value : undefined,
122
+ unit: queued[outputNumber] ? queued[outputNumber].unit : undefined
123
+ // raw: packet.toString('hex').toUpperCase()
124
+ }
125
+ };
126
+ // If node has outputs, send original msg on first output and debug on second
127
+ participatingNode.send([queuedState[queueKey].origMsg || null, { payload: debugPayload }]);
128
+ } catch (err) {
129
+ // Do not break sending on debug failure
130
+ participatingNode.warn(`Failed to send debug msg: ${err.message}`);
131
+ }
132
+
133
+ });
175
134
 
176
- delete blockQueues[queueKey];
177
- delete blockTimers[queueKey];
178
- }
179
- }, DEBOUNCE_DELAY);
135
+ delete queuedState[queueKey];
136
+ delete blockTimers[queueKey];
137
+ }
138
+ }, DEBOUNCE_DELAY);
180
139
  }
181
140
 
182
141
  module.exports = {
183
- getBlockState,
184
- setBlockState,
185
- getBlockUnits,
186
- setBlockUnits,
187
142
  queueAndSend
188
143
  };
package/lib/units.js CHANGED
@@ -33,7 +33,7 @@ const UNITS = {
33
33
  22: { name_de: 'Durchfluss l/min', symb_de: 'l/min',name_en: 'Flow rate l/min', symb_en: 'l/min', decimals: 0 },
34
34
  23: { name_de: 'Druck bar', symb_de: 'bar',name_en: 'Pressure bar', symb_en: 'bar', decimals: 2 },
35
35
  24: { name_de: 'Arbeitszahl', symb_de: '',name_en: 'COP', symb_en: '', decimals: 2 },
36
- 25: { name_de: 'Länge km', symb_de: 'km',name_en: 'Length km', symb_en: 'km', decimals: 0 },
36
+ 25: { name_de: 'Länge km', symb_de: 'km',name_en: 'Length km', symb_en: 'km', decimals: 0},
37
37
  26: { name_de: 'Länge m', symb_de: 'm',name_en: 'Length m', symb_en: 'm', decimals: 1 },
38
38
  27: { name_de: 'Länge mm', symb_de: 'mm',name_en: 'Length mm', symb_en: 'mm', decimals: 1 },
39
39
  28: { name_de: 'Kubikmeter', symb_de: 'm³',name_en: 'Cubic meters', symb_en: 'm³', decimals: 0 },
@@ -45,8 +45,8 @@ const UNITS = {
45
45
  40: { name_de: 'Geschwindigkeit mm/min', symb_de: 'mm/min',name_en: 'Speed mm/min', symb_en: 'mm/min', decimals: 0 },
46
46
  41: { name_de: 'Geschwindigkeit mm/h', symb_de: 'mm/h',name_en: 'Speed mm/h', symb_en: 'mm/h', decimals: 0 },
47
47
  42: { name_de: 'Geschwindigkeit mm/d', symb_de: 'mm/d',name_en: 'Speed mm/d', symb_en: 'mm/d', decimals: 0 },
48
- 43: { name_de: 'Digital (aus/ein)', symb_de: 'Aus/Ein',name_en: 'Digital (off/on)', symb_en: 'Off/On', decimals: 0 },
49
- 44: { name_de: 'Digital (nein/ja)', symb_de: 'Nein/Ja',name_en: 'Digital (no/yes)', symb_en: 'No/Yes', decimals: 0 },
48
+ 43: { name_de: 'Ein/Aus', symb_de: 'Aus/Ein',name_en: 'On/Off', symb_en: 'Off/On', decimals: 0, digital: true },
49
+ 44: { name_de: 'Ja/Nein', symb_de: 'Nein/Ja',name_en: 'Yes/No', symb_en: 'No/Yes', decimals: 0, digital: true },
50
50
  46: { name_de: 'RAS', symb_de: '°C',name_en: 'RAS', symb_en: '°C', decimals: 1 },
51
51
  50: { name_de: 'Euro', symb_de: '€',name_en: 'Euro', symb_en: '€', decimals: 2 },
52
52
  51: { name_de: 'Dollar', symb_de: '$',name_en: 'Dollar', symb_en: '$', decimals: 2 },
@@ -62,17 +62,17 @@ const UNITS = {
62
62
  65: { name_de: 'Druck mbar', symb_de: 'mbar',name_en: 'Pressure mbar', symb_en: 'mbar', decimals: 1 },
63
63
  66: { name_de: 'Druck Pa', symb_de: 'Pa',name_en: 'Pressure Pa', symb_en: 'Pa', decimals: 0 },
64
64
  67: { name_de: 'CO2-Gehalt ppm', symb_de: 'ppm',name_en: 'CO2 content ppm', symb_en: 'ppm', decimals: 0 },
65
- 68: { name_de: '', symb_de: '',name_en: '', symb_en: '', decimals: 0 },
66
65
  69: { name_de: 'Leistung W', symb_de: 'W',name_en: 'Power W', symb_en: 'W', decimals: 0 },
67
66
  70: { name_de: 'Gewicht t', symb_de: 't',name_en: 'Weight t', symb_en: 't', decimals: 2 },
68
- 71: { name_de: 'Gewicht kg', symb_de: 'kg',name_en: 'Weight kg', symb_en: 'kg', decimals: 1 },
67
+ 71: { name_de: 'Gewicht kg', symb_de: 'kg',name_en: 'Weight kg', symb_en: 'kg', decimals: 1},
69
68
  72: { name_de: 'Gewicht g', symb_de: 'g',name_en: 'Weight g', symb_en: 'g', decimals: 1 },
70
69
  73: { name_de: 'Länge cm', symb_de: 'cm',name_en: 'Length cm', symb_en: 'cm', decimals: 1 },
71
70
  74: { name_de: 'Temperatur K', symb_de: 'K',name_en: 'Temperature K', symb_en: 'K', decimals: 0 },
72
71
  75: { name_de: 'Lichtstärke', symb_de: 'lx',name_en: 'Light intensity', symb_en: 'lx', decimals: 1 },
73
72
  76: { name_de: 'Radonkonzentration', symb_de: 'Bq/m³',name_en: 'Radon concentration', symb_en: 'Bq/m³', decimals: 0 },
74
73
  77: { name_de: 'Preis ct/kWh', symb_de: 'ct/kWh',name_en: 'Price ct/kWh', symb_en: 'ct/kWh', decimals: 3 },
75
- 78: { name_de: 'Digital (geschl./offen)', symb_de: 'Geschlossen/Offen',name_en: 'Digital (closed/open)', symb_en: 'Closed/Open', decimals: 0 }
74
+ 78: { name_de: 'Offen/Geschlossen', symb_de: 'Geschlossen/Offen',name_en: 'Open/Closed', symb_en: 'Closed/Open', decimals: 0, digital: true },
75
+ 79: { name_de: 'Konzentration ppb', symb_de: 'ppb',name_en: 'Concentration ppb', symb_en: 'ppb', decimals: 0 }
76
76
  };
77
77
 
78
- module.exports = UNITS;
78
+ module.exports = UNITS ;
package/lib/utils.js CHANGED
@@ -9,13 +9,13 @@
9
9
  const UNITS = require ('./units.js');
10
10
 
11
11
  // Utilities for unit conversion
12
- function convertCoEToValue(rawValue, unitId, protocolVersion) {
13
- const unitDecimals = getUnitDecimals(unitId, protocolVersion);
12
+ function convertRawToValue(rawValue, unitId, coeVersion) {
13
+ const unitDecimals = getUnitDecimals(unitId, coeVersion);
14
14
  return rawValue / Math.pow(10, unitDecimals);
15
15
  }
16
16
 
17
- function convertValueToCoE(value, unitId, protocolVersion) {
18
- const unitDecimals = getUnitDecimals(unitId, protocolVersion);
17
+ function convertValueToRaw(value, unitId, coeVersion) {
18
+ const unitDecimals = getUnitDecimals(unitId, coeVersion);
19
19
  return Math.round(value * Math.pow(10, unitDecimals));
20
20
  }
21
21
 
@@ -25,11 +25,11 @@ function getUnitInfo(unitId, langCode) {
25
25
  const idKey = String(unitId);
26
26
 
27
27
  // Use UNITS from units.js as base
28
- if (UNITS && UNITS[idKey]) {
28
+ if (UNITS && UNITS[idKey]) {
29
29
  const unit = UNITS[idKey];
30
30
  return {
31
31
  name: useGerman ? unit.name_de : unit.name_en,
32
- symbol: useGerman ? unit.symb_de : unit.symb_en
32
+ symbol: unit.digital ? '' : (useGerman ? unit.symb_de : unit.symb_en) // No symbol for digital types
33
33
  };
34
34
  } else {
35
35
  return {
@@ -39,6 +39,19 @@ function getUnitInfo(unitId, langCode) {
39
39
  }
40
40
  }
41
41
 
42
+ // Generate digital state key based on unit and value
43
+ function getDigitalStateKey(unit, value, prefix = "coe.status.") {
44
+ const states = {
45
+ 43: value ? "on" : "off",
46
+ 44: value ? "yes" : "no",
47
+ 78: value ? "open" : "closed",
48
+ default: value ? "on" : "off"
49
+ };
50
+
51
+ const state = states[unit] || states.default;
52
+ return `${prefix}${state}`;
53
+ }
54
+
42
55
  // Retrieve unit decimals from unit list
43
56
  function getUnitDecimals(unitId, protocolVersion) {
44
57
  let unitDecimals;
@@ -74,90 +87,82 @@ function getUnitLanguage(lang) {
74
87
 
75
88
  // Translate output number to block position (CoE V1)
76
89
  function getBlockInfo(dataType, outputNumber) {
90
+ let blockNumber, position, dType;
77
91
  outputNumber = parseInt(outputNumber);
78
- if (isNaN(outputNumber) || outputNumber < 1) {
79
- // Default to block 1 position 0
80
- return { block: 1, position: 0 };
81
- }
82
92
 
83
- if (dataType === 'analog') {
84
- // Analog: Outputs 1..32 → Blocks 1..8 (4 Outputs each)
85
- const block = Math.floor((outputNumber - 1) / 4) + 1; // 1..8
86
- const position = (outputNumber - 1) % 4; // 0..3
87
- return { block: block, position: position };
93
+ if (isNaN(outputNumber) || outputNumber < 1) { // Default to block 1 position 0
94
+ blockNumber = 1;
95
+ position = 0;
96
+ dType = 'a';
88
97
  } else {
89
- // Digital: Outputs 1..16 → Block 0, 17..32 → Block 9
90
- if (outputNumber <= 16) {
91
- return { block: 0, position: outputNumber - 1 }; // 0..15
98
+ if (dataType === 'analog') {
99
+ // Analog: Outputs 1..32 → Blocks 1..8 (4 Outputs each)
100
+ blockNumber = Math.floor((outputNumber - 1) / 4) + 1; // 1..8
101
+ position = (outputNumber - 1) % 4; // 0..3
102
+ dType = 'a';
92
103
  } else {
93
- return { block: 9, position: outputNumber - 17 }; // 0..15
94
- }
95
- }
96
- }
97
-
98
- // Merge incoming (V2) block data with LKGV (Last Known Good Values)
99
- function mergeBlockData(currentState, newBlock) {
100
- const isDigital = newBlock.dataType === 'digital' || newBlock.blockNumber === 0 || newBlock.blockNumber === 9;
101
- const numValues = isDigital ? 16 : 4;
102
-
103
- const updatedBlock = { // Initialize the updated block (copy)
104
- ...currentState,
105
- values: new Array(numValues).fill(undefined),
106
- units: isDigital ? null : new Array(numValues).fill(undefined)
107
- };
108
-
109
- const oldValues = currentState.values || [];
110
- const oldUnits = currentState.units || [];
111
-
112
- for (let i = 0; i < numValues; i++) { // Copy the old LKGV (Last Known Good Values)
113
- if (oldValues[i] !== undefined) {
114
- updatedBlock.values[i] = oldValues[i];
115
- }
116
- }
117
-
118
- if (!isDigital && updatedBlock.units && oldUnits) { // Copy old units
119
- for (let i = 0; i < 4; i++) {
120
- if (oldUnits[i] !== undefined) {
121
- updatedBlock.units[i] = oldUnits[i];
104
+ dType = 'd';
105
+ // Digital: Outputs 1..16 → Block 0, 17..32 → Block 9
106
+ if (outputNumber <= 16) {
107
+ blockNumber = 0;
108
+ position = outputNumber - 1; // 0..1
109
+ } else {
110
+ blockNumber = 9;
111
+ position = outputNumber - 17; // 0..15
122
112
  }
123
113
  }
124
114
  }
115
+ return { number: blockNumber, position: position};
116
+ }
125
117
 
126
- for (let i = 0; i < numValues; i++) { // Merge with the new (sparse V2) values
127
- if (newBlock.values && newBlock.values[i] !== undefined) {
128
- updatedBlock.values[i] = newBlock.values[i];
129
- }
118
+ // Translate block number to output arrays (CoE V1)
119
+ function getOutputsfromBlock(blockNumber, dataType) {
120
+ let start, length;
121
+ if (dataType === 'analog') {
122
+ length = 4;
123
+ start = ((blockNumber - 1) * 4) + 1;
130
124
  }
131
-
132
- if (!isDigital && updatedBlock.units && newBlock.units) { // Merge the new units (only analog)
133
- for (let i = 0; i < 4; i++) {
134
- if (newBlock.units[i] !== undefined) {
135
- updatedBlock.units[i] = newBlock.units[i];
136
- }
125
+ else { // digital
126
+ length = 16;
127
+ if (blockNumber === 0) {
128
+ start = 1;
129
+ } else { // block 9
130
+ start = 17;
137
131
  }
138
132
  }
133
+ return Array.from({ length }, (_, i) => start + i);
134
+ }
139
135
 
140
- return updatedBlock;
136
+ // Merge incoming (V2) node data with LKGV (Last Known Good Values)
137
+ function mergeNodeData(currentState, newNode) {
138
+ return {
139
+ ...currentState,
140
+ ...newNode,
141
+ outputs: {
142
+ ...currentState.outputs,
143
+ ...newNode.outputs
144
+ }
145
+ };
141
146
  }
142
147
 
143
148
  // Create empty block state (incoming block)
144
149
  function createEmptyState(incomingBlock) {
145
- const isDigital = incomingBlock.dataType === 'digital' || incomingBlock.blockNumber === 0 || incomingBlock.blockNumber === 9;
146
- const numValues = isDigital ? 16 : 4;
150
+ const isDigital = incomingBlock.dataType === 'digital';
151
+
147
152
  return {
148
153
  nodeNumber: incomingBlock.nodeNumber,
149
- blockNumber: incomingBlock.blockNumber,
150
154
  dataType: isDigital ? 'digital' : 'analog',
151
- values: new Array(numValues).fill(undefined),
152
- units: isDigital ? null : new Array(numValues).fill(undefined),
155
+ outputs: {}
153
156
  };
154
157
  }
155
158
 
156
159
  module.exports = {
157
- convertCoEToValue,
158
- convertValueToCoE,
160
+ convertRawToValue,
161
+ convertValueToRaw,
159
162
  getUnitInfo,
163
+ getDigitalStateKey,
160
164
  getBlockInfo,
161
- mergeBlockData,
165
+ getOutputsfromBlock,
166
+ mergeNodeData,
162
167
  createEmptyState
163
168
  };