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,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
|
|
257
|
-
if (
|
|
258
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
58
|
-
"admin
|
|
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",
|