iobroker.utility-monitor 1.4.2
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/LICENSE +21 -0
- package/README.md +384 -0
- package/admin/i18n/de.json +5 -0
- package/admin/i18n/en.json +5 -0
- package/admin/i18n/es.json +5 -0
- package/admin/i18n/fr.json +5 -0
- package/admin/i18n/it.json +5 -0
- package/admin/i18n/nl.json +5 -0
- package/admin/i18n/pl.json +5 -0
- package/admin/i18n/pt.json +5 -0
- package/admin/i18n/ru.json +5 -0
- package/admin/i18n/uk.json +5 -0
- package/admin/i18n/zh-cn.json +5 -0
- package/admin/jsonConfig.json +1542 -0
- package/admin/utility-monitor.png +0 -0
- package/io-package.json +188 -0
- package/lib/adapter-config.d.ts +19 -0
- package/lib/billingManager.js +806 -0
- package/lib/calculator.js +254 -0
- package/lib/configParser.js +92 -0
- package/lib/consumptionManager.js +407 -0
- package/lib/messagingHandler.js +339 -0
- package/lib/multiMeterManager.js +749 -0
- package/lib/stateManager.js +1556 -0
- package/main.js +297 -0
- package/package.json +80 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculator module for nebenkosten-monitor
|
|
3
|
+
* Provides utility functions for gas conversion, cost calculation, and consumption aggregation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Converts gas volume from m³ to kWh
|
|
8
|
+
* Formula: kWh = m³ × Brennwert × Z-Zahl
|
|
9
|
+
*
|
|
10
|
+
* @param {number} m3 - Volume in cubic meters
|
|
11
|
+
* @param {number} brennwert - Calorific value (typically ~11.5 kWh/m³)
|
|
12
|
+
* @param {number} zZahl - Z-number/state number (typically ~0.95)
|
|
13
|
+
* @returns {number} Energy in kWh
|
|
14
|
+
*/
|
|
15
|
+
function convertGasM3ToKWh(m3, brennwert = 11.5, zZahl = 0.95) {
|
|
16
|
+
if (typeof m3 !== 'number' || typeof brennwert !== 'number' || typeof zZahl !== 'number') {
|
|
17
|
+
throw new TypeError('All parameters must be numbers');
|
|
18
|
+
}
|
|
19
|
+
if (m3 < 0 || brennwert <= 0 || zZahl <= 0 || zZahl > 1) {
|
|
20
|
+
throw new RangeError('Invalid parameter values');
|
|
21
|
+
}
|
|
22
|
+
return m3 * brennwert * zZahl;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gets the current price - simplified version
|
|
27
|
+
*
|
|
28
|
+
* @param {number} price - Current price per unit
|
|
29
|
+
* @param {number} basicCharge - Basic charge per month
|
|
30
|
+
* @returns {object} Price object {price, basicCharge}
|
|
31
|
+
*/
|
|
32
|
+
function getCurrentPrice(price, basicCharge = 0) {
|
|
33
|
+
return {
|
|
34
|
+
price: price || 0,
|
|
35
|
+
basicCharge: basicCharge || 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calculates cost for a consumption value using current price
|
|
41
|
+
*
|
|
42
|
+
* @param {number} consumption - Consumption in kWh or m³
|
|
43
|
+
* @param {number} price - Current price per unit
|
|
44
|
+
* @returns {number} Cost in €
|
|
45
|
+
*/
|
|
46
|
+
function calculateCost(consumption, price) {
|
|
47
|
+
if (typeof consumption !== 'number' || consumption < 0) {
|
|
48
|
+
throw new TypeError('Consumption must be a non-negative number');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return consumption * (price || 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
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
|
+
/**
|
|
118
|
+
* Checks if the current time is within the High Tariff (HT) period
|
|
119
|
+
*
|
|
120
|
+
* @param {object} config - Adapter configuration
|
|
121
|
+
* @param {string} type - Utility type: 'gas' or 'strom'
|
|
122
|
+
* @returns {boolean} True if current time is HT, false if NT
|
|
123
|
+
*/
|
|
124
|
+
function isHTTime(config, type) {
|
|
125
|
+
if (!config || !type) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const enabled = config[`${type}HtNtEnabled`];
|
|
130
|
+
if (!enabled) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const startTimeStr = config[`${type}HtStart`];
|
|
135
|
+
const endTimeStr = config[`${type}HtEnd`];
|
|
136
|
+
|
|
137
|
+
if (!startTimeStr || !endTimeStr) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const now = new Date();
|
|
142
|
+
const currentHours = now.getHours();
|
|
143
|
+
const currentMinutes = now.getMinutes();
|
|
144
|
+
const currentTimeMinutes = currentHours * 60 + currentMinutes;
|
|
145
|
+
|
|
146
|
+
const [startH, startM] = startTimeStr.split(':').map(val => parseInt(val, 10));
|
|
147
|
+
const [endH, endM] = endTimeStr.split(':').map(val => parseInt(val, 10));
|
|
148
|
+
|
|
149
|
+
const startTimeMinutes = startH * 60 + (startM || 0);
|
|
150
|
+
const endTimeMinutes = endH * 60 + (endM || 0);
|
|
151
|
+
|
|
152
|
+
if (startTimeMinutes <= endTimeMinutes) {
|
|
153
|
+
// HT period during the day (e.g. 06:00 - 22:00)
|
|
154
|
+
return currentTimeMinutes >= startTimeMinutes && currentTimeMinutes < endTimeMinutes;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// HT period over midnight (e.g. 22:00 - 06:00)
|
|
158
|
+
return currentTimeMinutes >= startTimeMinutes || currentTimeMinutes < endTimeMinutes;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Default constants for the nebenkosten-monitor adapter
|
|
163
|
+
*/
|
|
164
|
+
const DEFAULTS = {
|
|
165
|
+
// Gas conversion defaults
|
|
166
|
+
GAS_BRENNWERT: 11.5, // kWh/m³
|
|
167
|
+
GAS_Z_ZAHL: 0.95, // State number (dimensionless)
|
|
168
|
+
|
|
169
|
+
// Rounding precision
|
|
170
|
+
ROUNDING_DECIMALS: 2,
|
|
171
|
+
|
|
172
|
+
// Time constants
|
|
173
|
+
MILLISECONDS_PER_DAY: 1000 * 60 * 60 * 24,
|
|
174
|
+
DAYS_IN_NORMAL_YEAR: 365,
|
|
175
|
+
DAYS_IN_LEAP_YEAR: 366,
|
|
176
|
+
|
|
177
|
+
// Validation constraints
|
|
178
|
+
MIN_PRICE: 0,
|
|
179
|
+
MAX_PRICE: 9999,
|
|
180
|
+
MIN_CONSUMPTION: 0,
|
|
181
|
+
};
|
|
182
|
+
|
|
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
|
+
module.exports = {
|
|
242
|
+
convertGasM3ToKWh,
|
|
243
|
+
getCurrentPrice,
|
|
244
|
+
calculateCost,
|
|
245
|
+
ensureNumber,
|
|
246
|
+
roundToDecimals,
|
|
247
|
+
parseGermanDate,
|
|
248
|
+
isHTTime,
|
|
249
|
+
formatDateString,
|
|
250
|
+
parseDateString,
|
|
251
|
+
isLeapYear,
|
|
252
|
+
getMonthsDifference,
|
|
253
|
+
DEFAULTS,
|
|
254
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parst einen Config-Wert sicher zu einer Zahl
|
|
5
|
+
*
|
|
6
|
+
* @param {any} value - Der zu parsende Wert
|
|
7
|
+
* @param {number} defaultValue - Default-Wert wenn Parsing fehlschlägt
|
|
8
|
+
* @returns {number} - Geparster Zahlenwert
|
|
9
|
+
*/
|
|
10
|
+
function parseConfigNumber(value, defaultValue = 0) {
|
|
11
|
+
if (value === null || value === undefined || value === '') {
|
|
12
|
+
return defaultValue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Wenn es bereits eine Zahl ist
|
|
16
|
+
if (typeof value === 'number') {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// String zu Zahl konvertieren
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
// Ersetze Komma durch Punkt für deutsche Dezimalzahlen
|
|
23
|
+
const normalized = value.replace(',', '.');
|
|
24
|
+
const parsed = parseFloat(normalized);
|
|
25
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validates if a sensor datapoint ID exists and is valid
|
|
33
|
+
*
|
|
34
|
+
* @param {string} sensorDP - Sensor datapoint ID
|
|
35
|
+
* @returns {boolean} - True if valid
|
|
36
|
+
*/
|
|
37
|
+
function isValidSensorDP(sensorDP) {
|
|
38
|
+
if (!sensorDP || typeof sensorDP !== 'string') {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// Basic validation: should contain at least one dot and not be empty
|
|
42
|
+
return sensorDP.trim().length > 0 && sensorDP.includes('.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validates and parses a date string
|
|
47
|
+
*
|
|
48
|
+
* @param {string} dateStr - Date string
|
|
49
|
+
* @param {string} defaultValue - Default value if parsing fails
|
|
50
|
+
* @returns {string} - Validated date string or default
|
|
51
|
+
*/
|
|
52
|
+
function parseConfigDate(dateStr, defaultValue = '') {
|
|
53
|
+
if (!dateStr || typeof dateStr !== 'string') {
|
|
54
|
+
return defaultValue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const trimmed = dateStr.trim();
|
|
58
|
+
if (trimmed.length === 0) {
|
|
59
|
+
return defaultValue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// German date format: DD.MM.YYYY
|
|
63
|
+
const germanDateRegex = /^\d{1,2}\.\d{1,2}\.\d{2,4}$/;
|
|
64
|
+
// ISO date format: YYYY-MM-DD
|
|
65
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
66
|
+
|
|
67
|
+
if (germanDateRegex.test(trimmed) || isoDateRegex.test(trimmed)) {
|
|
68
|
+
return trimmed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return defaultValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validates a price/cost value (must be non-negative)
|
|
76
|
+
*
|
|
77
|
+
* @param {any} value - Price value
|
|
78
|
+
* @param {number} defaultValue - Default value
|
|
79
|
+
* @returns {number} - Validated price
|
|
80
|
+
*/
|
|
81
|
+
function parseConfigPrice(value, defaultValue = 0) {
|
|
82
|
+
const parsed = parseConfigNumber(value, defaultValue);
|
|
83
|
+
// Prices cannot be negative
|
|
84
|
+
return parsed < 0 ? defaultValue : parsed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
parseConfigNumber,
|
|
89
|
+
isValidSensorDP,
|
|
90
|
+
parseConfigDate,
|
|
91
|
+
parseConfigPrice,
|
|
92
|
+
};
|