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
|
@@ -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,178 @@
|
|
|
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
|
+
module.exports = {
|
|
170
|
+
ensureNumber,
|
|
171
|
+
roundToDecimals,
|
|
172
|
+
parseGermanDate,
|
|
173
|
+
formatDateString,
|
|
174
|
+
isLeapYear,
|
|
175
|
+
getMonthsDifference,
|
|
176
|
+
normalizeMeterName,
|
|
177
|
+
safeSetObjectNotExists,
|
|
178
|
+
};
|
|
@@ -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,64 @@ 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(
|
|
129
|
+
`${type.label} Zähler "${meter.name}": Kein Vertragsbeginn konfiguriert!`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (!meter.sensorDP) {
|
|
133
|
+
this.log.warn(
|
|
134
|
+
`${type.label} Zähler "${meter.name}": Kein Sensor-Datenpunkt konfiguriert!`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
85
143
|
// --- Delegation Methods (backward compatibility for internal calls) ---
|
|
86
144
|
|
|
87
145
|
async initializeUtility(type, isActive) {
|
|
88
146
|
return this.consumptionManager.initializeUtility(type, isActive);
|
|
89
147
|
}
|
|
90
148
|
|
|
91
|
-
async handleSensorUpdate(type, sensorDP, value) {
|
|
92
|
-
return this.consumptionManager.handleSensorUpdate(type, sensorDP, value);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
149
|
async updateCurrentPrice(type) {
|
|
96
150
|
return this.consumptionManager.updateCurrentPrice(type);
|
|
97
151
|
}
|
|
@@ -195,20 +249,15 @@ class UtilityMonitor extends utils.Adapter {
|
|
|
195
249
|
if (id.includes('.billing.closePeriod') && state.val === true && !state.ack) {
|
|
196
250
|
const parts = id.split('.');
|
|
197
251
|
|
|
198
|
-
// Parse state ID: nebenkosten-monitor.0.gas.
|
|
199
|
-
// Remove adapter prefix: gas.
|
|
252
|
+
// Parse state ID: nebenkosten-monitor.0.gas.meterName.billing.closePeriod
|
|
253
|
+
// Remove adapter prefix: gas.meterName.billing.closePeriod
|
|
200
254
|
const statePathParts = parts.slice(2); // Remove "nebenkosten-monitor" and "0"
|
|
201
255
|
|
|
202
|
-
//
|
|
203
|
-
if (statePathParts.length ===
|
|
204
|
-
// Main meter: gas.billing.closePeriod
|
|
205
|
-
const type = statePathParts[0];
|
|
206
|
-
this.log.info(`User triggered billing period closure for ${type} (main meter)`);
|
|
207
|
-
await this.closeBillingPeriod(type);
|
|
208
|
-
} else if (statePathParts.length === 4) {
|
|
209
|
-
// Additional meter: gas.erdgeschoss.billing.closePeriod
|
|
256
|
+
// All meters now use: gas.meterName.billing.closePeriod (length === 4)
|
|
257
|
+
if (statePathParts.length === 4) {
|
|
210
258
|
const type = statePathParts[0];
|
|
211
259
|
const meterName = statePathParts[1];
|
|
260
|
+
|
|
212
261
|
this.log.info(`User triggered billing period closure for ${type}.${meterName}`);
|
|
213
262
|
|
|
214
263
|
// Find the meter object from multiMeterManager
|
|
@@ -221,40 +270,50 @@ class UtilityMonitor extends utils.Adapter {
|
|
|
221
270
|
this.log.error(`Meter "${meterName}" not found for type ${type}!`);
|
|
222
271
|
await this.setStateAsync(`${type}.${meterName}.billing.closePeriod`, false, true);
|
|
223
272
|
}
|
|
273
|
+
} else {
|
|
274
|
+
// Invalid path format
|
|
275
|
+
this.log.warn(`Invalid billing period closure trigger: ${id}`);
|
|
224
276
|
}
|
|
225
277
|
return;
|
|
226
278
|
}
|
|
227
279
|
|
|
228
280
|
// Check if this is an adjustment value change
|
|
281
|
+
// New structure: gas.meterName.adjustment.value (4 parts after namespace)
|
|
229
282
|
if (id.includes('.adjustment.value') && !state.ack) {
|
|
230
283
|
const parts = id.split('.');
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
await this.setStateAsync(`${type}.adjustment.applied`, Date.now(), true);
|
|
284
|
+
// Parse: nebenkosten-monitor.0.gas.meterName.adjustment.value
|
|
285
|
+
const statePathParts = parts.slice(2); // Remove "nebenkosten-monitor" and "0"
|
|
234
286
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
287
|
+
if (statePathParts.length === 3) {
|
|
288
|
+
// New structure: gas.meterName.adjustment.value
|
|
289
|
+
const type = statePathParts[0];
|
|
290
|
+
const meterName = statePathParts[1];
|
|
239
291
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
292
|
+
this.log.info(`Adjustment value changed for ${type}.${meterName}: ${state.val}`);
|
|
293
|
+
await this.setStateAsync(`${type}.${meterName}.adjustment.applied`, Date.now(), true);
|
|
294
|
+
|
|
295
|
+
// Update costs for THIS specific meter
|
|
296
|
+
if (this.multiMeterManager) {
|
|
297
|
+
const meters = this.multiMeterManager.getMetersForType(type);
|
|
298
|
+
const meter = meters.find(m => m.name === meterName);
|
|
299
|
+
if (meter) {
|
|
300
|
+
await this.multiMeterManager.updateCosts(type, meterName, meter.config);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
246
303
|
return;
|
|
247
304
|
}
|
|
248
305
|
}
|
|
249
306
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
307
|
+
// Determine which utility this sensor belongs to
|
|
308
|
+
// All meters (including main) are now handled by multiMeterManager
|
|
309
|
+
if (this.multiMeterManager) {
|
|
310
|
+
const meters = this.multiMeterManager.findMeterBySensor(id);
|
|
311
|
+
if (meters.length > 0 && state.val != null) {
|
|
312
|
+
// Convert sensor value to number (handles strings, German commas, etc.)
|
|
313
|
+
const numValue = calculator.ensureNumber(state.val);
|
|
314
|
+
// Call handleSensorUpdate for each meter using this sensor
|
|
315
|
+
for (const meterInfo of meters) {
|
|
316
|
+
await this.multiMeterManager.handleSensorUpdate(meterInfo.type, meterInfo.meterName, id, numValue);
|
|
258
317
|
}
|
|
259
318
|
return;
|
|
260
319
|
}
|
|
@@ -267,7 +326,11 @@ class UtilityMonitor extends utils.Adapter {
|
|
|
267
326
|
* @param {Record<string, any>} obj - Message object from config
|
|
268
327
|
*/
|
|
269
328
|
async onMessage(obj) {
|
|
270
|
-
|
|
329
|
+
if (obj.command === 'importCSV') {
|
|
330
|
+
await this.importManager.handleImportCSV(obj);
|
|
331
|
+
} else {
|
|
332
|
+
await this.messagingHandler.handleMessage(obj);
|
|
333
|
+
}
|
|
271
334
|
}
|
|
272
335
|
}
|
|
273
336
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.utility-monitor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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",
|