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.
@@ -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
+ };