iobroker.utility-monitor 1.4.6 → 1.5.1
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/README.md +110 -62
- package/admin/custom/.vite/manifest.json +90 -0
- package/admin/custom/@mf-types/Components.d.ts +2 -0
- package/admin/custom/@mf-types/compiled-types/Components/CSVImporter.d.ts +11 -0
- package/admin/custom/@mf-types/compiled-types/Components.d.ts +2 -0
- package/admin/custom/@mf-types.d.ts +3 -0
- package/admin/custom/@mf-types.zip +0 -0
- package/admin/custom/CSVImporter_v15_11.js +4415 -0
- package/admin/custom/assets/Components-i0AZ59nl.js +18887 -0
- package/admin/custom/assets/UtilityMonitor__loadShare__react__loadShare__-Da99Mak4.js +42 -0
- package/admin/custom/assets/UtilityMonitor__mf_v__runtimeInit__mf_v__-BmC4OGk6.js +16 -0
- package/admin/custom/assets/_commonjsHelpers-Dj2_voLF.js +30 -0
- package/admin/custom/assets/hostInit-DEXfeB0W.js +10 -0
- package/admin/custom/assets/index-B3WVNJTz.js +401 -0
- package/admin/custom/assets/index-VBwl8x_k.js +64 -0
- package/admin/custom/assets/preload-helper-BelkbqnE.js +61 -0
- package/admin/custom/assets/virtualExposes-CqCLUNLT.js +19 -0
- package/admin/custom/index.html +12 -0
- package/admin/custom/mf-manifest.json +1 -0
- package/admin/jsonConfig.json +90 -31
- package/io-package.json +15 -31
- package/lib/billingManager.js +382 -137
- package/lib/calculator.js +41 -146
- package/lib/consumptionManager.js +9 -252
- package/lib/importManager.js +300 -0
- package/lib/messagingHandler.js +4 -2
- package/lib/meter/MeterRegistry.js +110 -0
- package/lib/multiMeterManager.js +580 -173
- package/lib/stateManager.js +502 -31
- package/lib/utils/billingHelper.js +69 -0
- package/lib/utils/consumptionHelper.js +47 -0
- package/lib/utils/helpers.js +234 -0
- package/lib/utils/stateCache.js +147 -0
- package/lib/utils/typeMapper.js +19 -0
- package/main.js +67 -8
- package/package.json +10 -4
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const calculator = require('./calculator');
|
|
4
|
+
const { getConfigType } = require('./utils/typeMapper');
|
|
5
|
+
const stateManager = require('./stateManager');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ImportManager handles CSV file parsing and data importing
|
|
9
|
+
* for utility-monitor historical data.
|
|
10
|
+
*/
|
|
11
|
+
class ImportManager {
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} adapter - ioBroker adapter instance
|
|
14
|
+
*/
|
|
15
|
+
constructor(adapter) {
|
|
16
|
+
this.adapter = adapter;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handles the 'importCSV' message from the Admin UI
|
|
21
|
+
*
|
|
22
|
+
* @param {Record<string, any>} obj - Message object
|
|
23
|
+
*/
|
|
24
|
+
async handleImportCSV(obj) {
|
|
25
|
+
if (!obj || !obj.message) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { type, meterName, content, format } = obj.message;
|
|
30
|
+
|
|
31
|
+
this.adapter.log.info(`[Import] Starting CSV import for ${type}.${meterName} (Format: ${format || 'generic'})`);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await this.processImport(type, meterName, content, format);
|
|
35
|
+
|
|
36
|
+
if (obj.callback) {
|
|
37
|
+
this.adapter.sendTo(obj.from, obj.command, result, obj.callback);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
this.adapter.log.error(`[Import] Failed to process CSV: ${error.message}`);
|
|
41
|
+
if (obj.callback) {
|
|
42
|
+
this.adapter.sendTo(obj.from, obj.command, { error: error.message }, obj.callback);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parses and processes the CSV content
|
|
49
|
+
*
|
|
50
|
+
* @param {string} type - gas, water, electricity
|
|
51
|
+
* @param {string} meterName - technical name of the meter
|
|
52
|
+
* @param {string} content - raw CSV content (base64 or string)
|
|
53
|
+
* @param {string} _format - format profile (e.g. 'ehb')
|
|
54
|
+
*/
|
|
55
|
+
async processImport(type, meterName, content, _format) {
|
|
56
|
+
// Decode base64 if necessary
|
|
57
|
+
let csvBody = content;
|
|
58
|
+
if (content.startsWith('data:') && content.includes('base64,')) {
|
|
59
|
+
const base64Data = content.split('base64,')[1];
|
|
60
|
+
csvBody = Buffer.from(base64Data, 'base64').toString('utf-8');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = csvBody.split(/\r?\n/).filter(line => line.trim() !== '');
|
|
64
|
+
if (lines.length < 2) {
|
|
65
|
+
throw new Error('Die Datei ist leer oder enthält zu wenige Daten.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Detect separator by looking at the first 5 lines and picking the most frequent separator
|
|
69
|
+
const possibleSeparators = [';', ',', '|', '\t'];
|
|
70
|
+
let separator = ';';
|
|
71
|
+
let maxTotalCols = 0;
|
|
72
|
+
|
|
73
|
+
for (const sep of possibleSeparators) {
|
|
74
|
+
let totalCols = 0;
|
|
75
|
+
for (let i = 0; i < Math.min(lines.length, 5); i++) {
|
|
76
|
+
totalCols += lines[i].split(sep).length;
|
|
77
|
+
}
|
|
78
|
+
if (totalCols > maxTotalCols) {
|
|
79
|
+
maxTotalCols = totalCols;
|
|
80
|
+
separator = sep;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let headers = lines[0].split(separator).map(h => h.trim().toLowerCase().replace(/^"|"$/g, ''));
|
|
85
|
+
|
|
86
|
+
// Column detection
|
|
87
|
+
let dateIdx = -1;
|
|
88
|
+
let valueIdx = -1;
|
|
89
|
+
|
|
90
|
+
const dateHeaders = ['date', 'datum', 'zeit', 'timestamp', 'zeitstempel', 'day', 'tag', 'ablesedatum'];
|
|
91
|
+
const valueHeaders = [
|
|
92
|
+
'value',
|
|
93
|
+
'wert',
|
|
94
|
+
'reading',
|
|
95
|
+
'zählerstand',
|
|
96
|
+
'stand',
|
|
97
|
+
'verbrauch',
|
|
98
|
+
'amount',
|
|
99
|
+
'kwh',
|
|
100
|
+
'm³',
|
|
101
|
+
'm3',
|
|
102
|
+
'ablesewert',
|
|
103
|
+
'wasserstand',
|
|
104
|
+
'gasstand',
|
|
105
|
+
'stromstand',
|
|
106
|
+
'kaltwasser',
|
|
107
|
+
'warmwasser',
|
|
108
|
+
'energie',
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
dateIdx = headers.findIndex(h => dateHeaders.some(dh => h.includes(dh)));
|
|
112
|
+
valueIdx = headers.findIndex(h => valueHeaders.some(vh => h.includes(vh)));
|
|
113
|
+
|
|
114
|
+
// Special case: If header is just numbers or very common terms, try searching for the specific media name
|
|
115
|
+
if (valueIdx === -1) {
|
|
116
|
+
const mediaTerms = { gas: 'gas', water: 'wasser', electricity: 'strom', pv: 'pv' };
|
|
117
|
+
const term = mediaTerms[type];
|
|
118
|
+
valueIdx = headers.findIndex(h => h.includes(term));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback to defaults if not found by header
|
|
122
|
+
if (dateIdx === -1) {
|
|
123
|
+
dateIdx = 0;
|
|
124
|
+
}
|
|
125
|
+
if (valueIdx === -1) {
|
|
126
|
+
valueIdx = 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.adapter.log.info(
|
|
130
|
+
`[Import] Found headers: [${headers.join(' | ')}]. Selected columns: Date="${headers[dateIdx]}" (Index ${dateIdx}), Value="${headers[valueIdx]}" (Index ${valueIdx}) (Separator: "${separator}")`,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Check if first line is actually data (contains numbers or date-like string)
|
|
134
|
+
let startIndex = 1;
|
|
135
|
+
const firstLineCols = lines[0].split(separator).map(c => c.trim().replace(/^"|"$/g, ''));
|
|
136
|
+
const dateStrFirst = firstLineCols[dateIdx] || '';
|
|
137
|
+
const isFirstLineData = !isNaN(parseFloat(firstLineCols[valueIdx])) || dateStrFirst.includes(':');
|
|
138
|
+
|
|
139
|
+
if (isFirstLineData) {
|
|
140
|
+
const hasHeaderText = headers.some(h =>
|
|
141
|
+
[...dateHeaders, ...valueHeaders].some(term => h.includes(term) && h.length > 2),
|
|
142
|
+
);
|
|
143
|
+
if (!hasHeaderText) {
|
|
144
|
+
startIndex = 0;
|
|
145
|
+
this.adapter.log.info('[Import] First line appears to be data, including it.');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const dataPoints = [];
|
|
150
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
151
|
+
const columns = lines[i].split(separator).map(c => c.trim().replace(/^"|"$/g, ''));
|
|
152
|
+
if (columns.length <= Math.max(dateIdx, valueIdx)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const dateStr = columns[dateIdx];
|
|
157
|
+
const valueStr = columns[valueIdx];
|
|
158
|
+
|
|
159
|
+
let timestamp = null;
|
|
160
|
+
let value = calculator.ensureNumber(valueStr);
|
|
161
|
+
|
|
162
|
+
// Robust Date Parsing
|
|
163
|
+
timestamp = calculator.parseDateString(dateStr);
|
|
164
|
+
|
|
165
|
+
if (!timestamp && !isNaN(Number(dateStr)) && dateStr.length > 10) {
|
|
166
|
+
// Possibly Unix Timestamp (ms)
|
|
167
|
+
timestamp = new Date(Number(dateStr));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (timestamp && !isNaN(timestamp.getTime()) && value > 0) {
|
|
171
|
+
dataPoints.push({ timestamp, value });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (dataPoints.length === 0) {
|
|
176
|
+
this.adapter.log.warn(
|
|
177
|
+
`[Import] No valid data found. Headers: ${headers.join('|')}, First Data Line: ${lines[1]}`,
|
|
178
|
+
);
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Keine gültigen Datenpunkte gefunden. Bitte sicherstellen, dass die Datei Spalten für Datum und Wert enthält.',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Sort by timestamp
|
|
185
|
+
dataPoints.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
186
|
+
|
|
187
|
+
this.adapter.log.info(
|
|
188
|
+
`[Import] Found ${dataPoints.length} valid records from ${dataPoints[0].timestamp.toLocaleDateString()} to ${dataPoints[dataPoints.length - 1].timestamp.toLocaleDateString()}`,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Pure History Import: Aggregates only
|
|
192
|
+
const basePath = `${type}.${meterName}`;
|
|
193
|
+
|
|
194
|
+
// Ensure channel and metadata structure
|
|
195
|
+
await this.ensureMeterObjects(type, meterName);
|
|
196
|
+
|
|
197
|
+
const currentYear = new Date().getFullYear();
|
|
198
|
+
const yearStats = {};
|
|
199
|
+
|
|
200
|
+
let count = 0;
|
|
201
|
+
for (const dp of dataPoints) {
|
|
202
|
+
try {
|
|
203
|
+
const year = dp.timestamp.getFullYear();
|
|
204
|
+
|
|
205
|
+
// Group by year for history states
|
|
206
|
+
if (year < currentYear) {
|
|
207
|
+
if (!yearStats[year]) {
|
|
208
|
+
yearStats[year] = { consumption: 0, volume: 0, count: 0 };
|
|
209
|
+
await stateManager.createHistoryStructure(this.adapter, type, meterName, year);
|
|
210
|
+
}
|
|
211
|
+
yearStats[year].consumption += dp.value;
|
|
212
|
+
if (type === 'gas') {
|
|
213
|
+
const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
|
|
214
|
+
const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
|
|
215
|
+
yearStats[year].volume += dp.value / (brennwert * zZahl);
|
|
216
|
+
}
|
|
217
|
+
yearStats[year].count++;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// No individual writing to meterReading state needed for pure history
|
|
221
|
+
count++;
|
|
222
|
+
} catch (e) {
|
|
223
|
+
this.adapter.log.error(`[Import] Error writing data point ${dp.timestamp.toISOString()}: ${e.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Write aggregated history states
|
|
228
|
+
for (const year in yearStats) {
|
|
229
|
+
const hPath = `${basePath}.history.${year}`;
|
|
230
|
+
const stats = yearStats[year];
|
|
231
|
+
await this.adapter.setStateAsync(
|
|
232
|
+
`${hPath}.consumption`,
|
|
233
|
+
calculator.roundToDecimals(stats.consumption, 2),
|
|
234
|
+
true,
|
|
235
|
+
);
|
|
236
|
+
if (type === 'gas') {
|
|
237
|
+
await this.adapter.setStateAsync(`${hPath}.volume`, calculator.roundToDecimals(stats.volume, 2), true);
|
|
238
|
+
}
|
|
239
|
+
// Recalculate costs for history year roughly using current price
|
|
240
|
+
const configType = getConfigType(type);
|
|
241
|
+
const price = this.adapter.config[`${configType}Preis`] || 0;
|
|
242
|
+
await this.adapter.setStateAsync(
|
|
243
|
+
`${hPath}.costs`,
|
|
244
|
+
calculator.roundToDecimals(stats.consumption * price, 2),
|
|
245
|
+
true,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Update metadata for pure history import
|
|
250
|
+
await this.adapter.setStateAsync(`${basePath}.lastImport`, Date.now(), true);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
count: count,
|
|
255
|
+
first: dataPoints[0].timestamp.toLocaleDateString(),
|
|
256
|
+
last: dataPoints[dataPoints.length - 1].timestamp.toLocaleDateString(),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Ensures that the minimal object structure for a meter exists (FLAT structure for imports)
|
|
262
|
+
*
|
|
263
|
+
* @param {string} type - gas, water, electricity, pv
|
|
264
|
+
* @param {string} meterName - name of the meter
|
|
265
|
+
*/
|
|
266
|
+
async ensureMeterObjects(type, meterName) {
|
|
267
|
+
const basePath = `${type}.${meterName}`;
|
|
268
|
+
const typeDe = { gas: 'Gas', water: 'Wasser', electricity: 'Strom', pv: 'PV' };
|
|
269
|
+
|
|
270
|
+
// 1. Create Channel for Type (if not exists)
|
|
271
|
+
await this.adapter.setObjectNotExistsAsync(type, {
|
|
272
|
+
type: 'channel',
|
|
273
|
+
common: { name: typeDe[type] || type },
|
|
274
|
+
native: {},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// 2. Create Channel for Meter
|
|
278
|
+
await this.adapter.setObjectNotExistsAsync(basePath, {
|
|
279
|
+
type: 'channel',
|
|
280
|
+
common: { name: `Zähler: ${meterName}` },
|
|
281
|
+
native: { meterName },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 3. Create metadata (last import timestamp)
|
|
285
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.lastImport`, {
|
|
286
|
+
type: 'state',
|
|
287
|
+
common: {
|
|
288
|
+
name: 'Letzter Import',
|
|
289
|
+
type: 'number',
|
|
290
|
+
role: 'value.time',
|
|
291
|
+
read: true,
|
|
292
|
+
write: false,
|
|
293
|
+
def: 0,
|
|
294
|
+
},
|
|
295
|
+
native: {},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = ImportManager;
|
package/lib/messagingHandler.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { getConfigType } = require('./utils/typeMapper');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* MessagingHandler handles all incoming adapter messages
|
|
5
7
|
* and outgoing notifications.
|
|
@@ -185,7 +187,7 @@ class MessagingHandler {
|
|
|
185
187
|
const typesDe = { gas: 'Gas', water: 'Wasser', electricity: 'Strom', pv: 'PV' };
|
|
186
188
|
|
|
187
189
|
for (const type of types) {
|
|
188
|
-
const configType =
|
|
190
|
+
const configType = getConfigType(type);
|
|
189
191
|
const enabledKey = `notification${configType.charAt(0).toUpperCase() + configType.slice(1)}Enabled`;
|
|
190
192
|
|
|
191
193
|
if (!this.adapter.config[enabledKey] || !this.adapter.config[`${configType}Aktiv`]) {
|
|
@@ -290,7 +292,7 @@ class MessagingHandler {
|
|
|
290
292
|
let hasData = false;
|
|
291
293
|
|
|
292
294
|
for (const type of types) {
|
|
293
|
-
const configType =
|
|
295
|
+
const configType = getConfigType(type); // strom, gas, wasser, pv
|
|
294
296
|
|
|
295
297
|
if (!this.adapter.config[`${configType}Aktiv`]) {
|
|
296
298
|
continue;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MeterRegistry manages the mapping between sensor data points and meters.
|
|
5
|
+
* Each sensor can be associated with one or more meters of different types.
|
|
6
|
+
*/
|
|
7
|
+
class MeterRegistry {
|
|
8
|
+
/**
|
|
9
|
+
* Initializes the registry
|
|
10
|
+
*/
|
|
11
|
+
constructor() {
|
|
12
|
+
// Maps sensorDP to array of {type, meterName}
|
|
13
|
+
this.registry = {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Registers a sensor with a meter
|
|
18
|
+
*
|
|
19
|
+
* @param {string} sensorDP - Sensor data point ID
|
|
20
|
+
* @param {string} type - Utility type (gas, water, electricity, pv)
|
|
21
|
+
* @param {string} meterName - Name of the meter
|
|
22
|
+
*/
|
|
23
|
+
register(sensorDP, type, meterName) {
|
|
24
|
+
if (!sensorDP) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!this.registry[sensorDP]) {
|
|
29
|
+
this.registry[sensorDP] = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if already registered
|
|
33
|
+
const exists = this.registry[sensorDP].some(entry => entry.type === type && entry.meterName === meterName);
|
|
34
|
+
|
|
35
|
+
if (!exists) {
|
|
36
|
+
this.registry[sensorDP].push({ type, meterName });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Finds all meters associated with a sensor
|
|
42
|
+
*
|
|
43
|
+
* @param {string} sensorDP - Sensor data point ID
|
|
44
|
+
* @returns {Array<{type: string, meterName: string}>} Array of meter entries
|
|
45
|
+
*/
|
|
46
|
+
findBySensor(sensorDP) {
|
|
47
|
+
return this.registry[sensorDP] || [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Removes a meter from a sensor
|
|
52
|
+
*
|
|
53
|
+
* @param {string} sensorDP - Sensor data point ID
|
|
54
|
+
* @param {string} type - Utility type
|
|
55
|
+
* @param {string} meterName - Name of the meter
|
|
56
|
+
*/
|
|
57
|
+
unregister(sensorDP, type, meterName) {
|
|
58
|
+
if (!this.registry[sensorDP]) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.registry[sensorDP] = this.registry[sensorDP].filter(
|
|
63
|
+
entry => !(entry.type === type && entry.meterName === meterName),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Clean up empty entries
|
|
67
|
+
if (this.registry[sensorDP].length === 0) {
|
|
68
|
+
delete this.registry[sensorDP];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clears all registrations for a sensor
|
|
74
|
+
*
|
|
75
|
+
* @param {string} sensorDP - Sensor data point ID
|
|
76
|
+
*/
|
|
77
|
+
clearSensor(sensorDP) {
|
|
78
|
+
delete this.registry[sensorDP];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets all registered sensors
|
|
83
|
+
*
|
|
84
|
+
* @returns {string[]} Array of sensor data point IDs
|
|
85
|
+
*/
|
|
86
|
+
getAllSensors() {
|
|
87
|
+
return Object.keys(this.registry);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Gets the complete registry
|
|
92
|
+
*
|
|
93
|
+
* @returns {object} The registry object
|
|
94
|
+
*/
|
|
95
|
+
getRegistry() {
|
|
96
|
+
return this.registry;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks if a sensor is registered
|
|
101
|
+
*
|
|
102
|
+
* @param {string} sensorDP - Sensor data point ID
|
|
103
|
+
* @returns {boolean} True if sensor is registered
|
|
104
|
+
*/
|
|
105
|
+
hasSensor(sensorDP) {
|
|
106
|
+
return !!this.registry[sensorDP];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = MeterRegistry;
|