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