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
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const calculator = require('../calculator');
4
+
5
+ /**
6
+ * Shared billing and cost calculation logic
7
+ */
8
+
9
+ /**
10
+ * Calculates accumulated basic charges over a period
11
+ *
12
+ * @param {number} monthlyFee - Monthly basic charge
13
+ * @param {number} annualFee - Fixed annual fee
14
+ * @param {number} months - Number of months since contract start
15
+ * @returns {object} { basicCharge, annualFee, total }
16
+ */
17
+ function calculateAccumulatedCharges(monthlyFee, annualFee, months) {
18
+ const basicCharge = (monthlyFee || 0) * months;
19
+ const annual = annualFee || 0;
20
+ return {
21
+ basicCharge: calculator.roundToDecimals(basicCharge, 2),
22
+ annualFee: calculator.roundToDecimals(annual, 2),
23
+ total: calculator.roundToDecimals(basicCharge + annual, 2),
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Calculates total paid and balance
29
+ *
30
+ * @param {number} monthlyAbschlag - Monthly installment
31
+ * @param {number} months - Months since start
32
+ * @param {number} totalCosts - Total costs calculated
33
+ * @returns {object} { paid, balance }
34
+ */
35
+ function calculateBalance(monthlyAbschlag, months, totalCosts) {
36
+ const paid = (monthlyAbschlag || 0) * months;
37
+ const balance = totalCosts > 0.01 || paid > 0.01 ? paid - totalCosts : 0;
38
+
39
+ return {
40
+ paid: calculator.roundToDecimals(paid, 2),
41
+ balance: calculator.roundToDecimals(balance, 2),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Calculates costs for HT/NT split
47
+ *
48
+ * @param {number} htQty - High tariff quantity
49
+ * @param {number} htPrice - High tariff price
50
+ * @param {number} ntQty - Low tariff quantity
51
+ * @param {number} ntPrice - Low tariff price
52
+ * @returns {object} { htCosts, ntCosts, total }
53
+ */
54
+ function calculateHTNTCosts(htQty, htPrice, ntQty, ntPrice) {
55
+ const ht = (htQty || 0) * (htPrice || 0);
56
+ const nt = (ntQty || 0) * (ntPrice || 0);
57
+
58
+ return {
59
+ htCosts: calculator.roundToDecimals(ht, 2),
60
+ ntCosts: calculator.roundToDecimals(nt, 2),
61
+ total: calculator.roundToDecimals(ht + nt, 2),
62
+ };
63
+ }
64
+
65
+ module.exports = {
66
+ calculateAccumulatedCharges,
67
+ calculateBalance,
68
+ calculateHTNTCosts,
69
+ };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const calculator = require('../calculator');
4
+
5
+ /**
6
+ * Shared consumption logic for different managers
7
+ */
8
+
9
+ /**
10
+ * Calculates gas energy (kWh) and volume (m³)
11
+ *
12
+ * @param {number} value - Raw sensor value (m³)
13
+ * @param {number} brennwert - Calorific value
14
+ * @param {number} zZahl - Z-number
15
+ * @returns {object} { energy, volume }
16
+ */
17
+ function calculateGas(value, brennwert, zZahl) {
18
+ const energy = calculator.convertGasM3ToKWh(value, brennwert, zZahl);
19
+ return {
20
+ energy: calculator.roundToDecimals(energy, 2),
21
+ volume: calculator.roundToDecimals(value, 2),
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Returns the suffix (HT/NT) based on current time
27
+ *
28
+ * @param {object} config - Adapter config
29
+ * @param {string} type - Utility type (config name)
30
+ * @returns {string} 'HT', 'NT' or empty if disabled
31
+ */
32
+ function getHTNTSuffix(config, type) {
33
+ if (!config || !type) {
34
+ return '';
35
+ }
36
+ const enabled = config[`${type}HtNtEnabled`];
37
+ if (!enabled) {
38
+ return '';
39
+ }
40
+
41
+ return calculator.isHTTime(config, type) ? 'HT' : 'NT';
42
+ }
43
+
44
+ module.exports = {
45
+ calculateGas,
46
+ getHTNTSuffix,
47
+ };
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Helper utilities for iobroker.utility-monitor
5
+ */
6
+
7
+ /**
8
+ * Ensures a value is a number, handling German decimal commas if provided as string.
9
+ *
10
+ * @param {any} value - Value to convert
11
+ * @returns {number} The numeric value
12
+ */
13
+ function ensureNumber(value) {
14
+ if (value === undefined || value === null || value === '') {
15
+ return 0;
16
+ }
17
+ if (typeof value === 'number') {
18
+ return isNaN(value) ? 0 : value;
19
+ }
20
+ if (typeof value === 'string') {
21
+ let normalized = value.trim();
22
+ // Handle common European formats: 1.234,56 -> 1234.56 or 1234,56 -> 1234.56
23
+ if (normalized.includes(',') && normalized.includes('.')) {
24
+ // Assume . is thousands and , is decimal
25
+ normalized = normalized.replace(/\./g, '').replace(',', '.');
26
+ } else if (normalized.includes(',')) {
27
+ // Assume , is decimal
28
+ normalized = normalized.replace(',', '.');
29
+ }
30
+ const parsed = parseFloat(normalized);
31
+ return isNaN(parsed) ? 0 : parsed;
32
+ }
33
+ const num = Number(value);
34
+ return isNaN(num) ? 0 : num;
35
+ }
36
+
37
+ /**
38
+ * Rounds a number to specified decimal places
39
+ *
40
+ * @param {number|string} value - Value to round
41
+ * @param {number} decimals - Number of decimal places (default: 2)
42
+ * @returns {number} Rounded value
43
+ */
44
+ function roundToDecimals(value, decimals = 2) {
45
+ const numValue = ensureNumber(value);
46
+ const factor = Math.pow(10, decimals);
47
+ return Math.round(numValue * factor) / factor;
48
+ }
49
+
50
+ /**
51
+ * Parses a German date string (DD.MM.YYYY) into a Date object
52
+ *
53
+ * @param {string} dateStr - Date string in format DD.MM.YYYY
54
+ * @returns {Date|null} Date object or null if invalid
55
+ */
56
+ function parseGermanDate(dateStr) {
57
+ if (!dateStr || typeof dateStr !== 'string') {
58
+ return null;
59
+ }
60
+
61
+ const trimmed = dateStr.trim();
62
+
63
+ // 1. Try German format (DD.MM.YYYY)
64
+ if (trimmed.includes('.') && !trimmed.includes('-')) {
65
+ const parts = trimmed.split('.');
66
+ if (parts.length === 3) {
67
+ const day = parseInt(parts[0], 10);
68
+ const month = parseInt(parts[1], 10) - 1;
69
+ let year = parseInt(parts[2], 10);
70
+
71
+ if (year < 70) {
72
+ year += 2000;
73
+ } else if (year < 100) {
74
+ year += 1900;
75
+ }
76
+
77
+ if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
78
+ return new Date(year, month, day, 12, 0, 0);
79
+ }
80
+ }
81
+ }
82
+
83
+ // 2. Try ISO or other standard formats
84
+ const fallback = new Date(trimmed);
85
+ if (!isNaN(fallback.getTime())) {
86
+ return fallback;
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Formats a Date object to YYYY-MM-DD HH:mm:ss string
94
+ *
95
+ * @param {Date} date - Date object
96
+ * @returns {string|null} Formatted date string or null
97
+ */
98
+ function formatDateString(date) {
99
+ if (!date || !(date instanceof Date)) {
100
+ return null;
101
+ }
102
+
103
+ const pad = num => num.toString().padStart(2, '0');
104
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
105
+ }
106
+
107
+ /**
108
+ * Checks if a year is a leap year
109
+ *
110
+ * @param {number} year - Year to check
111
+ * @returns {boolean} True if leap year
112
+ */
113
+ function isLeapYear(year) {
114
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
115
+ }
116
+
117
+ /**
118
+ * Calculates the difference in months between two dates
119
+ *
120
+ * @param {Date} startDate - Start date
121
+ * @param {Date} endDate - End date
122
+ * @returns {number} Difference in months
123
+ */
124
+ function getMonthsDifference(startDate, endDate) {
125
+ if (!startDate || !endDate) {
126
+ return 0;
127
+ }
128
+ return (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth());
129
+ }
130
+
131
+ /**
132
+ * Normalizes a meter name to a valid ioBroker ID part
133
+ *
134
+ * @param {string} name - The name to normalize
135
+ * @returns {string} The normalized name
136
+ */
137
+ function normalizeMeterName(name) {
138
+ if (!name) {
139
+ return 'unknown';
140
+ }
141
+
142
+ return name
143
+ .toLowerCase()
144
+ .replace(/ä/g, 'ae')
145
+ .replace(/ö/g, 'oe')
146
+ .replace(/ü/g, 'ue')
147
+ .replace(/ß/g, 'ss')
148
+ .replace(/[^a-z0-9]/g, '_')
149
+ .replace(/_+/g, '_')
150
+ .replace(/^_|_$/g, '')
151
+ .substring(0, 32);
152
+ }
153
+
154
+ /**
155
+ * Safe wrapper for setObjectNotExistsAsync with error handling
156
+ *
157
+ * @param {object} adapter - Adapter instance
158
+ * @param {string} id - State ID
159
+ * @param {object} obj - Object definition
160
+ */
161
+ async function safeSetObjectNotExists(adapter, id, obj) {
162
+ try {
163
+ await adapter.setObjectNotExistsAsync(id, obj);
164
+ } catch (e) {
165
+ adapter.log.error(`Error creating object ${id}: ${e.message}`);
166
+ }
167
+ }
168
+
169
+ module.exports = {
170
+ ensureNumber,
171
+ roundToDecimals,
172
+ parseGermanDate,
173
+ formatDateString,
174
+ isLeapYear,
175
+ getMonthsDifference,
176
+ normalizeMeterName,
177
+ safeSetObjectNotExists,
178
+ };
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Maps internal utility type to config/state name
5
+ *
6
+ * @param {string} type - gas, water, electricity, pv
7
+ * @returns {string} - gas, wasser, strom, pv
8
+ */
9
+ function getConfigType(type) {
10
+ const mapping = {
11
+ electricity: 'strom',
12
+ water: 'wasser',
13
+ gas: 'gas',
14
+ pv: 'pv',
15
+ };
16
+ return mapping[type] || type;
17
+ }
18
+
19
+ module.exports = { getConfigType };
package/main.js CHANGED
@@ -10,6 +10,8 @@ const ConsumptionManager = require('./lib/consumptionManager');
10
10
  const BillingManager = require('./lib/billingManager');
11
11
  const MessagingHandler = require('./lib/messagingHandler');
12
12
  const MultiMeterManager = require('./lib/multiMeterManager');
13
+ const ImportManager = require('./lib/importManager');
14
+ const calculator = require('./lib/calculator');
13
15
 
14
16
  class UtilityMonitor extends utils.Adapter {
15
17
  /**
@@ -29,6 +31,7 @@ class UtilityMonitor extends utils.Adapter {
29
31
  this.consumptionManager = new ConsumptionManager(this);
30
32
  this.billingManager = new BillingManager(this);
31
33
  this.messagingHandler = new MessagingHandler(this);
34
+ this.importManager = new ImportManager(this);
32
35
  this.multiMeterManager = null; // Initialized in onReady after other managers
33
36
 
34
37
  this.periodicTimers = {};
@@ -43,6 +46,9 @@ class UtilityMonitor extends utils.Adapter {
43
46
  // Initialize MultiMeterManager
44
47
  this.multiMeterManager = new MultiMeterManager(this, this.consumptionManager, this.billingManager);
45
48
 
49
+ // Validate configuration before starting
50
+ this.validateConfiguration();
51
+
46
52
  // Initialize each utility type based on configuration
47
53
  // Note: initializeUtility() internally calls multiMeterManager.initializeType()
48
54
  await this.initializeUtility('gas', this.config.gasAktiv);
@@ -82,16 +88,64 @@ class UtilityMonitor extends utils.Adapter {
82
88
  this.log.info('Nebenkosten-Monitor initialized successfully');
83
89
  }
84
90
 
91
+ /**
92
+ * Validates the adapter configuration and logs warnings for missing settings
93
+ */
94
+ validateConfiguration() {
95
+ const types = [
96
+ { key: 'gas', configKey: 'gas', label: 'Gas' },
97
+ { key: 'water', configKey: 'wasser', label: 'Wasser' },
98
+ { key: 'electricity', configKey: 'strom', label: 'Strom' },
99
+ { key: 'pv', configKey: 'pv', label: 'PV' },
100
+ ];
101
+
102
+ for (const type of types) {
103
+ const isActive = this.config[`${type.configKey}Aktiv`];
104
+ if (!isActive) {
105
+ continue;
106
+ }
107
+
108
+ // Check main meter contract start
109
+ const contractStart = this.config[`${type.configKey}ContractStart`];
110
+ if (!contractStart) {
111
+ this.log.warn(
112
+ `${type.label}: Kein Vertragsbeginn konfiguriert! Für korrekte Jahresberechnungen und Abrechnungsperioden sollte ein Vertragsbeginn gesetzt werden.`,
113
+ );
114
+ }
115
+
116
+ // Check main meter sensor
117
+ const sensorDP = this.config[`${type.configKey}SensorDP`];
118
+ if (!sensorDP) {
119
+ this.log.warn(`${type.label}: Kein Sensor-Datenpunkt konfiguriert!`);
120
+ }
121
+
122
+ // Check additional meters
123
+ const additionalMeters = this.config[`${type.configKey}AdditionalMeters`];
124
+ if (Array.isArray(additionalMeters)) {
125
+ for (const meter of additionalMeters) {
126
+ if (meter && meter.name) {
127
+ if (!meter.contractStart) {
128
+ this.log.warn(
129
+ `${type.label} Zähler "${meter.name}": Kein Vertragsbeginn konfiguriert!`,
130
+ );
131
+ }
132
+ if (!meter.sensorDP) {
133
+ this.log.warn(
134
+ `${type.label} Zähler "${meter.name}": Kein Sensor-Datenpunkt konfiguriert!`,
135
+ );
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+
85
143
  // --- Delegation Methods (backward compatibility for internal calls) ---
86
144
 
87
145
  async initializeUtility(type, isActive) {
88
146
  return this.consumptionManager.initializeUtility(type, isActive);
89
147
  }
90
148
 
91
- async handleSensorUpdate(type, sensorDP, value) {
92
- return this.consumptionManager.handleSensorUpdate(type, sensorDP, value);
93
- }
94
-
95
149
  async updateCurrentPrice(type) {
96
150
  return this.consumptionManager.updateCurrentPrice(type);
97
151
  }
@@ -253,9 +307,14 @@ class UtilityMonitor extends utils.Adapter {
253
307
  // Determine which utility this sensor belongs to
254
308
  // All meters (including main) are now handled by multiMeterManager
255
309
  if (this.multiMeterManager) {
256
- const meterInfo = this.multiMeterManager.findMeterBySensor(id);
257
- if (meterInfo && typeof state.val === 'number') {
258
- await this.multiMeterManager.handleSensorUpdate(meterInfo.type, meterInfo.meterName, id, state.val);
310
+ const meters = this.multiMeterManager.findMeterBySensor(id);
311
+ if (meters.length > 0 && state.val != null) {
312
+ // Convert sensor value to number (handles strings, German commas, etc.)
313
+ const numValue = calculator.ensureNumber(state.val);
314
+ // Call handleSensorUpdate for each meter using this sensor
315
+ for (const meterInfo of meters) {
316
+ await this.multiMeterManager.handleSensorUpdate(meterInfo.type, meterInfo.meterName, id, numValue);
317
+ }
259
318
  return;
260
319
  }
261
320
  }
@@ -267,7 +326,11 @@ class UtilityMonitor extends utils.Adapter {
267
326
  * @param {Record<string, any>} obj - Message object from config
268
327
  */
269
328
  async onMessage(obj) {
270
- await this.messagingHandler.handleMessage(obj);
329
+ if (obj.command === 'importCSV') {
330
+ await this.importManager.handleImportCSV(obj);
331
+ } else {
332
+ await this.messagingHandler.handleMessage(obj);
333
+ }
271
334
  }
272
335
  }
273
336
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.utility-monitor",
3
- "version": "1.4.6",
3
+ "version": "1.5.0",
4
4
  "description": "Monitor gas, water, and electricity consumption with cost calculation",
5
5
  "author": {
6
6
  "name": "fischi87",
@@ -54,8 +54,14 @@
54
54
  },
55
55
  "main": "main.js",
56
56
  "files": [
57
- "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
58
- "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
57
+ "admin/*.json",
58
+ "admin/*.json5",
59
+ "admin/*.png",
60
+ "admin/*.svg",
61
+ "admin/*.html",
62
+ "admin/*.js",
63
+ "admin/i18n/*.json",
64
+ "admin/custom/**",
59
65
  "lib/",
60
66
  "www/",
61
67
  "io-package.json",
@@ -63,7 +69,7 @@
63
69
  "main.js"
64
70
  ],
65
71
  "scripts": {
66
- "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/*.test.js}\"",
72
+ "test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test|admin)/**/*.test.js,*.test.js,test/**/*.test.js}\"",
67
73
  "test:package": "mocha test/package --exit",
68
74
  "test:integration": "mocha test/integration --exit",
69
75
  "test": "npm run test:js && npm run test:package",