iobroker.utility-monitor 1.4.5 → 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.
- package/README.md +159 -44
- 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 +219 -35
- package/io-package.json +51 -2
- package/lib/billingManager.js +276 -170
- package/lib/calculator.js +19 -138
- package/lib/consumptionManager.js +48 -331
- package/lib/importManager.js +300 -0
- package/lib/messagingHandler.js +112 -49
- package/lib/meter/MeterRegistry.js +110 -0
- package/lib/multiMeterManager.js +410 -181
- package/lib/stateManager.js +508 -36
- package/lib/utils/billingHelper.js +69 -0
- package/lib/utils/consumptionHelper.js +47 -0
- package/lib/utils/helpers.js +178 -0
- package/lib/utils/typeMapper.js +19 -0
- package/main.js +99 -36
- package/package.json +10 -4
package/lib/multiMeterManager.js
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
const calculator = require('./calculator');
|
|
4
4
|
const stateManager = require('./stateManager');
|
|
5
5
|
const { parseConfigNumber } = require('./configParser');
|
|
6
|
+
const { getConfigType } = require('./utils/typeMapper');
|
|
7
|
+
const MeterRegistry = require('./meter/MeterRegistry');
|
|
8
|
+
const helpers = require('./utils/helpers');
|
|
9
|
+
const consumptionHelper = require('./utils/consumptionHelper');
|
|
10
|
+
const billingHelper = require('./utils/billingHelper');
|
|
11
|
+
|
|
12
|
+
// Default constants
|
|
13
|
+
const DEFAULT_SPIKE_THRESHOLD = 500; // Default sensor spike detection limit
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
16
|
* MultiMeterManager handles multiple meters per utility type
|
|
@@ -19,41 +27,19 @@ class MultiMeterManager {
|
|
|
19
27
|
this.consumptionManager = consumptionManager;
|
|
20
28
|
this.billingManager = billingManager;
|
|
21
29
|
this.lastSensorValues = {};
|
|
22
|
-
this.meterRegistry =
|
|
30
|
+
this.meterRegistry = new MeterRegistry();
|
|
31
|
+
this.tempBaselineStore = {}; // Tracks first value after start to prevent peaks
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
/**
|
|
26
35
|
* Maps internal utility type to config/state name
|
|
27
36
|
*
|
|
37
|
+
* @deprecated Use getConfigType from utils/typeMapper directly
|
|
28
38
|
* @param {string} type - gas, water, or electricity
|
|
29
39
|
* @returns {string} - gas, wasser, or strom
|
|
30
40
|
*/
|
|
31
41
|
getConfigType(type) {
|
|
32
|
-
|
|
33
|
-
electricity: 'strom',
|
|
34
|
-
water: 'wasser',
|
|
35
|
-
gas: 'gas',
|
|
36
|
-
};
|
|
37
|
-
return mapping[type] || type;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Normalizes meter name to valid folder name
|
|
42
|
-
* Rules: lowercase, alphanumeric only, max 20 chars
|
|
43
|
-
*
|
|
44
|
-
* @param {string} name - User-provided meter name
|
|
45
|
-
* @returns {string} - Normalized name
|
|
46
|
-
*/
|
|
47
|
-
normalizeMeterName(name) {
|
|
48
|
-
if (!name || typeof name !== 'string') {
|
|
49
|
-
return 'unnamed';
|
|
50
|
-
}
|
|
51
|
-
return (
|
|
52
|
-
name
|
|
53
|
-
.toLowerCase()
|
|
54
|
-
.replace(/[^a-z0-9]/g, '')
|
|
55
|
-
.substring(0, 20) || 'unnamed'
|
|
56
|
-
);
|
|
42
|
+
return getConfigType(type);
|
|
57
43
|
}
|
|
58
44
|
|
|
59
45
|
/**
|
|
@@ -70,8 +56,14 @@ class MultiMeterManager {
|
|
|
70
56
|
// Main meter (always present if type is active)
|
|
71
57
|
const mainActive = this.adapter.config[`${configType}Aktiv`];
|
|
72
58
|
if (mainActive) {
|
|
59
|
+
// Get main meter name from config and normalize
|
|
60
|
+
const mainMeterName = this.adapter.config[`${configType}MainMeterName`] || 'main';
|
|
61
|
+
const normalizedName = helpers.normalizeMeterName(mainMeterName);
|
|
62
|
+
const displayName = mainMeterName; // Original name for display
|
|
63
|
+
|
|
73
64
|
meters.push({
|
|
74
|
-
name:
|
|
65
|
+
name: normalizedName,
|
|
66
|
+
displayName: displayName,
|
|
75
67
|
config: {
|
|
76
68
|
sensorDP: this.adapter.config[`${configType}SensorDP`],
|
|
77
69
|
preis: parseConfigNumber(this.adapter.config[`${configType}Preis`], 0),
|
|
@@ -91,7 +83,7 @@ class MultiMeterManager {
|
|
|
91
83
|
if (Array.isArray(additionalMeters)) {
|
|
92
84
|
for (const meterConfig of additionalMeters) {
|
|
93
85
|
if (meterConfig && meterConfig.name && meterConfig.sensorDP) {
|
|
94
|
-
const normalizedName =
|
|
86
|
+
const normalizedName = helpers.normalizeMeterName(meterConfig.name);
|
|
95
87
|
|
|
96
88
|
// Debug: Log raw config for troubleshooting
|
|
97
89
|
this.adapter.log.debug(
|
|
@@ -123,13 +115,13 @@ class MultiMeterManager {
|
|
|
123
115
|
}
|
|
124
116
|
|
|
125
117
|
/**
|
|
126
|
-
* Finds
|
|
118
|
+
* Finds meters by sensor datapoint
|
|
127
119
|
*
|
|
128
120
|
* @param {string} sensorDP - Sensor datapoint ID
|
|
129
|
-
* @returns {
|
|
121
|
+
* @returns {Array} - Array of {type, meterName} objects
|
|
130
122
|
*/
|
|
131
123
|
findMeterBySensor(sensorDP) {
|
|
132
|
-
return this.meterRegistry
|
|
124
|
+
return this.meterRegistry.findBySensor(sensorDP);
|
|
133
125
|
}
|
|
134
126
|
|
|
135
127
|
/**
|
|
@@ -173,22 +165,31 @@ class MultiMeterManager {
|
|
|
173
165
|
* @returns {Promise<void>}
|
|
174
166
|
*/
|
|
175
167
|
async initializeMeter(type, meterName, config, displayName) {
|
|
176
|
-
const basePath =
|
|
168
|
+
const basePath = `${type}.${meterName}`;
|
|
177
169
|
const label = displayName || meterName;
|
|
178
170
|
|
|
179
171
|
this.adapter.log.info(`Initializing ${type} meter: ${label}`);
|
|
180
172
|
|
|
181
173
|
if (!config.sensorDP) {
|
|
182
174
|
this.adapter.log.warn(`${type} meter "${label}" has no sensor datapoint configured!`);
|
|
183
|
-
|
|
175
|
+
try {
|
|
176
|
+
await this.adapter.setStateAsync(`${basePath}.info.sensorActive`, false, true);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.adapter.log.warn(`Could not set sensorActive state for ${basePath}: ${error.message}`);
|
|
179
|
+
}
|
|
184
180
|
return;
|
|
185
181
|
}
|
|
186
182
|
|
|
187
|
-
// Create state structure
|
|
188
|
-
|
|
183
|
+
// Create state structure with error handling
|
|
184
|
+
try {
|
|
185
|
+
await stateManager.createMeterStructure(this.adapter, type, meterName, config);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.adapter.log.error(`Failed to create state structure for ${basePath}: ${error.message}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
189
190
|
|
|
190
|
-
// Register sensor
|
|
191
|
-
this.meterRegistry
|
|
191
|
+
// Register sensor mapping
|
|
192
|
+
this.meterRegistry.register(config.sensorDP, type, meterName);
|
|
192
193
|
|
|
193
194
|
this.adapter.log.debug(`Using sensor datapoint for ${type}.${meterName}: ${config.sensorDP}`);
|
|
194
195
|
|
|
@@ -212,8 +213,10 @@ class MultiMeterManager {
|
|
|
212
213
|
// Initialize with current sensor value
|
|
213
214
|
try {
|
|
214
215
|
const sensorState = await this.adapter.getForeignStateAsync(config.sensorDP);
|
|
215
|
-
if (sensorState && sensorState.val
|
|
216
|
-
|
|
216
|
+
if (sensorState && sensorState.val != null) {
|
|
217
|
+
// Convert to number (handles strings, German commas, etc.)
|
|
218
|
+
const numValue = calculator.ensureNumber(sensorState.val);
|
|
219
|
+
await this.handleSensorUpdate(type, meterName, config.sensorDP, numValue);
|
|
217
220
|
}
|
|
218
221
|
} catch (error) {
|
|
219
222
|
this.adapter.log.warn(`Could not read initial value from ${config.sensorDP}: ${error.message}`);
|
|
@@ -221,7 +224,7 @@ class MultiMeterManager {
|
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
// Initialize period start timestamps
|
|
224
|
-
const timestampRoles = ['lastDayStart', 'lastMonthStart', 'lastYearStart'];
|
|
227
|
+
const timestampRoles = ['lastDayStart', 'lastWeekStart', 'lastMonthStart', 'lastYearStart'];
|
|
225
228
|
|
|
226
229
|
for (const role of timestampRoles) {
|
|
227
230
|
const statePath = `${basePath}.statistics.${role}`;
|
|
@@ -264,37 +267,9 @@ class MultiMeterManager {
|
|
|
264
267
|
}
|
|
265
268
|
}
|
|
266
269
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (sensorState && typeof sensorState.val === 'number') {
|
|
271
|
-
let currentRaw = sensorState.val;
|
|
272
|
-
|
|
273
|
-
if (config.offset !== 0) {
|
|
274
|
-
currentRaw = currentRaw - config.offset;
|
|
275
|
-
this.adapter.log.debug(
|
|
276
|
-
`Applied offset for ${type}.${meterName}: -${config.offset}, new value: ${currentRaw}`,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
let yearlyConsumption = Math.max(0, currentRaw - config.initialReading);
|
|
280
|
-
|
|
281
|
-
// For gas: convert m³ to kWh
|
|
282
|
-
if (type === 'gas') {
|
|
283
|
-
const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
|
|
284
|
-
const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
|
|
285
|
-
const yearlyVolume = yearlyConsumption;
|
|
286
|
-
yearlyConsumption = calculator.convertGasM3ToKWh(yearlyConsumption, brennwert, zZahl);
|
|
287
|
-
await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, yearlyVolume, true);
|
|
288
|
-
this.adapter.log.info(
|
|
289
|
-
`Init yearly ${type}.${meterName}: ${yearlyConsumption.toFixed(2)} kWh = ${(currentRaw - config.initialReading).toFixed(2)} m³`,
|
|
290
|
-
);
|
|
291
|
-
} else {
|
|
292
|
-
this.adapter.log.info(`Init yearly ${type}.${meterName}: ${yearlyConsumption.toFixed(2)}`);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyConsumption, true);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
270
|
+
// NOTE: Initial yearly consumption is calculated in handleSensorUpdate()
|
|
271
|
+
// which is called immediately after this initialization (line 224)
|
|
272
|
+
// This avoids race conditions where state objects aren't fully created yet
|
|
298
273
|
|
|
299
274
|
// Update current price
|
|
300
275
|
await this.updateCurrentPrice(type, meterName, config);
|
|
@@ -349,8 +324,10 @@ class MultiMeterManager {
|
|
|
349
324
|
return;
|
|
350
325
|
}
|
|
351
326
|
|
|
352
|
-
const basePath =
|
|
353
|
-
this.adapter.log.debug(`
|
|
327
|
+
const basePath = `${type}.${meterName}`;
|
|
328
|
+
this.adapter.log.debug(`[${basePath}] handleSensorUpdate: value=${value}, sensorDP=${sensorDP}`);
|
|
329
|
+
|
|
330
|
+
const now = Date.now();
|
|
354
331
|
|
|
355
332
|
// Get meter config
|
|
356
333
|
const meters = this.getMetersForType(type);
|
|
@@ -361,92 +338,279 @@ class MultiMeterManager {
|
|
|
361
338
|
}
|
|
362
339
|
|
|
363
340
|
const config = meter.config;
|
|
364
|
-
const now = Date.now();
|
|
365
|
-
let consumption = value;
|
|
366
|
-
let consumptionM3 = null;
|
|
367
341
|
|
|
368
|
-
|
|
342
|
+
// Pre-process consumption value (offset, gas conversion)
|
|
343
|
+
const processed = await this._preprocessValue(type, value, config);
|
|
344
|
+
const { consumption, consumptionM3 } = processed;
|
|
369
345
|
|
|
370
|
-
//
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
346
|
+
// 1. Initialization Logic (Per Session)
|
|
347
|
+
if (this.lastSensorValues[sensorDP] === undefined) {
|
|
348
|
+
await this._handleFirstSensorValue(type, meterName, sensorDP, processed, basePath, config, now);
|
|
349
|
+
return;
|
|
374
350
|
}
|
|
375
351
|
|
|
376
|
-
//
|
|
352
|
+
// 2. Update meter reading states
|
|
353
|
+
await this.adapter.setStateAsync(`${basePath}.info.meterReading`, consumption, true);
|
|
377
354
|
if (type === 'gas') {
|
|
378
|
-
|
|
379
|
-
const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
|
|
380
|
-
consumptionM3 = consumption;
|
|
381
|
-
await this.adapter.setStateAsync(`${basePath}.info.meterReadingVolume`, consumption, true);
|
|
382
|
-
consumption = calculator.convertGasM3ToKWh(consumption, brennwert, zZahl);
|
|
383
|
-
consumption = calculator.roundToDecimals(consumption, 2);
|
|
355
|
+
await this.adapter.setStateAsync(`${basePath}.info.meterReadingVolume`, consumptionM3 || 0, true);
|
|
384
356
|
}
|
|
385
357
|
|
|
386
|
-
//
|
|
387
|
-
await this.adapter.setStateAsync(`${basePath}.info.meterReading`, consumption, true);
|
|
388
|
-
|
|
389
|
-
// Calculate deltas
|
|
358
|
+
// 3. Delta Calibration & Spike Protection
|
|
390
359
|
const lastValue = this.lastSensorValues[sensorDP];
|
|
391
360
|
this.lastSensorValues[sensorDP] = consumption;
|
|
392
361
|
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
362
|
+
if (consumption < lastValue) {
|
|
363
|
+
await this._handleMeterReset(type, meterName, lastValue, consumption, config);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const delta = calculator.roundToDecimals(consumption - lastValue, 4);
|
|
368
|
+
if (delta <= 0) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const spikeThreshold = this.adapter.config.sensorSpikeThreshold || DEFAULT_SPIKE_THRESHOLD;
|
|
373
|
+
if (delta > spikeThreshold) {
|
|
374
|
+
await this._handleSuspiciousDelta(
|
|
375
|
+
type,
|
|
376
|
+
meterName,
|
|
377
|
+
delta,
|
|
378
|
+
consumption,
|
|
379
|
+
consumptionM3,
|
|
380
|
+
config,
|
|
381
|
+
basePath,
|
|
382
|
+
now,
|
|
383
|
+
);
|
|
401
384
|
return;
|
|
402
385
|
}
|
|
403
386
|
|
|
404
|
-
const delta = consumption - lastValue;
|
|
405
387
|
this.adapter.log.debug(`${type}.${meterName} delta: ${delta}`);
|
|
406
388
|
|
|
407
|
-
//
|
|
389
|
+
// 4. Update Consumption Values (Daily, Weekly, Monthly)
|
|
390
|
+
const deltaVolume =
|
|
391
|
+
type === 'gas'
|
|
392
|
+
? delta /
|
|
393
|
+
((this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT) *
|
|
394
|
+
(this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL))
|
|
395
|
+
: 0;
|
|
396
|
+
await this._updateTotalConsumptionStates(basePath, type, delta, deltaVolume);
|
|
397
|
+
|
|
398
|
+
// 5. HT/NT Tracking
|
|
399
|
+
await this._updateHTNTConsumptionStates(basePath, type, delta, config);
|
|
400
|
+
|
|
401
|
+
// 6. Yearly & Costs
|
|
402
|
+
const yearlyAmountFinal = await this._updateYearlyConsumption(
|
|
403
|
+
type,
|
|
404
|
+
meterName,
|
|
405
|
+
config,
|
|
406
|
+
consumption,
|
|
407
|
+
consumptionM3,
|
|
408
|
+
delta,
|
|
409
|
+
basePath,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
await this.updateCosts(type, meterName, config, yearlyAmountFinal);
|
|
413
|
+
await this.updateTotalCosts(type);
|
|
414
|
+
|
|
415
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.lastUpdate`, now, true);
|
|
416
|
+
await this.adapter.setStateAsync(`${basePath}.info.lastSync`, now, true);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Preprocesses raw sensor value (offset, gas conversion)
|
|
421
|
+
*
|
|
422
|
+
* @param {string} type - Utility type
|
|
423
|
+
* @param {number} value - Raw sensor value
|
|
424
|
+
* @param {object} config - Meter configuration
|
|
425
|
+
* @returns {Promise<{consumption: number, consumptionM3: number|null}>} Processed values
|
|
426
|
+
*/
|
|
427
|
+
async _preprocessValue(type, value, config) {
|
|
428
|
+
let consumption = value;
|
|
429
|
+
let consumptionM3 = null;
|
|
430
|
+
|
|
431
|
+
if (config.offset !== 0) {
|
|
432
|
+
consumption = consumption - config.offset;
|
|
433
|
+
}
|
|
434
|
+
|
|
408
435
|
if (type === 'gas') {
|
|
409
436
|
const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
|
|
410
437
|
const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
|
|
411
|
-
const deltaVolume = delta / (brennwert * zZahl);
|
|
412
438
|
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
439
|
+
const res = consumptionHelper.calculateGas(consumption, brennwert, zZahl);
|
|
440
|
+
consumptionM3 = res.volume;
|
|
441
|
+
consumption = res.energy;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return { consumption, consumptionM3 };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Handles the very first sensor value in a session (recovery or initial)
|
|
449
|
+
*
|
|
450
|
+
* @param {string} type - Utility type
|
|
451
|
+
* @param {string} meterName - Meter name
|
|
452
|
+
* @param {string} sensorDP - Sensor data point path
|
|
453
|
+
* @param {object} processed - Processed consumption values
|
|
454
|
+
* @param {string} basePath - State base path
|
|
455
|
+
* @param {object} config - Meter configuration
|
|
456
|
+
* @param {number} now - Current timestamp
|
|
457
|
+
*/
|
|
458
|
+
async _handleFirstSensorValue(type, meterName, sensorDP, processed, basePath, config, now) {
|
|
459
|
+
const { consumption, consumptionM3 } = processed;
|
|
460
|
+
const currentState = await this.adapter.getStateAsync(`${basePath}.info.meterReading`);
|
|
461
|
+
const recoveredValue = currentState?.val ?? 0;
|
|
462
|
+
|
|
463
|
+
if (recoveredValue > 0 && Math.abs(consumption - recoveredValue) < 100) {
|
|
464
|
+
this.adapter.log.info(`[${basePath}] Recovered persistent baseline: ${recoveredValue}`);
|
|
465
|
+
this.lastSensorValues[sensorDP] = recoveredValue;
|
|
466
|
+
} else {
|
|
467
|
+
if (recoveredValue > 0) {
|
|
468
|
+
this.adapter.log.warn(
|
|
469
|
+
`[${basePath}] Recovered state (${recoveredValue}) differs significantly from new value (${consumption}).`,
|
|
470
|
+
);
|
|
471
|
+
} else {
|
|
472
|
+
this.adapter.log.info(
|
|
473
|
+
`[${basePath}] No previous reading found. Setting initial baseline to ${consumption}`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
this.lastSensorValues[sensorDP] = consumption;
|
|
477
|
+
await this.adapter.setStateAsync(`${basePath}.info.meterReading`, consumption, true);
|
|
478
|
+
if (type === 'gas') {
|
|
479
|
+
await this.adapter.setStateAsync(`${basePath}.info.meterReadingVolume`, consumptionM3 || 0, true);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (config.initialReading > 0) {
|
|
483
|
+
await this.calculateAbsoluteYearly(type, meterName, config, consumption, consumptionM3 || 0, now);
|
|
484
|
+
await this.updateCosts(type, meterName, config);
|
|
485
|
+
await this.updateTotalCosts(type);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
await this.adapter.setStateAsync(`${basePath}.info.lastSync`, now, true);
|
|
489
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.lastUpdate`, now, true);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Handles meter reset or replacement condition
|
|
494
|
+
*
|
|
495
|
+
* @param {string} type - Utility type
|
|
496
|
+
* @param {string} meterName - Meter name
|
|
497
|
+
* @param {number} lastValue - Previous sensor value
|
|
498
|
+
* @param {number} consumption - Current consumption value
|
|
499
|
+
* @param {object} config - Meter configuration
|
|
500
|
+
*/
|
|
501
|
+
async _handleMeterReset(type, meterName, lastValue, consumption, config) {
|
|
502
|
+
this.adapter.log.warn(
|
|
503
|
+
`${type}.${meterName}: Zählerstand gesunken (${lastValue} -> ${consumption}). Gehe von Zählerwechsel oder Reset aus.`,
|
|
504
|
+
);
|
|
505
|
+
await this.updateCosts(type, meterName, config);
|
|
506
|
+
await this.updateTotalCosts(type);
|
|
507
|
+
}
|
|
416
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Handles suspicious delta (spike detection)
|
|
511
|
+
*
|
|
512
|
+
* @param {string} type - Utility type
|
|
513
|
+
* @param {string} meterName - Meter name
|
|
514
|
+
* @param {number} delta - Calculated delta
|
|
515
|
+
* @param {number} consumption - Current consumption value
|
|
516
|
+
* @param {number|null} consumptionM3 - Current gas volume
|
|
517
|
+
* @param {object} config - Meter configuration
|
|
518
|
+
* @param {string} basePath - State base path
|
|
519
|
+
* @param {number} now - Current timestamp
|
|
520
|
+
*/
|
|
521
|
+
async _handleSuspiciousDelta(type, meterName, delta, consumption, consumptionM3, config, basePath, now) {
|
|
522
|
+
this.adapter.log.warn(`[${basePath}] Discarding suspicious delta of ${delta}. Treating as baseline reset.`);
|
|
523
|
+
if (config.initialReading > 0) {
|
|
524
|
+
await this.calculateAbsoluteYearly(type, meterName, config, consumption, consumptionM3 || 0, now);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Updates all period consumption states
|
|
530
|
+
*
|
|
531
|
+
* @param {string} basePath - State base path
|
|
532
|
+
* @param {string} type - Utility type
|
|
533
|
+
* @param {number} delta - Consumption delta
|
|
534
|
+
* @param {number} deltaVolume - Gas volume delta
|
|
535
|
+
*/
|
|
536
|
+
async _updateTotalConsumptionStates(basePath, type, delta, deltaVolume) {
|
|
537
|
+
const periods = ['daily', 'weekly', 'monthly'];
|
|
538
|
+
for (const period of periods) {
|
|
539
|
+
const state = await this.adapter.getStateAsync(`${basePath}.consumption.${period}`);
|
|
417
540
|
await this.adapter.setStateAsync(
|
|
418
|
-
`${basePath}.consumption
|
|
419
|
-
calculator.roundToDecimals((
|
|
541
|
+
`${basePath}.consumption.${period}`,
|
|
542
|
+
calculator.roundToDecimals((state?.val || 0) + delta, 2),
|
|
420
543
|
true,
|
|
421
544
|
);
|
|
545
|
+
|
|
546
|
+
if (type === 'gas' && deltaVolume > 0) {
|
|
547
|
+
const volState = await this.adapter.getStateAsync(`${basePath}.consumption.${period}Volume`);
|
|
548
|
+
await this.adapter.setStateAsync(
|
|
549
|
+
`${basePath}.consumption.${period}Volume`,
|
|
550
|
+
calculator.roundToDecimals((volState?.val || 0) + deltaVolume, 2),
|
|
551
|
+
true,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Updates HT/NT specific consumption states
|
|
559
|
+
*
|
|
560
|
+
* @param {string} basePath - State base path
|
|
561
|
+
* @param {string} type - Utility type
|
|
562
|
+
* @param {number} delta - Consumption delta
|
|
563
|
+
* @param {object} config - Meter configuration
|
|
564
|
+
*/
|
|
565
|
+
async _updateHTNTConsumptionStates(basePath, type, delta, config) {
|
|
566
|
+
if (!config.htNtEnabled) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const configType = this.getConfigType(type);
|
|
571
|
+
const suffix = consumptionHelper.getHTNTSuffix(this.adapter.config, configType);
|
|
572
|
+
if (!suffix) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const periods = ['daily', 'weekly', 'monthly'];
|
|
577
|
+
for (const period of periods) {
|
|
578
|
+
const state = await this.adapter.getStateAsync(`${basePath}.consumption.${period}${suffix}`);
|
|
422
579
|
await this.adapter.setStateAsync(
|
|
423
|
-
`${basePath}.consumption
|
|
424
|
-
calculator.roundToDecimals((
|
|
580
|
+
`${basePath}.consumption.${period}${suffix}`,
|
|
581
|
+
calculator.roundToDecimals((state?.val || 0) + delta, 2),
|
|
425
582
|
true,
|
|
426
583
|
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (type === 'gas') {
|
|
587
|
+
const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
|
|
588
|
+
const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
|
|
589
|
+
const deltaVolume = delta / (brennwert * zZahl);
|
|
590
|
+
|
|
591
|
+
const state = await this.adapter.getStateAsync(`${basePath}.consumption.weeklyVolume${suffix}`);
|
|
427
592
|
await this.adapter.setStateAsync(
|
|
428
|
-
`${basePath}.consumption.
|
|
429
|
-
calculator.roundToDecimals((
|
|
593
|
+
`${basePath}.consumption.weeklyVolume${suffix}`,
|
|
594
|
+
calculator.roundToDecimals((state?.val || 0) + deltaVolume, 2),
|
|
430
595
|
true,
|
|
431
596
|
);
|
|
432
597
|
}
|
|
598
|
+
}
|
|
433
599
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
// Yearly consumption
|
|
600
|
+
/**
|
|
601
|
+
* Updates yearly consumption
|
|
602
|
+
*
|
|
603
|
+
* @param {string} type - Utility type
|
|
604
|
+
* @param {string} meterName - Meter name
|
|
605
|
+
* @param {object} config - Meter configuration
|
|
606
|
+
* @param {number} consumption - Current consumption value
|
|
607
|
+
* @param {number|null} consumptionM3 - Current gas volume
|
|
608
|
+
* @param {number} delta - Consumption delta
|
|
609
|
+
* @param {string} basePath - State base path
|
|
610
|
+
* @returns {Promise<number>} final yearly amount
|
|
611
|
+
*/
|
|
612
|
+
async _updateYearlyConsumption(type, meterName, config, consumption, consumptionM3, delta, basePath) {
|
|
613
|
+
let yearlyAmountFinal;
|
|
450
614
|
if (config.initialReading > 0) {
|
|
451
615
|
let yearlyAmount;
|
|
452
616
|
if (type === 'gas') {
|
|
@@ -462,21 +626,55 @@ class MultiMeterManager {
|
|
|
462
626
|
} else {
|
|
463
627
|
yearlyAmount = Math.max(0, consumption - config.initialReading);
|
|
464
628
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
calculator.roundToDecimals(yearlyAmount, 2),
|
|
468
|
-
true,
|
|
469
|
-
);
|
|
629
|
+
yearlyAmountFinal = calculator.roundToDecimals(yearlyAmount, 2);
|
|
630
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyAmountFinal, true);
|
|
470
631
|
} else {
|
|
471
632
|
const yState = await this.adapter.getStateAsync(`${basePath}.consumption.yearly`);
|
|
633
|
+
yearlyAmountFinal = calculator.roundToDecimals((yState?.val || 0) + delta, 2);
|
|
634
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyAmountFinal, true);
|
|
635
|
+
}
|
|
636
|
+
return yearlyAmountFinal;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Helper to calculate absolute yearly consumption (usually on start/reset)
|
|
641
|
+
*
|
|
642
|
+
* @param {string} type - Utility type
|
|
643
|
+
* @param {string} meterName - Meter name
|
|
644
|
+
* @param {object} config - Meter configuration
|
|
645
|
+
* @param {number} consumption - Current consumption value
|
|
646
|
+
* @param {number} consumptionM3 - Current consumption in m³ (for gas)
|
|
647
|
+
* @param {number} now - Current timestamp
|
|
648
|
+
*/
|
|
649
|
+
async calculateAbsoluteYearly(type, meterName, config, consumption, consumptionM3, now) {
|
|
650
|
+
const basePath = `${type}.${meterName}`;
|
|
651
|
+
let yearlyAmountFinal;
|
|
652
|
+
|
|
653
|
+
let yearlyAmount;
|
|
654
|
+
if (type === 'gas') {
|
|
655
|
+
const yearlyM3 = Math.max(0, (consumptionM3 || 0) - config.initialReading);
|
|
656
|
+
this.adapter.log.debug(
|
|
657
|
+
`[${basePath}] Yearly absolute (Gas): consumptionM3=${consumptionM3}, initialReading=${config.initialReading}, resultM3=${yearlyM3}`,
|
|
658
|
+
);
|
|
472
659
|
await this.adapter.setStateAsync(
|
|
473
|
-
`${basePath}.consumption.
|
|
474
|
-
calculator.roundToDecimals(
|
|
660
|
+
`${basePath}.consumption.yearlyVolume`,
|
|
661
|
+
calculator.roundToDecimals(yearlyM3, 2),
|
|
475
662
|
true,
|
|
476
663
|
);
|
|
664
|
+
const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
|
|
665
|
+
const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
|
|
666
|
+
yearlyAmount = calculator.convertGasM3ToKWh(yearlyM3, brennwert, zZahl);
|
|
667
|
+
} else {
|
|
668
|
+
yearlyAmount = Math.max(0, consumption - config.initialReading);
|
|
669
|
+
this.adapter.log.debug(
|
|
670
|
+
`[${basePath}] Yearly absolute: consumption=${consumption}, initialReading=${config.initialReading}, result=${yearlyAmount}`,
|
|
671
|
+
);
|
|
477
672
|
}
|
|
673
|
+
yearlyAmountFinal = calculator.roundToDecimals(yearlyAmount, 2);
|
|
674
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyAmountFinal, true);
|
|
478
675
|
|
|
479
|
-
|
|
676
|
+
// Pass direct calculated values to updateCosts to avoid race condition with DB
|
|
677
|
+
await this.updateCosts(type, meterName, config, yearlyAmountFinal);
|
|
480
678
|
await this.updateTotalCosts(type);
|
|
481
679
|
|
|
482
680
|
await this.adapter.setStateAsync(`${basePath}.consumption.lastUpdate`, now, true);
|
|
@@ -491,14 +689,14 @@ class MultiMeterManager {
|
|
|
491
689
|
* @param {object} config - Meter configuration
|
|
492
690
|
*/
|
|
493
691
|
async updateCurrentPrice(type, meterName, config) {
|
|
494
|
-
const basePath =
|
|
692
|
+
const basePath = `${type}.${meterName}`;
|
|
495
693
|
const configType = this.getConfigType(type);
|
|
496
694
|
|
|
497
695
|
let tariffName = 'Standard';
|
|
498
696
|
let activePrice = config.preis || 0;
|
|
499
697
|
|
|
500
|
-
// Only
|
|
501
|
-
if (
|
|
698
|
+
// Only meters with HT/NT enabled support it
|
|
699
|
+
if (config.htNtEnabled) {
|
|
502
700
|
const isHT = calculator.isHTTime(this.adapter.config, configType);
|
|
503
701
|
if (isHT) {
|
|
504
702
|
activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
|
|
@@ -516,6 +714,7 @@ class MultiMeterManager {
|
|
|
516
714
|
true,
|
|
517
715
|
);
|
|
518
716
|
await this.adapter.setStateAsync(`${basePath}.info.currentTariff`, tariffName, true);
|
|
717
|
+
await this.adapter.setStateAsync(`${basePath}.info.lastSync`, Date.now(), true);
|
|
519
718
|
}
|
|
520
719
|
|
|
521
720
|
/**
|
|
@@ -524,14 +723,21 @@ class MultiMeterManager {
|
|
|
524
723
|
* @param {string} type - Utility type
|
|
525
724
|
* @param {string} meterName - Meter name
|
|
526
725
|
* @param {object} config - Meter configuration
|
|
726
|
+
* @param {number} [forcedYearly] - Optional already calculated yearly consumption (avoids DB race)
|
|
527
727
|
*/
|
|
528
|
-
async updateCosts(type, meterName, config) {
|
|
529
|
-
const basePath =
|
|
728
|
+
async updateCosts(type, meterName, config, forcedYearly) {
|
|
729
|
+
const basePath = `${type}.${meterName}`;
|
|
530
730
|
|
|
531
731
|
// Get consumption values
|
|
532
732
|
const daily = (await this.adapter.getStateAsync(`${basePath}.consumption.daily`))?.val || 0;
|
|
533
733
|
const monthly = (await this.adapter.getStateAsync(`${basePath}.consumption.monthly`))?.val || 0;
|
|
534
|
-
|
|
734
|
+
|
|
735
|
+
let yearly;
|
|
736
|
+
if (forcedYearly !== undefined && forcedYearly !== null) {
|
|
737
|
+
yearly = forcedYearly;
|
|
738
|
+
} else {
|
|
739
|
+
yearly = (await this.adapter.getStateAsync(`${basePath}.consumption.yearly`))?.val || 0;
|
|
740
|
+
}
|
|
535
741
|
|
|
536
742
|
// Get price
|
|
537
743
|
const price = config.preis || 0;
|
|
@@ -542,67 +748,66 @@ class MultiMeterManager {
|
|
|
542
748
|
|
|
543
749
|
// Calculate consumption costs
|
|
544
750
|
const dailyCost = daily * price;
|
|
751
|
+
const weeklyState = await this.adapter.getStateAsync(`${basePath}.consumption.weekly`);
|
|
752
|
+
const weeklyCost = (weeklyState?.val || 0) * price;
|
|
545
753
|
const monthlyCost = monthly * price;
|
|
546
754
|
const yearlyCost = yearly * price;
|
|
547
755
|
|
|
548
756
|
await this.adapter.setStateAsync(`${basePath}.costs.daily`, calculator.roundToDecimals(dailyCost, 2), true);
|
|
757
|
+
await this.adapter.setStateAsync(`${basePath}.costs.weekly`, calculator.roundToDecimals(weeklyCost, 2), true);
|
|
549
758
|
await this.adapter.setStateAsync(`${basePath}.costs.monthly`, calculator.roundToDecimals(monthlyCost, 2), true);
|
|
550
759
|
await this.adapter.setStateAsync(`${basePath}.costs.yearly`, calculator.roundToDecimals(yearlyCost, 2), true);
|
|
551
760
|
|
|
552
761
|
// Calculate accumulated costs based on contract start
|
|
553
|
-
const
|
|
554
|
-
let monthsSinceYearStart = 1;
|
|
555
|
-
let basicChargeAccumulated = 0;
|
|
556
|
-
|
|
557
|
-
if (yearStartState && yearStartState.val) {
|
|
558
|
-
// yearStartState.val is a timestamp (number)
|
|
559
|
-
const yearStartDate = new Date(yearStartState.val);
|
|
560
|
-
if (!isNaN(yearStartDate.getTime())) {
|
|
561
|
-
const now = new Date();
|
|
562
|
-
|
|
563
|
-
// Calculate months since contract start (started months, including current)
|
|
564
|
-
monthsSinceYearStart = calculator.getMonthsDifference(yearStartDate, now) + 1;
|
|
565
|
-
|
|
566
|
-
// Calculate accumulated basic charge (monthly fee × months)
|
|
567
|
-
basicChargeAccumulated = (config.grundgebuehr || 0) * monthsSinceYearStart;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Jahresgebühr ist ein FIXER Wert pro Jahr (z.B. 60€)
|
|
572
|
-
// NICHT pro-rata nach Monaten/Tagen berechnen!
|
|
573
|
-
const annualFeeAccumulated = config.jahresgebuehr || 0;
|
|
762
|
+
const monthsSinceYearStart = await this._calculateMonthsSinceYearStart(basePath);
|
|
574
763
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
true,
|
|
764
|
+
const charges = billingHelper.calculateAccumulatedCharges(
|
|
765
|
+
config.grundgebuehr,
|
|
766
|
+
config.jahresgebuehr,
|
|
767
|
+
monthsSinceYearStart,
|
|
580
768
|
);
|
|
769
|
+
const basicChargeAccumulated = charges.basicCharge;
|
|
770
|
+
const annualFeeAccumulated = charges.annualFee;
|
|
581
771
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
true,
|
|
586
|
-
);
|
|
772
|
+
// Update basicCharge and annualFee states
|
|
773
|
+
await this.adapter.setStateAsync(`${basePath}.costs.basicCharge`, basicChargeAccumulated, true);
|
|
774
|
+
await this.adapter.setStateAsync(`${basePath}.costs.annualFee`, annualFeeAccumulated, true);
|
|
587
775
|
|
|
588
776
|
// Calculate total yearly costs and balance
|
|
589
|
-
const totalYearlyCost = yearlyCost +
|
|
590
|
-
|
|
777
|
+
const totalYearlyCost = Math.max(0, yearlyCost + charges.total);
|
|
591
778
|
await this.adapter.setStateAsync(
|
|
592
779
|
`${basePath}.costs.totalYearly`,
|
|
593
780
|
calculator.roundToDecimals(totalYearlyCost, 2),
|
|
594
781
|
true,
|
|
595
782
|
);
|
|
596
783
|
|
|
597
|
-
const
|
|
598
|
-
const balance = totalYearlyCost - paidTotal;
|
|
784
|
+
const balanceRes = billingHelper.calculateBalance(config.abschlag, monthsSinceYearStart, totalYearlyCost);
|
|
599
785
|
|
|
600
786
|
this.adapter.log.debug(
|
|
601
|
-
`[${basePath}]
|
|
787
|
+
`[${basePath}] Cost calculation (MultiMeter): daily=${daily}, monthly=${monthly}, yearly=${yearly}, price=${price}, totalYearly=${totalYearlyCost}, paidTotal=${balanceRes.paid}, balance=${balanceRes.balance}`,
|
|
602
788
|
);
|
|
603
789
|
|
|
604
|
-
await this.adapter.setStateAsync(`${basePath}.costs.paidTotal`,
|
|
605
|
-
await this.adapter.setStateAsync(`${basePath}.costs.balance`,
|
|
790
|
+
await this.adapter.setStateAsync(`${basePath}.costs.paidTotal`, balanceRes.paid, true);
|
|
791
|
+
await this.adapter.setStateAsync(`${basePath}.costs.balance`, balanceRes.balance, true);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Calculates months since year start for a meter
|
|
796
|
+
*
|
|
797
|
+
* @param {string} basePath - State base path
|
|
798
|
+
* @returns {Promise<number>} Months since start (at least 1)
|
|
799
|
+
*/
|
|
800
|
+
async _calculateMonthsSinceYearStart(basePath) {
|
|
801
|
+
const yearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
|
|
802
|
+
let monthsSinceYearStart = 1;
|
|
803
|
+
|
|
804
|
+
if (yearStartState && yearStartState.val) {
|
|
805
|
+
const yearStartDate = new Date(yearStartState.val);
|
|
806
|
+
if (!isNaN(yearStartDate.getTime())) {
|
|
807
|
+
monthsSinceYearStart = calculator.getMonthsDifference(yearStartDate, new Date()) + 1;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return Math.max(1, monthsSinceYearStart);
|
|
606
811
|
}
|
|
607
812
|
|
|
608
813
|
/**
|
|
@@ -618,21 +823,35 @@ class MultiMeterManager {
|
|
|
618
823
|
return;
|
|
619
824
|
}
|
|
620
825
|
|
|
826
|
+
// Check if totals structure exists before trying to update
|
|
827
|
+
const totalsExists = await this.adapter.getObjectAsync(`${type}.totals`);
|
|
828
|
+
if (!totalsExists) {
|
|
829
|
+
// Totals structure not yet created, skip update
|
|
830
|
+
// This happens during initialization when handleSensorUpdate is called
|
|
831
|
+
// before createTotalsStructure has run
|
|
832
|
+
this.adapter.log.debug(`Skipping total costs update for ${type} - totals structure not yet created`);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
621
836
|
let totalDaily = 0;
|
|
837
|
+
let totalWeekly = 0;
|
|
622
838
|
let totalMonthly = 0;
|
|
623
839
|
let totalYearly = 0;
|
|
624
840
|
let totalCostsDaily = 0;
|
|
841
|
+
let totalCostsWeekly = 0;
|
|
625
842
|
let totalCostsMonthly = 0;
|
|
626
843
|
let totalCostsYearly = 0;
|
|
627
844
|
|
|
628
845
|
for (const meter of meters) {
|
|
629
|
-
const basePath =
|
|
846
|
+
const basePath = `${type}.${meter.name}`;
|
|
630
847
|
|
|
631
848
|
totalDaily += (await this.adapter.getStateAsync(`${basePath}.consumption.daily`))?.val || 0;
|
|
849
|
+
totalWeekly += (await this.adapter.getStateAsync(`${basePath}.consumption.weekly`))?.val || 0;
|
|
632
850
|
totalMonthly += (await this.adapter.getStateAsync(`${basePath}.consumption.monthly`))?.val || 0;
|
|
633
851
|
totalYearly += (await this.adapter.getStateAsync(`${basePath}.consumption.yearly`))?.val || 0;
|
|
634
852
|
|
|
635
853
|
totalCostsDaily += (await this.adapter.getStateAsync(`${basePath}.costs.daily`))?.val || 0;
|
|
854
|
+
totalCostsWeekly += (await this.adapter.getStateAsync(`${basePath}.costs.weekly`))?.val || 0;
|
|
636
855
|
totalCostsMonthly += (await this.adapter.getStateAsync(`${basePath}.costs.monthly`))?.val || 0;
|
|
637
856
|
totalCostsYearly += (await this.adapter.getStateAsync(`${basePath}.costs.totalYearly`))?.val || 0;
|
|
638
857
|
}
|
|
@@ -642,6 +861,11 @@ class MultiMeterManager {
|
|
|
642
861
|
calculator.roundToDecimals(totalDaily, 2),
|
|
643
862
|
true,
|
|
644
863
|
);
|
|
864
|
+
await this.adapter.setStateAsync(
|
|
865
|
+
`${type}.totals.consumption.weekly`,
|
|
866
|
+
calculator.roundToDecimals(totalWeekly, 2),
|
|
867
|
+
true,
|
|
868
|
+
);
|
|
645
869
|
await this.adapter.setStateAsync(
|
|
646
870
|
`${type}.totals.consumption.monthly`,
|
|
647
871
|
calculator.roundToDecimals(totalMonthly, 2),
|
|
@@ -658,6 +882,11 @@ class MultiMeterManager {
|
|
|
658
882
|
calculator.roundToDecimals(totalCostsDaily, 2),
|
|
659
883
|
true,
|
|
660
884
|
);
|
|
885
|
+
await this.adapter.setStateAsync(
|
|
886
|
+
`${type}.totals.costs.weekly`,
|
|
887
|
+
calculator.roundToDecimals(totalCostsWeekly, 2),
|
|
888
|
+
true,
|
|
889
|
+
);
|
|
661
890
|
await this.adapter.setStateAsync(
|
|
662
891
|
`${type}.totals.costs.monthly`,
|
|
663
892
|
calculator.roundToDecimals(totalCostsMonthly, 2),
|