iobroker.utility-monitor 1.4.6 → 1.5.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 (35) hide show
  1. package/README.md +98 -55
  2. package/admin/custom/.vite/manifest.json +90 -0
  3. package/admin/custom/@mf-types/Components.d.ts +2 -0
  4. package/admin/custom/@mf-types/compiled-types/Components/CSVImporter.d.ts +11 -0
  5. package/admin/custom/@mf-types/compiled-types/Components.d.ts +2 -0
  6. package/admin/custom/@mf-types.d.ts +3 -0
  7. package/admin/custom/@mf-types.zip +0 -0
  8. package/admin/custom/CSVImporter_v15_11.js +4415 -0
  9. package/admin/custom/assets/Components-i0AZ59nl.js +18887 -0
  10. package/admin/custom/assets/UtilityMonitor__loadShare__react__loadShare__-Da99Mak4.js +42 -0
  11. package/admin/custom/assets/UtilityMonitor__mf_v__runtimeInit__mf_v__-BmC4OGk6.js +16 -0
  12. package/admin/custom/assets/_commonjsHelpers-Dj2_voLF.js +30 -0
  13. package/admin/custom/assets/hostInit-DEXfeB0W.js +10 -0
  14. package/admin/custom/assets/index-B3WVNJTz.js +401 -0
  15. package/admin/custom/assets/index-VBwl8x_k.js +64 -0
  16. package/admin/custom/assets/preload-helper-BelkbqnE.js +61 -0
  17. package/admin/custom/assets/virtualExposes-CqCLUNLT.js +19 -0
  18. package/admin/custom/index.html +12 -0
  19. package/admin/custom/mf-manifest.json +1 -0
  20. package/admin/jsonConfig.json +90 -31
  21. package/io-package.json +39 -3
  22. package/lib/billingManager.js +235 -123
  23. package/lib/calculator.js +19 -138
  24. package/lib/consumptionManager.js +9 -252
  25. package/lib/importManager.js +300 -0
  26. package/lib/messagingHandler.js +4 -2
  27. package/lib/meter/MeterRegistry.js +110 -0
  28. package/lib/multiMeterManager.js +397 -174
  29. package/lib/stateManager.js +502 -31
  30. package/lib/utils/billingHelper.js +69 -0
  31. package/lib/utils/consumptionHelper.js +47 -0
  32. package/lib/utils/helpers.js +178 -0
  33. package/lib/utils/typeMapper.js +19 -0
  34. package/main.js +71 -8
  35. package/package.json +10 -4
package/lib/calculator.js CHANGED
@@ -1,7 +1,4 @@
1
- /**
2
- * Calculator module for nebenkosten-monitor
3
- * Provides utility functions for gas conversion, cost calculation, and consumption aggregation
4
- */
1
+ const helpers = require('./utils/helpers');
5
2
 
6
3
  /**
7
4
  * Converts gas volume from m³ to kWh
@@ -13,13 +10,17 @@
13
10
  * @returns {number} Energy in kWh
14
11
  */
