node-red-contrib-ta-cmi-coe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE +201 -0
- package/README.md +277 -0
- package/__tests__/blockinfo.test.js +24 -0
- package/__tests__/conversion.test.js +22 -0
- package/__tests__/udp.test.js +46 -0
- package/coe/coe-input.html +61 -0
- package/coe/coe-input.js +144 -0
- package/coe/coe-monitor.html +60 -0
- package/coe/coe-monitor.js +138 -0
- package/coe/coe-output.html +99 -0
- package/coe/coe-output.js +68 -0
- package/coe/config.html +52 -0
- package/coe/config.js +101 -0
- package/coe/locales/de/coe-input.html +10 -0
- package/coe/locales/de/coe-input.json +19 -0
- package/coe/locales/de/coe-monitor.html +10 -0
- package/coe/locales/de/coe-monitor.json +20 -0
- package/coe/locales/de/coe-output.html +14 -0
- package/coe/locales/de/coe-output.json +18 -0
- package/coe/locales/de/config.html +13 -0
- package/coe/locales/de/config.json +10 -0
- package/coe/locales/en-US/coe-input.html +10 -0
- package/coe/locales/en-US/coe-input.json +19 -0
- package/coe/locales/en-US/coe-monitor.html +10 -0
- package/coe/locales/en-US/coe-monitor.json +20 -0
- package/coe/locales/en-US/coe-output.html +14 -0
- package/coe/locales/en-US/coe-output.json +18 -0
- package/coe/locales/en-US/config.html +13 -0
- package/coe/locales/en-US/config.json +10 -0
- package/coe/units-config.js +36 -0
- package/lib/coe-v2.js +189 -0
- package/lib/coe.js +109 -0
- package/lib/queueing.js +188 -0
- package/lib/units.js +78 -0
- package/lib/utils.js +163 -0
- package/package.json +39 -0
package/lib/queueing.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Queueing and Debouncing Module
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2025 Florian Mayrhofer
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { createCoEPacket } = require('../lib/coe');
|
|
10
|
+
|
|
11
|
+
const blockStateCache = {};
|
|
12
|
+
const blockUnitsCache = {}; // Cache for units per block
|
|
13
|
+
const blockQueues = {};
|
|
14
|
+
const blockTimers = {};
|
|
15
|
+
const DEBOUNCE_DELAY = 50; // ms (Time slot for message collection)
|
|
16
|
+
|
|
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];
|
|
35
|
+
}
|
|
36
|
+
|
|
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;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return blockUnitsCache[key] ? [...blockUnitsCache[key]] : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
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
|
+
|
|
66
|
+
// 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);
|
|
69
|
+
|
|
70
|
+
// New queueing logic
|
|
71
|
+
let baseValues;
|
|
72
|
+
let baseUnits;
|
|
73
|
+
|
|
74
|
+
if (blockQueues[queueKey]) {
|
|
75
|
+
baseValues = [...blockQueues[queueKey].values];
|
|
76
|
+
baseUnits = blockQueues[queueKey].units ? [...blockQueues[queueKey].units] : null;
|
|
77
|
+
} else {
|
|
78
|
+
baseValues = getBlockState(nodeNumber, blockNumber, dataType);
|
|
79
|
+
baseUnits = (dataType === 'analog') ? getBlockUnits(nodeNumber, blockNumber, dataType) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 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
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
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
|
+
// Delete existing timer if any
|
|
119
|
+
if (blockTimers[queueKey]) {
|
|
120
|
+
clearTimeout(blockTimers[queueKey]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Start a new timer to send the queued message after the debounce delay
|
|
124
|
+
blockTimers[queueKey] = setTimeout(() => {
|
|
125
|
+
const queued = blockQueues[queueKey];
|
|
126
|
+
if (queued) {
|
|
127
|
+
const packet = createCoEPacket(
|
|
128
|
+
nodeNumber,
|
|
129
|
+
blockNumber,
|
|
130
|
+
queued.values,
|
|
131
|
+
queued.units,
|
|
132
|
+
dataType,
|
|
133
|
+
version
|
|
134
|
+
);
|
|
135
|
+
|
|
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
|
+
}
|
|
141
|
+
|
|
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({
|
|
167
|
+
fill: "green",
|
|
168
|
+
shape: "dot",
|
|
169
|
+
text: `${mergedText} [${version}]`
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
queued.node.status({fill: "grey", shape: "ring", text: `${readyText} [v${version}]`});
|
|
174
|
+
}, 2000);
|
|
175
|
+
|
|
176
|
+
delete blockQueues[queueKey];
|
|
177
|
+
delete blockTimers[queueKey];
|
|
178
|
+
}
|
|
179
|
+
}, DEBOUNCE_DELAY);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
getBlockState,
|
|
184
|
+
setBlockState,
|
|
185
|
+
getBlockUnits,
|
|
186
|
+
setBlockUnits,
|
|
187
|
+
queueAndSend
|
|
188
|
+
};
|
package/lib/units.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central Unit Definitions for TA CMI CoE
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2025 Florian Mayrhofer
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Central Unit Definitions
|
|
10
|
+
|
|
11
|
+
const UNITS = {
|
|
12
|
+
0: { name_de: 'Dimensionslos', symb_de: '',name_en: 'Dimensionless', symb_en: '', decimals: 0 },
|
|
13
|
+
1: { name_de: 'Temperatur °C', symb_de: '°C',name_en: 'Temperature °C', symb_en: '°C', decimals: 1 },
|
|
14
|
+
2: { name_de: 'Solarstrahlung', symb_de: 'W/m²',name_en: 'Solar radiation', symb_en: 'W/m²', decimals: 0 },
|
|
15
|
+
3: { name_de: 'Durchfluss l/h', symb_de: 'l/h',name_en: 'Flow rate l/h', symb_en: 'l/h', decimals: 0 },
|
|
16
|
+
4: { name_de: 'Sekunden', symb_de: 'Sek',name_en: 'Seconds', symb_en: 'sec', decimals: 0 },
|
|
17
|
+
5: { name_de: 'Minuten', symb_de: 'Min',name_en: 'Minutes', symb_en: 'min', decimals: 0 },
|
|
18
|
+
6: { name_de: 'Durchfluss l/Imp', symb_de: 'l/Imp',name_en: 'Flow rate l/Imp', symb_en: 'l/Imp', decimals: 1 },
|
|
19
|
+
7: { name_de: 'Temperatur', symb_de: 'K',name_en: 'Temperature', symb_en: 'K', decimals: 1 },
|
|
20
|
+
8: { name_de: 'Prozent', symb_de: '%',name_en: 'Percent', symb_en: '%', decimals: 1 },
|
|
21
|
+
10: { name_de: 'Leistung kW', symb_de: 'kW',name_en: 'Power kW', symb_en: 'kW', decimals: 1 },
|
|
22
|
+
11: { name_de: 'Energie kWh', symb_de: 'kWh',name_en: 'Energy kWh', symb_en: 'kWh', decimals: 1 },
|
|
23
|
+
12: { name_de: 'Energie MWh', symb_de: 'MWh',name_en: 'Energy MWh', symb_en: 'MWh', decimals: 0 },
|
|
24
|
+
13: { name_de: 'Spannung', symb_de: 'V',name_en: 'Voltage', symb_en: 'V', decimals: 2 },
|
|
25
|
+
14: { name_de: 'Stromstärke mA', symb_de: 'mA',name_en: 'Current mA', symb_en: 'mA', decimals: 1 },
|
|
26
|
+
15: { name_de: 'Stunden', symb_de: 'Std',name_en: 'Hours', symb_en: 'hr', decimals: 0 },
|
|
27
|
+
16: { name_de: 'Tage', symb_de: 'Tage',name_en: 'Days', symb_en: 'Days', decimals: 0 },
|
|
28
|
+
17: { name_de: 'Anzahl Impulse', symb_de: 'Imp',name_en: 'Number of pulses', symb_en: 'Imp', decimals: 0 },
|
|
29
|
+
18: { name_de: 'Widerstand', symb_de: 'kΩ',name_en: 'Resistance', symb_en: 'kΩ', decimals: 2 },
|
|
30
|
+
19: { name_de: 'Liter', symb_de: 'l',name_en: 'Liters', symb_en: 'l', decimals: 0 },
|
|
31
|
+
20: { name_de: 'Geschwindigkeit km/h', symb_de: 'km/h',name_en: 'Speed km/h', symb_en: 'km/h', decimals: 0 },
|
|
32
|
+
21: { name_de: 'Frequenz', symb_de: 'Hz',name_en: 'Frequency', symb_en: 'Hz', decimals: 2 },
|
|
33
|
+
22: { name_de: 'Durchfluss l/min', symb_de: 'l/min',name_en: 'Flow rate l/min', symb_en: 'l/min', decimals: 0 },
|
|
34
|
+
23: { name_de: 'Druck bar', symb_de: 'bar',name_en: 'Pressure bar', symb_en: 'bar', decimals: 2 },
|
|
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 },
|
|
37
|
+
26: { name_de: 'Länge m', symb_de: 'm',name_en: 'Length m', symb_en: 'm', decimals: 1 },
|
|
38
|
+
27: { name_de: 'Länge mm', symb_de: 'mm',name_en: 'Length mm', symb_en: 'mm', decimals: 1 },
|
|
39
|
+
28: { name_de: 'Kubikmeter', symb_de: 'm³',name_en: 'Cubic meters', symb_en: 'm³', decimals: 0 },
|
|
40
|
+
35: { name_de: 'Durchfluss l/d', symb_de: 'l/d',name_en: 'Flow rate l/d', symb_en: 'l/d', decimals: 0 },
|
|
41
|
+
36: { name_de: 'Geschwindigkeit m/s', symb_de: 'm/s',name_en: 'Speed m/s', symb_en: 'm/s', decimals: 0 },
|
|
42
|
+
37: { name_de: 'Durchfluss m³/min', symb_de: 'm³/min',name_en: 'Flow rate m³/min', symb_en: 'm³/min', decimals: 0 },
|
|
43
|
+
38: { name_de: 'Durchfluss m³/h', symb_de: 'm³/h',name_en: 'Flow rate m³/h', symb_en: 'm³/h', decimals: 0 },
|
|
44
|
+
39: { name_de: 'Durchfluss m³/d', symb_de: 'm³/d',name_en: 'Flow rate m³/d', symb_en: 'm³/d', decimals: 0 },
|
|
45
|
+
40: { name_de: 'Geschwindigkeit mm/min', symb_de: 'mm/min',name_en: 'Speed mm/min', symb_en: 'mm/min', decimals: 0 },
|
|
46
|
+
41: { name_de: 'Geschwindigkeit mm/h', symb_de: 'mm/h',name_en: 'Speed mm/h', symb_en: 'mm/h', decimals: 0 },
|
|
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 },
|
|
50
|
+
46: { name_de: 'RAS', symb_de: '°C',name_en: 'RAS', symb_en: '°C', decimals: 1 },
|
|
51
|
+
50: { name_de: 'Euro', symb_de: '€',name_en: 'Euro', symb_en: '€', decimals: 2 },
|
|
52
|
+
51: { name_de: 'Dollar', symb_de: '$',name_en: 'Dollar', symb_en: '$', decimals: 2 },
|
|
53
|
+
52: { name_de: 'Absolute Feuchte', symb_de: 'g/m³',name_en: 'Absolute humidity', symb_en: 'g/m³', decimals: 1 },
|
|
54
|
+
53: { name_de: 'Dimensionslos(,5)', symb_de: '',name_en: 'Dimensional (.5)', symb_en: '', decimals: 5 },
|
|
55
|
+
54: { name_de: 'Grad (Winkel)', symb_de: '°',name_en: 'Degrees (Angle)', symb_en: '°', decimals: 1 },
|
|
56
|
+
56: { name_de: 'Grad (1/100 .6)', symb_de: '°',name_en: 'Degrees (.6)', symb_en: '°', decimals: 6 },
|
|
57
|
+
57: { name_de: 'Sekunden', symb_de: 's',name_en: 'Seconds', symb_en: 's', decimals: 1 },
|
|
58
|
+
58: { name_de: 'Dimensionslos(,1)', symb_de: '',name_en: 'Dimensional (.1)', symb_en: '', decimals: 1 },
|
|
59
|
+
59: { name_de: 'Prozent (,0)', symb_de: '%',name_en: 'Percent (.0)', symb_en: '%', decimals: 0 },
|
|
60
|
+
60: { name_de: 'Uhrzeit', symb_de: 'h',name_en: 'Time', symb_en: 'h', decimals: 0 },
|
|
61
|
+
63: { name_de: 'Stromstärke A', symb_de: 'A',name_en: 'Current A', symb_en: 'A', decimals: 1 },
|
|
62
|
+
65: { name_de: 'Druck mbar', symb_de: 'mbar',name_en: 'Pressure mbar', symb_en: 'mbar', decimals: 1 },
|
|
63
|
+
66: { name_de: 'Druck Pa', symb_de: 'Pa',name_en: 'Pressure Pa', symb_en: 'Pa', decimals: 0 },
|
|
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
|
+
69: { name_de: 'Leistung W', symb_de: 'W',name_en: 'Power W', symb_en: 'W', decimals: 0 },
|
|
67
|
+
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 },
|
|
69
|
+
72: { name_de: 'Gewicht g', symb_de: 'g',name_en: 'Weight g', symb_en: 'g', decimals: 1 },
|
|
70
|
+
73: { name_de: 'Länge cm', symb_de: 'cm',name_en: 'Length cm', symb_en: 'cm', decimals: 1 },
|
|
71
|
+
74: { name_de: 'Temperatur K', symb_de: 'K',name_en: 'Temperature K', symb_en: 'K', decimals: 0 },
|
|
72
|
+
75: { name_de: 'Lichtstärke', symb_de: 'lx',name_en: 'Light intensity', symb_en: 'lx', decimals: 1 },
|
|
73
|
+
76: { name_de: 'Radonkonzentration', symb_de: 'Bq/m³',name_en: 'Radon concentration', symb_en: 'Bq/m³', decimals: 0 },
|
|
74
|
+
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 }
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
module.exports = UNITS;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoE Utilities Module (used internally by nodes)
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2025 Florian Mayrhofer
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const UNITS = require ('./units.js');
|
|
10
|
+
|
|
11
|
+
// Utilities for unit conversion
|
|
12
|
+
function convertCoEToValue(rawValue, unitId, protocolVersion) {
|
|
13
|
+
const unitDecimals = getUnitDecimals(unitId, protocolVersion);
|
|
14
|
+
return rawValue / Math.pow(10, unitDecimals);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function convertValueToCoE(value, unitId, protocolVersion) {
|
|
18
|
+
const unitDecimals = getUnitDecimals(unitId, protocolVersion);
|
|
19
|
+
return Math.round(value * Math.pow(10, unitDecimals));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Retrieves unit name, symbol from unit list
|
|
23
|
+
function getUnitInfo(unitId, langCode) {
|
|
24
|
+
const useGerman = (getUnitLanguage(langCode) === "de");
|
|
25
|
+
const idKey = String(unitId);
|
|
26
|
+
|
|
27
|
+
// Use UNITS from units.js as base
|
|
28
|
+
if (UNITS && UNITS[idKey]) {
|
|
29
|
+
const unit = UNITS[idKey];
|
|
30
|
+
return {
|
|
31
|
+
name: useGerman ? unit.name_de : unit.name_en,
|
|
32
|
+
symbol: useGerman ? unit.symb_de : unit.symb_en
|
|
33
|
+
};
|
|
34
|
+
} else {
|
|
35
|
+
return {
|
|
36
|
+
name: `Unknown (${idKey})`,
|
|
37
|
+
symbol: ''
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Retrieve unit decimals from unit list
|
|
43
|
+
function getUnitDecimals(unitId, protocolVersion) {
|
|
44
|
+
let unitDecimals;
|
|
45
|
+
const unitKey = String(unitId);
|
|
46
|
+
|
|
47
|
+
if (UNITS && UNITS[unitKey]) {
|
|
48
|
+
unitDecimals = UNITS[unitKey].decimals;
|
|
49
|
+
} else {
|
|
50
|
+
unitDecimals = 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// V2 specific overrides
|
|
54
|
+
if (protocolVersion === 2) {
|
|
55
|
+
const v2_Overrides = {
|
|
56
|
+
10: { decimals: 2 } // Power kW: V1=1, V2=2 decimals
|
|
57
|
+
// Add more overrides if needed
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (v2_Overrides[unitId]) {
|
|
61
|
+
unitDecimals = v2_Overrides[unitId].decimals;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return unitDecimals;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Determine unit language code
|
|
68
|
+
function getUnitLanguage(lang) {
|
|
69
|
+
if (lang.toLowerCase().startsWith("de")) {
|
|
70
|
+
return "de";
|
|
71
|
+
}
|
|
72
|
+
return "en";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Translate output number to block position (CoE V1)
|
|
76
|
+
function getBlockInfo(dataType, outputNumber) {
|
|
77
|
+
outputNumber = parseInt(outputNumber);
|
|
78
|
+
if (isNaN(outputNumber) || outputNumber < 1) {
|
|
79
|
+
// Default to block 1 position 0
|
|
80
|
+
return { block: 1, position: 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
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 };
|
|
88
|
+
} 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
|
|
92
|
+
} 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];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
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
|
+
}
|
|
130
|
+
}
|
|
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
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return updatedBlock;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create empty block state (incoming block)
|
|
144
|
+
function createEmptyState(incomingBlock) {
|
|
145
|
+
const isDigital = incomingBlock.dataType === 'digital' || incomingBlock.blockNumber === 0 || incomingBlock.blockNumber === 9;
|
|
146
|
+
const numValues = isDigital ? 16 : 4;
|
|
147
|
+
return {
|
|
148
|
+
nodeNumber: incomingBlock.nodeNumber,
|
|
149
|
+
blockNumber: incomingBlock.blockNumber,
|
|
150
|
+
dataType: isDigital ? 'digital' : 'analog',
|
|
151
|
+
values: new Array(numValues).fill(undefined),
|
|
152
|
+
units: isDigital ? null : new Array(numValues).fill(undefined),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
convertCoEToValue,
|
|
158
|
+
convertValueToCoE,
|
|
159
|
+
getUnitInfo,
|
|
160
|
+
getBlockInfo,
|
|
161
|
+
mergeBlockData,
|
|
162
|
+
createEmptyState
|
|
163
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-ta-cmi-coe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node-RED nodes for TA CMI CoE (CAN over Ethernet)",
|
|
5
|
+
"author": "Florian Mayrhofer",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"node-red",
|
|
9
|
+
"technische-alternative",
|
|
10
|
+
"ta",
|
|
11
|
+
"cmi",
|
|
12
|
+
"coe",
|
|
13
|
+
"can-over-ethernet",
|
|
14
|
+
"uvr",
|
|
15
|
+
"heating",
|
|
16
|
+
"automation"
|
|
17
|
+
],
|
|
18
|
+
"node-red": {
|
|
19
|
+
"nodes": {
|
|
20
|
+
"coe-input": "coe/coe-input.js",
|
|
21
|
+
"coe-output": "coe/coe-output.js",
|
|
22
|
+
"coe-monitor": "coe/coe-monitor.js",
|
|
23
|
+
"cmiconfig": "coe/config.js",
|
|
24
|
+
"units-config": "coe/units-config.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"devDependencies": {},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=14.0.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/mayflo/node-red-contrib-ta-cmi-coe.git"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "jest"
|
|
38
|
+
}
|
|
39
|
+
}
|