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/calculator.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Calculator module for nebenkosten-monitor
|
|
3
|
-
* Provides utility functions for gas conversion, cost calculation, and consumption aggregation
|
|
4
|
-
*/
|
|
1
|
+
const helpers = require('./utils/helpers');
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* Converts gas volume from m³ to kWh
|
|
@@ -13,13 +10,17 @@
|
|
|
13
10
|
* @returns {number} Energy in kWh
|
|
14
11
|
*/
|
|
15
12
|
function convertGasM3ToKWh(m3, brennwert = 11.5, zZahl = 0.95) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
// Hier number zu string
|
|
14
|
+
const cleanM3 = helpers.ensureNumber(m3);
|
|
15
|
+
const cleanBrennwert = helpers.ensureNumber(brennwert);
|
|
16
|
+
const cleanZZahl = helpers.ensureNumber(zZahl);
|
|
17
|
+
|
|
18
|
+
// Validierung der Logik (jetzt mit den konvertierten Zahlen)
|
|
19
|
+
if (cleanM3 < 0 || cleanBrennwert <= 0 || cleanZZahl <= 0 || cleanZZahl > 1) {
|
|
20
|
+
throw new RangeError('Ungültige Parameterwerte für die Gas-Umrechnung');
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
return cleanM3 * cleanBrennwert * cleanZZahl;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -51,69 +52,6 @@ function calculateCost(consumption, price) {
|
|
|
51
52
|
return consumption * (price || 0);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
/**
|
|
55
|
-
* Ensures a value is a number, handling German decimal commas if provided as string.
|
|
56
|
-
*
|
|
57
|
-
* @param {any} value - Value to convert
|
|
58
|
-
* @returns {number}
|
|
59
|
-
*/
|
|
60
|
-
function ensureNumber(value) {
|
|
61
|
-
if (value === undefined || value === null || value === '') {
|
|
62
|
-
return 0;
|
|
63
|
-
}
|
|
64
|
-
if (typeof value === 'string') {
|
|
65
|
-
const normalized = value.replace(',', '.');
|
|
66
|
-
const parsed = parseFloat(normalized);
|
|
67
|
-
return isNaN(parsed) ? 0 : parsed;
|
|
68
|
-
}
|
|
69
|
-
const num = Number(value);
|
|
70
|
-
return isNaN(num) ? 0 : num;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Rounds a number to specified decimal places
|
|
75
|
-
*
|
|
76
|
-
* @param {number|string} value - Value to round
|
|
77
|
-
* @param {number} decimals - Number of decimal places (default: 2)
|
|
78
|
-
* @returns {number} Rounded value
|
|
79
|
-
*/
|
|
80
|
-
function roundToDecimals(value, decimals = 2) {
|
|
81
|
-
const numValue = ensureNumber(value);
|
|
82
|
-
const factor = Math.pow(10, decimals);
|
|
83
|
-
return Math.round(numValue * factor) / factor;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Parses a German date string (DD.MM.YYYY) into a Date object
|
|
88
|
-
*
|
|
89
|
-
* @param {string} dateStr - Date string in format DD.MM.YYYY
|
|
90
|
-
* @returns {Date|null} Date object or null if invalid
|
|
91
|
-
*/
|
|
92
|
-
function parseGermanDate(dateStr) {
|
|
93
|
-
if (!dateStr || typeof dateStr !== 'string') {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
const parts = dateStr.trim().split('.');
|
|
97
|
-
if (parts.length !== 3) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
let day = parseInt(parts[0], 10);
|
|
101
|
-
let month = parseInt(parts[1], 10) - 1; // Month is 0-indexed
|
|
102
|
-
let year = parseInt(parts[2], 10);
|
|
103
|
-
|
|
104
|
-
// Handle 2-digit years (e.g. 25 -> 2025)
|
|
105
|
-
if (year < 100) {
|
|
106
|
-
year += 2000;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (isNaN(day) || isNaN(month) || isNaN(year)) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Create date at noon to avoid timezone shift issues (especially with ISO export)
|
|
114
|
-
return new Date(year, month, day, 12, 0, 0);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
55
|
/**
|
|
118
56
|
* Checks if the current time is within the High Tariff (HT) period
|
|
119
57
|
*
|
|
@@ -180,75 +118,18 @@ const DEFAULTS = {
|
|
|
180
118
|
MIN_CONSUMPTION: 0,
|
|
181
119
|
};
|
|
182
120
|
|
|
183
|
-
/**
|
|
184
|
-
* Formats a Date object to YYYY-MM-DD HH:mm:ss string
|
|
185
|
-
*
|
|
186
|
-
* @param {Date} date - Date object
|
|
187
|
-
* @returns {string|null} Formatted date string or null
|
|
188
|
-
*/
|
|
189
|
-
function formatDateString(date) {
|
|
190
|
-
if (!date) {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
if (!(date instanceof Date)) {
|
|
194
|
-
// If it's already a string, try to parse and re-format, or just return if it handles
|
|
195
|
-
// But the error said "calculator.formatDateString is not a function", so we need it here.
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const pad = num => num.toString().padStart(2, '0');
|
|
200
|
-
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Parses a date string (ISO or standard format)
|
|
205
|
-
*
|
|
206
|
-
* @param {string} dateStr - Date string
|
|
207
|
-
* @returns {Date|null} Date object or null
|
|
208
|
-
*/
|
|
209
|
-
function parseDateString(dateStr) {
|
|
210
|
-
if (!dateStr) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
const date = new Date(dateStr);
|
|
214
|
-
if (isNaN(date.getTime())) {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
return date;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Checks if a year is a leap year
|
|
222
|
-
*
|
|
223
|
-
* @param {number} year - Year to check
|
|
224
|
-
* @returns {boolean} True if leap year
|
|
225
|
-
*/
|
|
226
|
-
function isLeapYear(year) {
|
|
227
|
-
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Calculates the difference in months between two dates
|
|
232
|
-
*
|
|
233
|
-
* @param {Date} startDate - Start date
|
|
234
|
-
* @param {Date} endDate - End date
|
|
235
|
-
* @returns {number} Difference in months
|
|
236
|
-
*/
|
|
237
|
-
function getMonthsDifference(startDate, endDate) {
|
|
238
|
-
return (endDate.getFullYear() - startDate.getFullYear()) * 12 + (endDate.getMonth() - startDate.getMonth());
|
|
239
|
-
}
|
|
240
|
-
|
|
241
121
|
module.exports = {
|
|
242
122
|
convertGasM3ToKWh,
|
|
243
123
|
getCurrentPrice,
|
|
244
124
|
calculateCost,
|
|
245
|
-
ensureNumber,
|
|
246
|
-
roundToDecimals,
|
|
247
|
-
parseGermanDate,
|
|
248
125
|
isHTTime,
|
|
249
|
-
formatDateString,
|
|
250
|
-
parseDateString,
|
|
251
|
-
isLeapYear,
|
|
252
|
-
getMonthsDifference,
|
|
253
126
|
DEFAULTS,
|
|
127
|
+
// Re-export helpers for backward compatibility
|
|
128
|
+
ensureNumber: helpers.ensureNumber,
|
|
129
|
+
roundToDecimals: helpers.roundToDecimals,
|
|
130
|
+
parseGermanDate: helpers.parseGermanDate,
|
|
131
|
+
formatDateString: helpers.formatDateString,
|
|
132
|
+
parseDateString: s => helpers.parseGermanDate(s),
|
|
133
|
+
isLeapYear: helpers.isLeapYear,
|
|
134
|
+
getMonthsDifference: helpers.getMonthsDifference,
|
|
254
135
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const calculator = require('./calculator');
|
|
4
|
+
const { getConfigType } = require('./utils/typeMapper');
|
|
4
5
|
const stateManager = require('./stateManager');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* ConsumptionManager handles
|
|
8
|
-
*
|
|
8
|
+
* ConsumptionManager handles utility initialization and price updates.
|
|
9
|
+
* NOTE: Sensor handling has been moved to MultiMeterManager since v1.4.6.
|
|
9
10
|
*/
|
|
10
11
|
class ConsumptionManager {
|
|
11
12
|
/**
|
|
@@ -13,22 +14,6 @@ class ConsumptionManager {
|
|
|
13
14
|
*/
|
|
14
15
|
constructor(adapter) {
|
|
15
16
|
this.adapter = adapter;
|
|
16
|
-
this.lastSensorValues = {};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Maps internal utility type to config/state name
|
|
21
|
-
*
|
|
22
|
-
* @param {string} type - gas, water, or electricity
|
|
23
|
-
* @returns {string} - gas, wasser, or strom
|
|
24
|
-
*/
|
|
25
|
-
getConfigType(type) {
|
|
26
|
-
const mapping = {
|
|
27
|
-
electricity: 'strom',
|
|
28
|
-
water: 'wasser',
|
|
29
|
-
gas: 'gas',
|
|
30
|
-
};
|
|
31
|
-
return mapping[type] || type;
|
|
32
17
|
}
|
|
33
18
|
|
|
34
19
|
/**
|
|
@@ -47,16 +32,16 @@ class ConsumptionManager {
|
|
|
47
32
|
|
|
48
33
|
this.adapter.log.info(`Initializing ${type} monitoring...`);
|
|
49
34
|
|
|
50
|
-
//
|
|
51
|
-
|
|
35
|
+
// State structure is now created by MultiMeterManager per meter (v1.4.6)
|
|
36
|
+
// Old createUtilityStateStructure removed - states are created under type.meterName.*
|
|
52
37
|
|
|
53
|
-
const configType =
|
|
38
|
+
const configType = getConfigType(type);
|
|
54
39
|
const sensorDPKey = `${configType}SensorDP`;
|
|
55
40
|
const sensorDP = this.adapter.config[sensorDPKey];
|
|
56
41
|
|
|
57
42
|
if (!sensorDP) {
|
|
58
43
|
this.adapter.log.warn(`${type} is active but no sensor datapoint configured!`);
|
|
59
|
-
|
|
44
|
+
// Note: sensorActive state is now created per meter by MultiMeterManager
|
|
60
45
|
return;
|
|
61
46
|
}
|
|
62
47
|
|
|
@@ -69,338 +54,70 @@ class ConsumptionManager {
|
|
|
69
54
|
this.adapter.log.info(`${type}: Managed with contract start: ${contractStartDateStr}`);
|
|
70
55
|
}
|
|
71
56
|
|
|
72
|
-
//
|
|
73
|
-
this.adapter.
|
|
74
|
-
await this.adapter.setStateAsync(`${type}.info.sensorActive`, true, true);
|
|
75
|
-
this.adapter.log.debug(`Subscribed to ${type} sensor: ${sensorDP}`);
|
|
57
|
+
// Sensor subscription is now handled by MultiMeterManager per meter
|
|
58
|
+
this.adapter.log.debug(`${type} sensor will be subscribed by MultiMeterManager: ${sensorDP}`);
|
|
76
59
|
|
|
77
60
|
// Initialize all meters (main + additional) via MultiMeterManager
|
|
61
|
+
// This handles everything now: state creation, sensor subscription, costs calculation
|
|
78
62
|
if (this.adapter.multiMeterManager) {
|
|
79
63
|
await this.adapter.multiMeterManager.initializeType(type);
|
|
80
64
|
}
|
|
81
65
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
const sensorState = await this.adapter.getForeignStateAsync(sensorDP);
|
|
92
|
-
if (sensorState && sensorState.val !== null && typeof sensorState.val === 'number') {
|
|
93
|
-
await this.handleSensorUpdate(type, sensorDP, sensorState.val);
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
this.adapter.log.warn(`Could not read initial value from ${sensorDP}: ${error.message}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Initialize period start timestamps if not set
|
|
100
|
-
const now = Date.now();
|
|
101
|
-
const dayStart = await this.adapter.getStateAsync(`${type}.statistics.lastDayStart`);
|
|
102
|
-
if (!dayStart || !dayStart.val) {
|
|
103
|
-
await this.adapter.setStateAsync(`${type}.statistics.lastDayStart`, now, true);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const monthStart = await this.adapter.getStateAsync(`${type}.statistics.lastMonthStart`);
|
|
107
|
-
if (!monthStart || !monthStart.val) {
|
|
108
|
-
await this.adapter.setStateAsync(`${type}.statistics.lastMonthStart`, now, true);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const yearStart = await this.adapter.getStateAsync(`${type}.statistics.lastYearStart`);
|
|
112
|
-
if (!yearStart || !yearStart.val) {
|
|
113
|
-
// Determine year start based on contract date or January 1st
|
|
114
|
-
const contractStartKey = `${configType}ContractStart`;
|
|
115
|
-
const contractStartDateStr = this.adapter.config[contractStartKey];
|
|
66
|
+
// Note: All initialization moved to MultiMeterManager in v1.4.6:
|
|
67
|
+
// - State creation (per meter)
|
|
68
|
+
// - Sensor value restoration (per meter)
|
|
69
|
+
// - Period start timestamps (per meter)
|
|
70
|
+
// - Initial yearly consumption calculation (per meter)
|
|
71
|
+
// - Current price updates (per meter)
|
|
72
|
+
// - Cost calculations (per meter)
|
|
73
|
+
// - Billing countdown (per meter)
|
|
74
|
+
// Old type-level states (gas.info.*, gas.statistics.*) are no longer used
|
|
116
75
|
|
|
117
|
-
|
|
118
|
-
if (contractStartDateStr) {
|
|
119
|
-
const contractStart = calculator.parseGermanDate(contractStartDateStr);
|
|
120
|
-
if (contractStart && !isNaN(contractStart.getTime())) {
|
|
121
|
-
// Calculate last anniversary
|
|
122
|
-
const nowDate = new Date(now);
|
|
123
|
-
const currentYear = nowDate.getFullYear();
|
|
124
|
-
yearStartDate = new Date(currentYear, contractStart.getMonth(), contractStart.getDate(), 12, 0, 0);
|
|
125
|
-
|
|
126
|
-
// If anniversary is in the future this year, take last year
|
|
127
|
-
if (yearStartDate > nowDate) {
|
|
128
|
-
yearStartDate.setFullYear(currentYear - 1);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!yearStartDate) {
|
|
134
|
-
// Fallback: January 1st of current year
|
|
135
|
-
const nowDate = new Date(now);
|
|
136
|
-
yearStartDate = new Date(nowDate.getFullYear(), 0, 1, 12, 0, 0);
|
|
137
|
-
this.adapter.log.info(
|
|
138
|
-
`${type}: No contract start found. Setting initial year start to January 1st: ${yearStartDate.toLocaleDateString('de-DE')}`,
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
await this.adapter.setStateAsync(`${type}.statistics.lastYearStart`, yearStartDate.getTime(), true);
|
|
143
|
-
}
|
|
144
|
-
// Update current price
|
|
145
|
-
await this.updateCurrentPrice(type);
|
|
146
|
-
|
|
147
|
-
// Initial cost calculation (wichtig! Sonst bleiben Kosten bei 0)
|
|
148
|
-
if (typeof this.adapter.updateCosts === 'function') {
|
|
149
|
-
await this.adapter.updateCosts(type);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Initialize yearly consumption from initial reading if set
|
|
153
|
-
const initialReadingKey = `${configType}InitialReading`;
|
|
154
|
-
const initialReading = this.adapter.config[initialReadingKey] || 0;
|
|
155
|
-
|
|
156
|
-
if (initialReading > 0) {
|
|
157
|
-
const sensorState = await this.adapter.getForeignStateAsync(sensorDP);
|
|
158
|
-
if (sensorState && typeof sensorState.val === 'number') {
|
|
159
|
-
let currentRaw = sensorState.val;
|
|
160
|
-
|
|
161
|
-
// Apply offset if configured (in original unit)
|
|
162
|
-
const offsetKey = `${configType}Offset`;
|
|
163
|
-
const offset = this.adapter.config[offsetKey] || 0;
|
|
164
|
-
if (offset !== 0) {
|
|
165
|
-
currentRaw = currentRaw - offset;
|
|
166
|
-
this.adapter.log.debug(`Applied offset for ${type}: -${offset}, new value: ${currentRaw}`);
|
|
167
|
-
}
|
|
168
|
-
let yearlyConsumption = Math.max(0, currentRaw - initialReading);
|
|
169
|
-
|
|
170
|
-
// For gas: convert m³ to kWh AFTER calculating the difference
|
|
171
|
-
if (type === 'gas') {
|
|
172
|
-
const brennwert = this.adapter.config.gasBrennwert || 11.5;
|
|
173
|
-
const zZahl = this.adapter.config.gasZahl || 0.95;
|
|
174
|
-
const yearlyVolume = yearlyConsumption;
|
|
175
|
-
yearlyConsumption = calculator.convertGasM3ToKWh(yearlyConsumption, brennwert, zZahl);
|
|
176
|
-
await this.adapter.setStateAsync(`${type}.consumption.yearlyVolume`, yearlyVolume, true);
|
|
177
|
-
this.adapter.log.info(
|
|
178
|
-
`Init yearly ${type}: ${yearlyConsumption.toFixed(2)} kWh = ${(currentRaw - initialReading).toFixed(2)} m³ (current: ${currentRaw.toFixed(2)} m³, initial: ${initialReading} m³)`,
|
|
179
|
-
);
|
|
180
|
-
} else {
|
|
181
|
-
this.adapter.log.info(
|
|
182
|
-
`Init yearly ${type}: ${yearlyConsumption.toFixed(2)} (current: ${currentRaw.toFixed(2)}, initial: ${initialReading})`,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
await this.adapter.setStateAsync(`${type}.consumption.yearly`, yearlyConsumption, true);
|
|
187
|
-
if (typeof this.adapter.updateCosts === 'function') {
|
|
188
|
-
await this.adapter.updateCosts(type);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Update billing countdown
|
|
194
|
-
if (typeof this.adapter.updateBillingCountdown === 'function') {
|
|
195
|
-
await this.adapter.updateBillingCountdown(type);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
this.adapter.log.debug(`Initial cost calculation completed for ${type}`);
|
|
76
|
+
this.adapter.log.debug(`${type} initialization delegated to MultiMeterManager`);
|
|
199
77
|
}
|
|
200
78
|
|
|
201
79
|
/**
|
|
202
|
-
*
|
|
80
|
+
* Updates the current price display for all meters of a type
|
|
81
|
+
* NOTE: Since v1.4.6, this updates ALL meters (main + additional)
|
|
82
|
+
* Only main meters support HT/NT, additional meters have fixed price
|
|
203
83
|
*
|
|
204
84
|
* @param {string} type - Utility type
|
|
205
|
-
* @param {string} sensorDP - Sensor datapoint ID
|
|
206
|
-
* @param {number} value - New sensor value
|
|
207
85
|
*/
|
|
208
|
-
async
|
|
209
|
-
|
|
210
|
-
this.adapter.log.warn(`Invalid sensor value for ${type}: ${value}`);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
this.adapter.log.debug(`Sensor update for ${type}: ${value}`);
|
|
215
|
-
|
|
216
|
-
const now = Date.now();
|
|
217
|
-
let consumption = value;
|
|
218
|
-
let consumptionM3 = null;
|
|
219
|
-
|
|
220
|
-
const configType = this.getConfigType(type);
|
|
221
|
-
|
|
222
|
-
// Apply offset FIRST
|
|
223
|
-
const offsetKey = `${configType}Offset`;
|
|
224
|
-
const offset = this.adapter.config[offsetKey] || 0;
|
|
225
|
-
if (offset !== 0) {
|
|
226
|
-
consumption = consumption - offset;
|
|
227
|
-
this.adapter.log.debug(`Applied offset for ${type}: -${offset}, new value: ${consumption}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// For gas, convert m³ to kWh
|
|
231
|
-
if (type === 'gas') {
|
|
232
|
-
const brennwert = this.adapter.config.gasBrennwert || 11.5;
|
|
233
|
-
const zZahl = this.adapter.config.gasZahl || 0.95;
|
|
234
|
-
consumptionM3 = consumption;
|
|
235
|
-
await this.adapter.setStateAsync(`${type}.info.meterReadingVolume`, consumption, true);
|
|
236
|
-
consumption = calculator.convertGasM3ToKWh(consumption, brennwert, zZahl);
|
|
237
|
-
consumption = calculator.roundToDecimals(consumption, 2);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Update meter reading
|
|
241
|
-
await this.adapter.setStateAsync(`${type}.info.meterReading`, consumption, true);
|
|
242
|
-
|
|
243
|
-
// Calculate deltas
|
|
244
|
-
const lastValue = this.lastSensorValues[sensorDP];
|
|
245
|
-
this.lastSensorValues[sensorDP] = consumption;
|
|
246
|
-
|
|
247
|
-
if (lastValue === undefined || consumption < lastValue) {
|
|
248
|
-
if (lastValue !== undefined && consumption < lastValue) {
|
|
249
|
-
this.adapter.log.warn(
|
|
250
|
-
`${type}: Sensor value decreased (${lastValue} -> ${consumption}). Assuming meter reset or replacement.`,
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
if (typeof this.adapter.updateCosts === 'function') {
|
|
254
|
-
await this.adapter.updateCosts(type);
|
|
255
|
-
}
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const delta = consumption - lastValue;
|
|
260
|
-
this.adapter.log.debug(`${type} delta: ${delta}`);
|
|
261
|
-
|
|
262
|
-
// Track volume for gas
|
|
263
|
-
if (type === 'gas') {
|
|
264
|
-
const brennwert = this.adapter.config.gasBrennwert || 11.5;
|
|
265
|
-
const zZahl = this.adapter.config.gasZahl || 0.95;
|
|
266
|
-
const deltaVolume = delta / (brennwert * zZahl);
|
|
267
|
-
|
|
268
|
-
const dailyVolume = await this.adapter.getStateAsync(`${type}.consumption.dailyVolume`);
|
|
269
|
-
const monthlyVolume = await this.adapter.getStateAsync(`${type}.consumption.monthlyVolume`);
|
|
270
|
-
const yearlyVolume = await this.adapter.getStateAsync(`${type}.consumption.yearlyVolume`);
|
|
271
|
-
|
|
272
|
-
await this.adapter.setStateAsync(
|
|
273
|
-
`${type}.consumption.dailyVolume`,
|
|
274
|
-
calculator.roundToDecimals((dailyVolume?.val || 0) + deltaVolume, 2),
|
|
275
|
-
true,
|
|
276
|
-
);
|
|
277
|
-
await this.adapter.setStateAsync(
|
|
278
|
-
`${type}.consumption.monthlyVolume`,
|
|
279
|
-
calculator.roundToDecimals((monthlyVolume?.val || 0) + deltaVolume, 2),
|
|
280
|
-
true,
|
|
281
|
-
);
|
|
282
|
-
await this.adapter.setStateAsync(
|
|
283
|
-
`${type}.consumption.yearlyVolume`,
|
|
284
|
-
calculator.roundToDecimals((yearlyVolume?.val || 0) + deltaVolume, 3),
|
|
285
|
-
true,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Update consumption values
|
|
290
|
-
const dailyState = await this.adapter.getStateAsync(`${type}.consumption.daily`);
|
|
291
|
-
await this.adapter.setStateAsync(
|
|
292
|
-
`${type}.consumption.daily`,
|
|
293
|
-
calculator.roundToDecimals((dailyState?.val || 0) + delta, 2),
|
|
294
|
-
true,
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
const monthlyState = await this.adapter.getStateAsync(`${type}.consumption.monthly`);
|
|
298
|
-
await this.adapter.setStateAsync(
|
|
299
|
-
`${type}.consumption.monthly`,
|
|
300
|
-
calculator.roundToDecimals((monthlyState?.val || 0) + delta, 2),
|
|
301
|
-
true,
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
// HT/NT tracking
|
|
305
|
-
const htNtEnabledKey = `${configType}HtNtEnabled`;
|
|
306
|
-
if (this.adapter.config[htNtEnabledKey]) {
|
|
307
|
-
const isHT = calculator.isHTTime(this.adapter.config, configType);
|
|
308
|
-
const suffix = isHT ? 'HT' : 'NT';
|
|
309
|
-
|
|
310
|
-
const dHTNT = await this.adapter.getStateAsync(`${type}.consumption.daily${suffix}`);
|
|
311
|
-
await this.adapter.setStateAsync(
|
|
312
|
-
`${type}.consumption.daily${suffix}`,
|
|
313
|
-
calculator.roundToDecimals((dHTNT?.val || 0) + delta, 2),
|
|
314
|
-
true,
|
|
315
|
-
);
|
|
86
|
+
async updateCurrentPrice(type) {
|
|
87
|
+
const configType = getConfigType(type);
|
|
316
88
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
`${type}.consumption.monthly${suffix}`,
|
|
320
|
-
calculator.roundToDecimals((mHTNT?.val || 0) + delta, 2),
|
|
321
|
-
true,
|
|
322
|
-
);
|
|
89
|
+
// Get all meters for this type
|
|
90
|
+
const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
|
|
323
91
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
calculator.roundToDecimals((yHTNT?.val || 0) + delta, 2),
|
|
328
|
-
true,
|
|
329
|
-
);
|
|
330
|
-
}
|
|
92
|
+
for (const meter of meters) {
|
|
93
|
+
let tariffName = 'Standard';
|
|
94
|
+
let activePrice = 0;
|
|
331
95
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const initialReading = this.adapter.config[initialReadingKey] || 0;
|
|
96
|
+
// Check if this meter has HT/NT enabled
|
|
97
|
+
const htNtEnabled = meter.config?.htNtEnabled || false;
|
|
335
98
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const brennwert = this.adapter.config.gasBrennwert || 11.5;
|
|
346
|
-
const zZahl = this.adapter.config.gasZahl || 0.95;
|
|
347
|
-
yearlyAmount = calculator.convertGasM3ToKWh(yearlyM3, brennwert, zZahl);
|
|
99
|
+
if (htNtEnabled) {
|
|
100
|
+
const isHT = calculator.isHTTime(this.adapter.config, configType);
|
|
101
|
+
if (isHT) {
|
|
102
|
+
activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
|
|
103
|
+
tariffName = 'Haupttarif (HT)';
|
|
104
|
+
} else {
|
|
105
|
+
activePrice = this.adapter.config[`${configType}NtPrice`] || 0;
|
|
106
|
+
tariffName = 'Nebentarif (NT)';
|
|
107
|
+
}
|
|
348
108
|
} else {
|
|
349
|
-
|
|
109
|
+
// Use meter's configured price
|
|
110
|
+
activePrice = meter.config?.preis || 0;
|
|
350
111
|
}
|
|
112
|
+
|
|
113
|
+
const basePath = `${type}.${meter.name}`;
|
|
351
114
|
await this.adapter.setStateAsync(
|
|
352
|
-
`${
|
|
353
|
-
calculator.roundToDecimals(
|
|
354
|
-
true,
|
|
355
|
-
);
|
|
356
|
-
} else {
|
|
357
|
-
const yState = await this.adapter.getStateAsync(`${type}.consumption.yearly`);
|
|
358
|
-
await this.adapter.setStateAsync(
|
|
359
|
-
`${type}.consumption.yearly`,
|
|
360
|
-
calculator.roundToDecimals((yState?.val || 0) + delta, 2),
|
|
115
|
+
`${basePath}.info.currentPrice`,
|
|
116
|
+
calculator.roundToDecimals(activePrice, 4),
|
|
361
117
|
true,
|
|
362
118
|
);
|
|
119
|
+
await this.adapter.setStateAsync(`${basePath}.info.currentTariff`, tariffName, true);
|
|
363
120
|
}
|
|
364
|
-
|
|
365
|
-
if (typeof this.adapter.updateCosts === 'function') {
|
|
366
|
-
await this.adapter.updateCosts(type);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
await this.adapter.setStateAsync(`${type}.consumption.lastUpdate`, now, true);
|
|
370
|
-
await this.adapter.setStateAsync(`${type}.info.lastSync`, now, true);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Updates the current price display
|
|
375
|
-
*
|
|
376
|
-
* @param {string} type - Utility type
|
|
377
|
-
*/
|
|
378
|
-
async updateCurrentPrice(type) {
|
|
379
|
-
const configType = this.getConfigType(type);
|
|
380
|
-
|
|
381
|
-
// Check for HT/NT
|
|
382
|
-
const htNtEnabledKey = `${configType}HtNtEnabled`;
|
|
383
|
-
const htNtEnabled = this.adapter.config[htNtEnabledKey] || false;
|
|
384
|
-
|
|
385
|
-
let tariffName = 'Standard';
|
|
386
|
-
let activePrice = 0;
|
|
387
|
-
|
|
388
|
-
if (htNtEnabled) {
|
|
389
|
-
const isHT = calculator.isHTTime(this.adapter.config, configType);
|
|
390
|
-
if (isHT) {
|
|
391
|
-
activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
|
|
392
|
-
tariffName = 'Haupttarif (HT)';
|
|
393
|
-
} else {
|
|
394
|
-
activePrice = this.adapter.config[`${configType}NtPrice`] || 0;
|
|
395
|
-
tariffName = 'Nebentarif (NT)';
|
|
396
|
-
}
|
|
397
|
-
} else {
|
|
398
|
-
const priceKey = `${configType}Preis`;
|
|
399
|
-
activePrice = this.adapter.config[priceKey] || 0;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
await this.adapter.setStateAsync(`${type}.info.currentPrice`, calculator.roundToDecimals(activePrice, 4), true);
|
|
403
|
-
await this.adapter.setStateAsync(`${type}.info.currentTariff`, tariffName, true);
|
|
404
121
|
}
|
|
405
122
|
}
|
|
406
123
|
|