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.
Files changed (36) hide show
  1. package/README.md +110 -62
  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 +15 -31
  22. package/lib/billingManager.js +382 -137
  23. package/lib/calculator.js +41 -146
  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 +580 -173
  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 +234 -0
  33. package/lib/utils/stateCache.js +147 -0
  34. package/lib/utils/typeMapper.js +19 -0
  35. package/main.js +67 -8
  36. 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,234 @@
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
+ /**
170
+ * Safe execution wrapper for async operations with error handling.
171
+ * Catches errors and logs them without crashing the adapter.
172
+ *
173
+ * @param {object} adapter - Adapter instance
174
+ * @param {Function} fn - Async function to execute
175
+ * @param {string} context - Context description for error logging
176
+ * @param {any} fallback - Fallback value to return on error
177
+ * @returns {Promise<any>} Result of fn() or fallback on error
178
+ */
179
+ async function safeExecute(adapter, fn, context, fallback = null) {
180
+ try {
181
+ return await fn();
182
+ } catch (error) {
183
+ if (adapter && adapter.log) {
184
+ adapter.log.error(`[${context}] ${error.message}`);
185
+ }
186
+ return fallback;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Debounce function to limit execution frequency
192
+ *
193
+ * @param {Function} fn - Function to debounce
194
+ * @param {number} delay - Delay in milliseconds
195
+ * @returns {Function} Debounced function
196
+ */
197
+ function debounce(fn, delay) {
198
+ let timeoutId;
199
+ return function (...args) {
200
+ clearTimeout(timeoutId);
201
+ timeoutId = setTimeout(() => fn.apply(this, args), delay);
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Validates that a value is within a specified range
207
+ *
208
+ * @param {number} value - Value to validate
209
+ * @param {number} min - Minimum allowed value
210
+ * @param {number} max - Maximum allowed value
211
+ * @param {number} [defaultValue] - Default value if out of range
212
+ * @returns {number} Validated value or default
213
+ */
214
+ function validateRange(value, min, max, defaultValue = min) {
215
+ const num = ensureNumber(value);
216
+ if (num < min || num > max) {
217
+ return defaultValue;
218
+ }
219
+ return num;
220
+ }
221
+
222
+ module.exports = {
223
+ ensureNumber,
224
+ roundToDecimals,
225
+ parseGermanDate,
226
+ formatDateString,
227
+ isLeapYear,
228
+ getMonthsDifference,
229
+ normalizeMeterName,
230
+ safeSetObjectNotExists,
231
+ safeExecute,
232
+ debounce,
233
+ validateRange,
234
+ };
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * StateCache - Provides caching for adapter state operations.
5
+ * Reduces database queries by caching state values during update cycles.
6
+ */
7
+ class StateCache {
8
+ /**
9
+ * Creates a new StateCache instance
10
+ *
11
+ * @param {object} adapter - ioBroker adapter instance
12
+ * @param {object} [options] - Configuration options
13
+ * @param {number} [options.maxAge] - Maximum cache age in milliseconds (default: 60s)
14
+ * @param {number} [options.maxSize] - Maximum number of cached entries
15
+ */
16
+ constructor(adapter, options = {}) {
17
+ this.adapter = adapter;
18
+ this.cache = new Map();
19
+ this.maxAge = options.maxAge || 60000;
20
+ this.maxSize = options.maxSize || 1000;
21
+ this.hits = 0;
22
+ this.misses = 0;
23
+ }
24
+
25
+ /**
26
+ * Gets a state value, using cache if available
27
+ *
28
+ * @param {string} id - State ID
29
+ * @returns {Promise<any>} State value or null
30
+ */
31
+ async get(id) {
32
+ const cached = this.cache.get(id);
33
+ const now = Date.now();
34
+
35
+ if (cached && now - cached.timestamp < this.maxAge) {
36
+ this.hits++;
37
+ return cached.value;
38
+ }
39
+
40
+ this.misses++;
41
+ const state = await this.adapter.getStateAsync(id);
42
+ const value = state?.val ?? null;
43
+
44
+ this._set(id, value);
45
+ return value;
46
+ }
47
+
48
+ /**
49
+ * Gets a state object (with val, ack, ts), using cache if available
50
+ *
51
+ * @param {string} id - State ID
52
+ * @returns {Promise<object|null>} State object or null
53
+ */
54
+ async getState(id) {
55
+ const cached = this.cache.get(`${id}_state`);
56
+ const now = Date.now();
57
+
58
+ if (cached && now - cached.timestamp < this.maxAge) {
59
+ this.hits++;
60
+ return cached.value;
61
+ }
62
+
63
+ this.misses++;
64
+ const state = await this.adapter.getStateAsync(id);
65
+
66
+ this._set(`${id}_state`, state);
67
+ if (state) {
68
+ this._set(id, state.val);
69
+ }
70
+ return state;
71
+ }
72
+
73
+ /**
74
+ * Sets a state value and updates cache
75
+ *
76
+ * @param {string} id - State ID
77
+ * @param {any} value - Value to set
78
+ * @param {boolean} ack - Acknowledge flag
79
+ * @returns {Promise<void>}
80
+ */
81
+ async set(id, value, ack = true) {
82
+ await this.adapter.setStateAsync(id, value, ack);
83
+ this._set(id, value);
84
+ }
85
+
86
+ /**
87
+ * Internal method to add to cache
88
+ *
89
+ * @param {string} id - State ID
90
+ * @param {any} value - Value to cache
91
+ */
92
+ _set(id, value) {
93
+ // Evict oldest entries if cache is full
94
+ if (this.cache.size >= this.maxSize) {
95
+ const oldestKey = this.cache.keys().next().value;
96
+ this.cache.delete(oldestKey);
97
+ }
98
+
99
+ this.cache.set(id, {
100
+ value,
101
+ timestamp: Date.now(),
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Clears the entire cache.
107
+ * Should be called at the end of each update cycle.
108
+ */
109
+ clear() {
110
+ this.cache.clear();
111
+ }
112
+
113
+ /**
114
+ * Invalidates a specific cache entry
115
+ *
116
+ * @param {string} id - State ID to invalidate
117
+ */
118
+ invalidate(id) {
119
+ this.cache.delete(id);
120
+ this.cache.delete(`${id}_state`);
121
+ }
122
+
123
+ /**
124
+ * Gets cache statistics
125
+ *
126
+ * @returns {object} Cache statistics
127
+ */
128
+ getStats() {
129
+ const total = this.hits + this.misses;
130
+ return {
131
+ hits: this.hits,
132
+ misses: this.misses,
133
+ hitRate: total > 0 ? `${((this.hits / total) * 100).toFixed(1)}%` : '0%',
134
+ size: this.cache.size,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Resets cache statistics
140
+ */
141
+ resetStats() {
142
+ this.hits = 0;
143
+ this.misses = 0;
144
+ }
145
+ }
146
+
147
+ module.exports = StateCache;
@@ -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,60 @@ 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(`${type.label} Zähler "${meter.name}": Kein Vertragsbeginn konfiguriert!`);
129
+ }
130
+ if (!meter.sensorDP) {
131
+ this.log.warn(`${type.label} Zähler "${meter.name}": Kein Sensor-Datenpunkt konfiguriert!`);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+
85
139
  // --- Delegation Methods (backward compatibility for internal calls) ---
86
140
 
87
141
  async initializeUtility(type, isActive) {
88
142
  return this.consumptionManager.initializeUtility(type, isActive);
89
143
  }
90
144
 
91
- async handleSensorUpdate(type, sensorDP, value) {
92
- return this.consumptionManager.handleSensorUpdate(type, sensorDP, value);
93
- }
94
-
95
145
  async updateCurrentPrice(type) {
96
146
  return this.consumptionManager.updateCurrentPrice(type);
97
147
  }
@@ -253,9 +303,14 @@ class UtilityMonitor extends utils.Adapter {
253
303
  // Determine which utility this sensor belongs to
254
304
  // All meters (including main) are now handled by multiMeterManager
255
305
  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);
306
+ const meters = this.multiMeterManager.findMeterBySensor(id);
307
+ if (meters.length > 0 && state.val != null) {
308
+ // Convert sensor value to number (handles strings, German commas, etc.)
309
+ const numValue = calculator.ensureNumber(state.val);
310
+ // Call handleSensorUpdate for each meter using this sensor
311
+ for (const meterInfo of meters) {
312
+ await this.multiMeterManager.handleSensorUpdate(meterInfo.type, meterInfo.meterName, id, numValue);
313
+ }
259
314
  return;
260
315
  }
261
316
  }
@@ -267,7 +322,11 @@ class UtilityMonitor extends utils.Adapter {
267
322
  * @param {Record<string, any>} obj - Message object from config
268
323
  */
269
324
  async onMessage(obj) {
270
- await this.messagingHandler.handleMessage(obj);
325
+ if (obj.command === 'importCSV') {
326
+ await this.importManager.handleImportCSV(obj);
327
+ } else {
328
+ await this.messagingHandler.handleMessage(obj);
329
+ }
271
330
  }
272
331
  }
273
332
 
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.1",
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",