15
12
  function convertGasM3ToKWh(m3, brennwert = 11.5, zZahl = 0.95) {
16
- if (typeof m3 !== 'number' || typeof brennwert !== 'number' || typeof zZahl !== 'number') {
17
- throw new TypeError('All parameters must be numbers');
18
- }
19
- if (m3 < 0 || brennwert <= 0 || zZahl <= 0 || zZahl > 1) {
20
- throw new RangeError('Invalid parameter values');
13
+ // Hier number zu string
14
+ const cleanM3 = helpers.ensureNumber(m3);
15
+ const cleanBrennwert = helpers.ensureNumber(brennwert);
16
+ const cleanZZahl = helpers.ensureNumber(zZahl);
17
+
18
+ // Validierung der Logik (jetzt mit den konvertierten Zahlen)
19
+ if (cleanM3 < 0 || cleanBrennwert <= 0 || cleanZZahl <= 0 || cleanZZahl > 1) {
20
+ throw new RangeError('Ungültige Parameterwerte für die Gas-Umrechnung');
21
21
  }
22
- return m3 * brennwert * zZahl;
22
+
23
+ return cleanM3 * cleanBrennwert * cleanZZahl;
23
24
  }
24
25
 
25
26
  /**
@@ -51,69 +52,6 @@ function calculateCost(consumption, price) {
51
52
  return consumption * (price || 0);
52
53
  }
53
54
 
54
- /**
55
- * Ensures a value is a number, handling German decimal commas if provided as string.
56
- *
57
- * @param {any} value - Value to convert
58
- * @returns {number}
59
- */
60
- function ensureNumber(value) {
61
- if (value === undefined || value === null || value === '') {
62
- return 0;
63
- }
64
- if (typeof value === 'string') {
65
- const normalized = value.replace(',', '.');
66
- const parsed = parseFloat(normalized);
67
- return isNaN(parsed) ? 0 : parsed;
68
- }
69
- const num = Number(value);
70
- return isNaN(num) ? 0 : num;
71
- }
72
-
73
- /**
74
- * Rounds a number to specified decimal places
75
- *
76
- * @param {number|string} value - Value to round
77
- * @param {number} decimals - Number of decimal places (default: 2)
78
- * @returns {number} Rounded value
79
- */
80
- function roundToDecimals(value, decimals = 2) {
81
- const numValue = ensureNumber(value);
82
- const factor = Math.pow(10, decimals);
83
- return Math.round(numValue * factor) / factor;
84
- }
85
-
86
- /**
87
- * Parses a German date string (DD.MM.YYYY) into a Date object
88
- *
89
- * @param {string} dateStr - Date string in format DD.MM.YYYY
90
- * @returns {Date|null} Date object or null if invalid
91
- */
92
- function parseGermanDate(dateStr) {
93
- if (!dateStr || typeof dateStr !== 'string') {
94
- return null;
95
- }
96
- const parts = dateStr.trim().split('.');
97
- if (parts.length !== 3) {
98
- return null;
99
- }
100
- let day = parseInt(parts[0], 10);
101
- let month = parseInt(parts[1], 10) - 1; // Month is 0-indexed
102
- let year = parseInt(parts[2], 10);
103
-
104
- // Handle 2-digit years (e.g. 25 -> 2025)
105
- if (year < 100) {
106
- year += 2000;
107
- }
108
-
109
- if (isNaN(day) || isNaN(month) || isNaN(year)) {
110
- return null;
111
- }
112
-
113
- // Create date at noon to avoid timezone shift issues (especially with ISO export)
114
- return new Date(year, month, day, 12, 0, 0);
115
- }
116
-
117
55
  /**
118
56
  * Checks if the current time is within the High Tariff (HT) period
119
57
  *
@@ -180,75 +118,18 @@ const DEFAULTS = {
180
118
  MIN_CONSUMPTION: 0,
181
119
  };
182
120
 
183
- /**
184
- * Formats a Date object to YYYY-MM-DD HH:mm:ss string
185
- *
186
- * @param {Date} date - Date object
187
- * @returns {string|null} Formatted date string or null
188
- */
189
- function formatDateString(date) {
190
- if (!date) {
191
- return null;
192
- }
193
- if (!(date instanceof Date)) {
194
- // If it's already a string, try to parse and re-format, or just return if it handles
195
- // But the error said "calculator.formatDateString is not a function", so we need it here.
196
- return null;
197
- }
198
-
199
- const pad = num => num.toString().padStart(2, '0');
200
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
201
- }
202
-
203
- /**
204
- * Parses a date string (ISO or standard format)
205
- *
206
- * @param {string} dateStr - Date string
207
- * @returns {Date|null} Date object or null
208
- */
209
- function parseDateString(dateStr) {
210
- if (!dateStr) {
211
- return null;
212
- }
213
- const date = new Date(dateStr);
214
- if (isNaN(date.getTime())) {
215
- return null;
216
- }
217
- return date;
218
- }
219
-
220
- /**
221
- * Checks if a year is a leap year
222
- *
223
- * @param {number} year - Year to check
224
- * @returns {boolean} True if leap year
225
- */
226
- function isLeapYear(year) {
227
- return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
228
- }
229
-
230
- /**
231
- * Calculates the difference in months between two dates
232
- *
233
- * @param {Date} startDate - Start date
234
- * @param {Date} endDate - End date
235
- * @returns {number} Difference in months
236
- */
237
- function getMonthsDifference(startDate, endDate) {
238
- return (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth());
239
- }
240
-
241
121
  module.exports = {
242
122
  convertGasM3ToKWh,
243
123
  getCurrentPrice,
244
124
  calculateCost,
245
- ensureNumber,
246
- roundToDecimals,
247
- parseGermanDate,
248
125
  isHTTime,
249
- formatDateString,
250
- parseDateString,
251
- isLeapYear,
252
- getMonthsDifference,
253
126
  DEFAULTS,
127
+ // Re-export helpers for backward compatibility
128
+ ensureNumber: helpers.ensureNumber,
129
+ roundToDecimals: helpers.roundToDecimals,
130
+ parseGermanDate: helpers.parseGermanDate,
131
+ formatDateString: helpers.formatDateString,
132
+ parseDateString: s => helpers.parseGermanDate(s),
133
+ isLeapYear: helpers.isLeapYear,
134
+ getMonthsDifference: helpers.getMonthsDifference,
254
135
  };
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const calculator = require('./calculator');
4
+ const { getConfigType } = require('./utils/typeMapper');
4
5
  const stateManager = require('./stateManager');
5
6
 
6
7
  /**
7
- * ConsumptionManager handles all sensor-related logic,
8
- * including initialization, unit conversion, and sensor updates.
8
+ * ConsumptionManager handles utility initialization and price updates.
9
+ * NOTE: Sensor handling has been moved to MultiMeterManager since v1.4.6.
9
10
  */
10
11
  class ConsumptionManager {
11
12
  /**
@@ -13,22 +14,6 @@ class ConsumptionManager {
13
14
  */
14
15
  constructor(adapter) {
15
16
  this.adapter = adapter;
16
- this.lastSensorValues = {};
17
- }
18
-
19
- /**
20
- * Maps internal utility type to config/state name
21
- *
22
- * @param {string} type - gas, water, or electricity
23
- * @returns {string} - gas, wasser, or strom
24
- */
25
- getConfigType(type) {
26
- const mapping = {
27
- electricity: 'strom',
28
- water: 'wasser',
29
- gas: 'gas',
30
- };
31
- return mapping[type] || type;
32
17
  }
33
18
 
34
19
  /**
@@ -50,7 +35,7 @@ class ConsumptionManager {
50
35
  // State structure is now created by MultiMeterManager per meter (v1.4.6)
51
36
  // Old createUtilityStateStructure removed - states are created under type.meterName.*
52
37
 
53
- const configType = this.getConfigType(type);
38
+ const configType = getConfigType(type);
54
39
  const sensorDPKey = `${configType}SensorDP`;
55
40
  const sensorDP = this.adapter.config[sensorDPKey];
56
41
 
@@ -79,244 +64,16 @@ class ConsumptionManager {
79
64
  }
80
65
 
81
66
  // Note: All initialization moved to MultiMeterManager in v1.4.6:
67
+ // - State creation (per meter)
82
68
  // - Sensor value restoration (per meter)
83
69
  // - Period start timestamps (per meter)
70
+ // - Initial yearly consumption calculation (per meter)
84
71
  // - Current price updates (per meter)
85
72
  // - Cost calculations (per meter)
73
+ // - Billing countdown (per meter)
86
74
  // Old type-level states (gas.info.*, gas.statistics.*) are no longer used
87
75
 
88
- // Initialize yearly consumption from initial reading if set
89
- // NOTE: This is now handled per meter by MultiMeterManager in v1.4.6
90
- // This legacy code path should not execute for new setups, but is kept for safety
91
- const initialReadingKey = `${configType}InitialReading`;
92
- const initialReading = this.adapter.config[initialReadingKey] || 0;
93
-
94
- if (initialReading > 0 && sensorDP) {
95
- // Get the main meter name to use the correct path
96
- const mainMeterNameKey = `${configType}MainMeterName`;
97
- const mainMeterName = this.adapter.config[mainMeterNameKey] || 'main';
98
- const basePath = `${type}.${mainMeterName}`;
99
-
100
- const sensorState = await this.adapter.getForeignStateAsync(sensorDP);
101
- if (sensorState && typeof sensorState.val === 'number') {
102
- let currentRaw = sensorState.val;
103
-
104
- // Apply offset if configured (in original unit)
105
- const offsetKey = `${configType}Offset`;
106
- const offset = this.adapter.config[offsetKey] || 0;
107
- if (offset !== 0) {
108
- currentRaw = currentRaw - offset;
109
- this.adapter.log.debug(`Applied offset for ${type}: -${offset}, new value: ${currentRaw}`);
110
- }
111
- let yearlyConsumption = Math.max(0, currentRaw - initialReading);
112
-
113
- // For gas: convert m³ to kWh AFTER calculating the difference
114
- if (type === 'gas') {
115
- const brennwert = this.adapter.config.gasBrennwert || 11.5;
116
- const zZahl = this.adapter.config.gasZahl || 0.95;
117
- const yearlyVolume = yearlyConsumption;
118
- yearlyConsumption = calculator.convertGasM3ToKWh(yearlyConsumption, brennwert, zZahl);
119
- await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, yearlyVolume, true);
120
- this.adapter.log.info(
121
- `Init yearly ${type}: ${yearlyConsumption.toFixed(2)} kWh = ${(currentRaw - initialReading).toFixed(2)} m³ (current: ${currentRaw.toFixed(2)} m³, initial: ${initialReading} m³)`,
122
- );
123
- } else {
124
- this.adapter.log.info(
125
- `Init yearly ${type}: ${yearlyConsumption.toFixed(2)} (current: ${currentRaw.toFixed(2)}, initial: ${initialReading})`,
126
- );
127
- }
128
-
129
- await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyConsumption, true);
130
- if (typeof this.adapter.updateCosts === 'function') {
131
- await this.adapter.updateCosts(type);
132
- }
133
- }
134
- }
135
-
136
- // Note: Billing countdown is now handled per meter by billingManager.updateBillingCountdown()
137
- // which is called during checkPeriodResets()
138
-
139
- this.adapter.log.debug(`Initial cost calculation completed for ${type}`);
140
- }
141
-
142
- /**
143
- * Handles sensor value updates
144
- *
145
- * NOTE: This method is DEPRECATED since v1.4.6!
146
- * All sensor updates are now handled by MultiMeterManager.handleSensorUpdate()
147
- * This method remains only as fallback but should NEVER be called in normal operation.
148
- *
149
- * @param {string} type - Utility type
150
- * @param {string} sensorDP - Sensor datapoint ID
151
- * @param {number} value - New sensor value
152
- */
153
- async handleSensorUpdate(type, sensorDP, value) {
154
- this.adapter.log.warn(
155
- `consumptionManager.handleSensorUpdate() called - this is deprecated! All sensors should be handled by MultiMeterManager.`,
156
- );
157
-
158
- if (typeof value !== 'number' || value < 0) {
159
- this.adapter.log.warn(`Invalid sensor value for ${type}: ${value}`);
160
- return;
161
- }
162
-
163
- this.adapter.log.debug(`[DEPRECATED] Sensor update for ${type}: ${value}`);
164
-
165
- const now = Date.now();
166
- let consumption = value;
167
- let consumptionM3 = null;
168
-
169
- const configType = this.getConfigType(type);
170
-
171
- // Apply offset FIRST
172
- const offsetKey = `${configType}Offset`;
173
- const offset = this.adapter.config[offsetKey] || 0;
174
- if (offset !== 0) {
175
- consumption = consumption - offset;
176
- this.adapter.log.debug(`Applied offset for ${type}: -${offset}, new value: ${consumption}`);
177
- }
178
-
179
- // For gas, convert m³ to kWh
180
- if (type === 'gas') {
181
- const brennwert = this.adapter.config.gasBrennwert || 11.5;
182
- const zZahl = this.adapter.config.gasZahl || 0.95;
183
- consumptionM3 = consumption;
184
- await this.adapter.setStateAsync(`${type}.info.meterReadingVolume`, consumption, true);
185
- consumption = calculator.convertGasM3ToKWh(consumption, brennwert, zZahl);
186
- consumption = calculator.roundToDecimals(consumption, 2);
187
- }
188
-
189
- // Update meter reading
190
- await this.adapter.setStateAsync(`${type}.info.meterReading`, consumption, true);
191
-
192
- // Calculate deltas
193
- const lastValue = this.lastSensorValues[sensorDP];
194
- this.lastSensorValues[sensorDP] = consumption;
195
-
196
- if (lastValue === undefined || consumption < lastValue) {
197
- if (lastValue !== undefined && consumption < lastValue) {
198
- this.adapter.log.warn(
199
- `${type}: Sensor value decreased (${lastValue} -> ${consumption}). Assuming meter reset or replacement.`,
200
- );
201
- }
202
- if (typeof this.adapter.updateCosts === 'function') {
203
- await this.adapter.updateCosts(type);
204
- }
205
- return;
206
- }
207
-
208
- const delta = consumption - lastValue;
209
- this.adapter.log.debug(`${type} delta: ${delta}`);
210
-
211
- // Track volume for gas
212
- if (type === 'gas') {
213
- const brennwert = this.adapter.config.gasBrennwert || 11.5;
214
- const zZahl = this.adapter.config.gasZahl || 0.95;
215
- const deltaVolume = delta / (brennwert * zZahl);
216
-
217
- const dailyVolume = await this.adapter.getStateAsync(`${type}.consumption.dailyVolume`);
218
- const monthlyVolume = await this.adapter.getStateAsync(`${type}.consumption.monthlyVolume`);
219
- const yearlyVolume = await this.adapter.getStateAsync(`${type}.consumption.yearlyVolume`);
220
-
221
- await this.adapter.setStateAsync(
222
- `${type}.consumption.dailyVolume`,
223
- calculator.roundToDecimals((dailyVolume?.val || 0) + deltaVolume, 2),
224
- true,
225
- );
226
- await this.adapter.setStateAsync(
227
- `${type}.consumption.monthlyVolume`,
228
- calculator.roundToDecimals((monthlyVolume?.val || 0) + deltaVolume, 2),
229
- true,
230
- );
231
- await this.adapter.setStateAsync(
232
- `${type}.consumption.yearlyVolume`,
233
- calculator.roundToDecimals((yearlyVolume?.val || 0) + deltaVolume, 3),
234
- true,
235
- );
236
- }
237
-
238
- // Update consumption values
239
- const dailyState = await this.adapter.getStateAsync(`${type}.consumption.daily`);
240
- await this.adapter.setStateAsync(
241
- `${type}.consumption.daily`,
242
- calculator.roundToDecimals((dailyState?.val || 0) + delta, 2),
243
- true,
244
- );
245
-
246
- const monthlyState = await this.adapter.getStateAsync(`${type}.consumption.monthly`);
247
- await this.adapter.setStateAsync(
248
- `${type}.consumption.monthly`,
249
- calculator.roundToDecimals((monthlyState?.val || 0) + delta, 2),
250
- true,
251
- );
252
-
253
- // HT/NT tracking
254
- const htNtEnabledKey = `${configType}HtNtEnabled`;
255
- if (this.adapter.config[htNtEnabledKey]) {
256
- const isHT = calculator.isHTTime(this.adapter.config, configType);
257
- const suffix = isHT ? 'HT' : 'NT';
258
-
259
- const dHTNT = await this.adapter.getStateAsync(`${type}.consumption.daily${suffix}`);
260
- await this.adapter.setStateAsync(
261
- `${type}.consumption.daily${suffix}`,
262
- calculator.roundToDecimals((dHTNT?.val || 0) + delta, 2),
263
- true,
264
- );
265
-
266
- const mHTNT = await this.adapter.getStateAsync(`${type}.consumption.monthly${suffix}`);
267
- await this.adapter.setStateAsync(
268
- `${type}.consumption.monthly${suffix}`,
269
- calculator.roundToDecimals((mHTNT?.val || 0) + delta, 2),
270
- true,
271
- );
272
-
273
- const yHTNT = await this.adapter.getStateAsync(`${type}.consumption.yearly${suffix}`);
274
- await this.adapter.setStateAsync(
275
- `${type}.consumption.yearly${suffix}`,
276
- calculator.roundToDecimals((yHTNT?.val || 0) + delta, 2),
277
- true,
278
- );
279
- }
280
-
281
- // Yearly consumption
282
- const initialReadingKey = `${configType}InitialReading`;
283
- const initialReading = this.adapter.config[initialReadingKey] || 0;
284
-
285
- if (initialReading > 0) {
286
- let yearlyAmount;
287
- if (type === 'gas') {
288
- const yearlyM3 = Math.max(0, (consumptionM3 || 0) - initialReading);
289
- await this.adapter.setStateAsync(
290
- `${type}.consumption.yearlyVolume`,
291
- calculator.roundToDecimals(yearlyM3, 2),
292
- true,
293
- );
294
- const brennwert = this.adapter.config.gasBrennwert || 11.5;
295
- const zZahl = this.adapter.config.gasZahl || 0.95;
296
- yearlyAmount = calculator.convertGasM3ToKWh(yearlyM3, brennwert, zZahl);
297
- } else {
298
- yearlyAmount = Math.max(0, consumption - initialReading);
299
- }
300
- await this.adapter.setStateAsync(
301
- `${type}.consumption.yearly`,
302
- calculator.roundToDecimals(yearlyAmount, 2),
303
- true,
304
- );
305
- } else {
306
- const yState = await this.adapter.getStateAsync(`${type}.consumption.yearly`);
307
- await this.adapter.setStateAsync(
308
- `${type}.consumption.yearly`,
309
- calculator.roundToDecimals((yState?.val || 0) + delta, 2),
310
- true,
311
- );
312
- }
313
-
314
- if (typeof this.adapter.updateCosts === 'function') {
315
- await this.adapter.updateCosts(type);
316
- }
317
-
318
- await this.adapter.setStateAsync(`${type}.consumption.lastUpdate`, now, true);
319
- await this.adapter.setStateAsync(`${type}.info.lastSync`, now, true);
76
+ this.adapter.log.debug(`${type} initialization delegated to MultiMeterManager`);
320
77
  }
321
78
 
322
79
  /**
@@ -327,7 +84,7 @@ class ConsumptionManager {
327
84
  * @param {string} type - Utility type
328
85
  */
329
86
  async updateCurrentPrice(type) {
330
- const configType = this.getConfigType(type);
87
+ const configType = getConfigType(type);
331
88
 
332
89
  // Get all meters for this type
333
90
  const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];