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
package/main.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* ioBroker Nebenkosten-Monitor Adapter
|
|
5
|
+
* Monitors gas, water, and electricity consumption with cost calculation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const utils = require('@iobroker/adapter-core');
|
|
9
|
+
const ConsumptionManager = require('./lib/consumptionManager');
|
|
10
|
+
const BillingManager = require('./lib/billingManager');
|
|
11
|
+
const MessagingHandler = require('./lib/messagingHandler');
|
|
12
|
+
const MultiMeterManager = require('./lib/multiMeterManager');
|
|
13
|
+
|
|
14
|
+
class NebenkostenMonitor extends utils.Adapter {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Partial<utils.AdapterOptions>} [options] - Adapter options
|
|
17
|
+
*/
|
|
18
|
+
constructor(options) {
|
|
19
|
+
super({
|
|
20
|
+
...options,
|
|
21
|
+
name: 'nebenkosten-monitor',
|
|
22
|
+
});
|
|
23
|
+
this.on('ready', this.onReady.bind(this));
|
|
24
|
+
this.on('stateChange', this.onStateChange.bind(this));
|
|
25
|
+
this.on('unload', this.onUnload.bind(this));
|
|
26
|
+
this.on('message', this.onMessage.bind(this));
|
|
27
|
+
|
|
28
|
+
// Initialize Managers
|
|
29
|
+
this.consumptionManager = new ConsumptionManager(this);
|
|
30
|
+
this.billingManager = new BillingManager(this);
|
|
31
|
+
this.messagingHandler = new MessagingHandler(this);
|
|
32
|
+
this.multiMeterManager = null; // Initialized in onReady after other managers
|
|
33
|
+
|
|
34
|
+
this.periodicTimers = {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Is called when databases are connected and adapter received configuration.
|
|
39
|
+
*/
|
|
40
|
+
async onReady() {
|
|
41
|
+
this.log.info('Nebenkosten-Monitor starting...');
|
|
42
|
+
|
|
43
|
+
// Initialize MultiMeterManager
|
|
44
|
+
this.multiMeterManager = new MultiMeterManager(this, this.consumptionManager, this.billingManager);
|
|
45
|
+
|
|
46
|
+
// Initialize each utility type based on configuration
|
|
47
|
+
await this.initializeUtility('gas', this.config.gasAktiv);
|
|
48
|
+
await this.initializeUtility('water', this.config.wasserAktiv);
|
|
49
|
+
await this.initializeUtility('electricity', this.config.stromAktiv);
|
|
50
|
+
|
|
51
|
+
await this.initializeUtility('pv', this.config.pvAktiv);
|
|
52
|
+
|
|
53
|
+
// Initialize Multi-Meter structures for each active type
|
|
54
|
+
if (this.config.gasAktiv) {
|
|
55
|
+
await this.multiMeterManager.initializeType('gas');
|
|
56
|
+
}
|
|
57
|
+
if (this.config.wasserAktiv) {
|
|
58
|
+
await this.multiMeterManager.initializeType('water');
|
|
59
|
+
}
|
|
60
|
+
if (this.config.stromAktiv) {
|
|
61
|
+
await this.multiMeterManager.initializeType('electricity');
|
|
62
|
+
}
|
|
63
|
+
if (this.config.pvAktiv) {
|
|
64
|
+
await this.multiMeterManager.initializeType('pv');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Initialize General Info States
|
|
68
|
+
await this.setObjectNotExistsAsync('info', {
|
|
69
|
+
type: 'channel',
|
|
70
|
+
common: { name: 'General Information' },
|
|
71
|
+
native: {},
|
|
72
|
+
});
|
|
73
|
+
await this.setObjectNotExistsAsync('info.lastMonthlyReport', {
|
|
74
|
+
type: 'state',
|
|
75
|
+
common: {
|
|
76
|
+
name: 'Last Monthly Report Sent Date',
|
|
77
|
+
type: 'string', // Storing ISO date string 'YYYY-MM-DD'
|
|
78
|
+
role: 'date',
|
|
79
|
+
read: true,
|
|
80
|
+
write: true,
|
|
81
|
+
def: '',
|
|
82
|
+
},
|
|
83
|
+
native: {},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Subscribe to billing period closure triggers
|
|
87
|
+
this.subscribeStates('*.billing.closePeriod');
|
|
88
|
+
|
|
89
|
+
// Subscribe to manual adjustment changes
|
|
90
|
+
this.subscribeStates('*.adjustment.value');
|
|
91
|
+
this.subscribeStates('*.adjustment.note');
|
|
92
|
+
|
|
93
|
+
// Set up periodic tasks
|
|
94
|
+
this.setupPeriodicTasks();
|
|
95
|
+
|
|
96
|
+
this.log.info('Nebenkosten-Monitor initialized successfully');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Delegation Methods (backward compatibility for internal calls) ---
|
|
100
|
+
|
|
101
|
+
async initializeUtility(type, isActive) {
|
|
102
|
+
return this.consumptionManager.initializeUtility(type, isActive);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async handleSensorUpdate(type, sensorDP, value) {
|
|
106
|
+
return this.consumptionManager.handleSensorUpdate(type, sensorDP, value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async updateCurrentPrice(type) {
|
|
110
|
+
return this.consumptionManager.updateCurrentPrice(type);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async updateCosts(type) {
|
|
114
|
+
// For Multi-Meter setups, delegate to multiMeterManager
|
|
115
|
+
if (this.multiMeterManager) {
|
|
116
|
+
const meters = this.multiMeterManager.getMetersForType(type);
|
|
117
|
+
if (meters.length > 0) {
|
|
118
|
+
// Update costs for each meter
|
|
119
|
+
for (const meter of meters) {
|
|
120
|
+
await this.multiMeterManager.updateCosts(type, meter.name, meter.config);
|
|
121
|
+
}
|
|
122
|
+
// Update totals if multiple meters exist
|
|
123
|
+
if (meters.length > 1) {
|
|
124
|
+
await this.multiMeterManager.updateTotalCosts(type);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Fallback to legacy billingManager for single-meter setups (backward compatibility)
|
|
130
|
+
return this.billingManager.updateCosts(type);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async closeBillingPeriod(type) {
|
|
134
|
+
return this.billingManager.closeBillingPeriod(type);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async updateBillingCountdown(type) {
|
|
138
|
+
return this.billingManager.updateBillingCountdown(type);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async resetDailyCounters(type) {
|
|
142
|
+
return this.billingManager.resetDailyCounters(type);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async resetMonthlyCounters(type) {
|
|
146
|
+
return this.billingManager.resetMonthlyCounters(type);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async resetYearlyCounters(type) {
|
|
150
|
+
return this.billingManager.resetYearlyCounters(type);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async checkPeriodResets() {
|
|
154
|
+
return this.billingManager.checkPeriodResets();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async checkNotifications() {
|
|
158
|
+
return this.messagingHandler.checkNotifications();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Sets up periodic tasks (daily reset, etc.)
|
|
163
|
+
*/
|
|
164
|
+
setupPeriodicTasks() {
|
|
165
|
+
// Check every minute for period changes
|
|
166
|
+
this.periodicTimers.checkPeriods = setInterval(async () => {
|
|
167
|
+
await this.checkPeriodResets();
|
|
168
|
+
}, 60000); // Every minute
|
|
169
|
+
|
|
170
|
+
// Initial check
|
|
171
|
+
this.checkPeriodResets();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Is called when adapter shuts down - callback has to be called under any circumstances!
|
|
176
|
+
*
|
|
177
|
+
* @param {() => void} callback - Callback function
|
|
178
|
+
*/
|
|
179
|
+
onUnload(callback) {
|
|
180
|
+
try {
|
|
181
|
+
this.log.info('Nebenkosten-Monitor shutting down...');
|
|
182
|
+
|
|
183
|
+
// Clear all timers
|
|
184
|
+
Object.values(this.periodicTimers).forEach(timer => {
|
|
185
|
+
if (timer) {
|
|
186
|
+
clearInterval(timer);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
callback();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
this.log.error(`Error during unloading: ${error.message}`);
|
|
193
|
+
callback();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Is called if a subscribed state changes
|
|
199
|
+
*
|
|
200
|
+
* @param {string} id - State ID
|
|
201
|
+
* @param {ioBroker.State | null | undefined} state - State object
|
|
202
|
+
*/
|
|
203
|
+
async onStateChange(id, state) {
|
|
204
|
+
if (!state || state.val === null || state.val === undefined) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check if this is a closePeriod button press
|
|
209
|
+
if (id.includes('.billing.closePeriod') && state.val === true && !state.ack) {
|
|
210
|
+
const parts = id.split('.');
|
|
211
|
+
|
|
212
|
+
// Parse state ID: nebenkosten-monitor.0.gas.erdgeschoss.billing.closePeriod
|
|
213
|
+
// Remove adapter prefix: gas.erdgeschoss.billing.closePeriod
|
|
214
|
+
const statePathParts = parts.slice(2); // Remove "nebenkosten-monitor" and "0"
|
|
215
|
+
|
|
216
|
+
// Determine if this is main meter or additional meter
|
|
217
|
+
if (statePathParts.length === 3) {
|
|
218
|
+
// Main meter: gas.billing.closePeriod
|
|
219
|
+
const type = statePathParts[0];
|
|
220
|
+
this.log.info(`User triggered billing period closure for ${type} (main meter)`);
|
|
221
|
+
await this.closeBillingPeriod(type);
|
|
222
|
+
} else if (statePathParts.length === 4) {
|
|
223
|
+
// Additional meter: gas.erdgeschoss.billing.closePeriod
|
|
224
|
+
const type = statePathParts[0];
|
|
225
|
+
const meterName = statePathParts[1];
|
|
226
|
+
this.log.info(`User triggered billing period closure for ${type}.${meterName}`);
|
|
227
|
+
|
|
228
|
+
// Find the meter object from multiMeterManager
|
|
229
|
+
const meters = this.multiMeterManager?.getMetersForType(type) || [];
|
|
230
|
+
const meter = meters.find(m => m.name === meterName);
|
|
231
|
+
|
|
232
|
+
if (meter) {
|
|
233
|
+
await this.billingManager.closeBillingPeriodForMeter(type, meter);
|
|
234
|
+
} else {
|
|
235
|
+
this.log.error(`Meter "${meterName}" not found for type ${type}!`);
|
|
236
|
+
await this.setStateAsync(`${type}.${meterName}.billing.closePeriod`, false, true);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if this is an adjustment value change
|
|
243
|
+
if (id.includes('.adjustment.value') && !state.ack) {
|
|
244
|
+
const parts = id.split('.');
|
|
245
|
+
const type = parts[parts.length - 3];
|
|
246
|
+
this.log.info(`Adjustment value changed for ${type}: ${state.val}`);
|
|
247
|
+
await this.setStateAsync(`${type}.adjustment.applied`, Date.now(), true);
|
|
248
|
+
|
|
249
|
+
// Update costs for all meters of this type
|
|
250
|
+
await this.updateCosts(type);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Determine which utility this sensor belongs to
|
|
255
|
+
// First check if it's a multi-meter sensor (additional meters)
|
|
256
|
+
if (this.multiMeterManager) {
|
|
257
|
+
const meterInfo = this.multiMeterManager.findMeterBySensor(id);
|
|
258
|
+
if (meterInfo && typeof state.val === 'number') {
|
|
259
|
+
await this.multiMeterManager.handleSensorUpdate(meterInfo.type, meterInfo.meterName, id, state.val);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check main meter sensors
|
|
265
|
+
const types = ['gas', 'water', 'electricity', 'pv'];
|
|
266
|
+
for (const type of types) {
|
|
267
|
+
const configType = this.consumptionManager.getConfigType(type);
|
|
268
|
+
|
|
269
|
+
if (this.config[`${configType}Aktiv`] && this.config[`${configType}SensorDP`] === id) {
|
|
270
|
+
if (typeof state.val === 'number') {
|
|
271
|
+
await this.handleSensorUpdate(type, id, state.val);
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Is called when adapter receives message from config window.
|
|
280
|
+
*
|
|
281
|
+
* @param {Record<string, any>} obj - Message object from config
|
|
282
|
+
*/
|
|
283
|
+
async onMessage(obj) {
|
|
284
|
+
await this.messagingHandler.handleMessage(obj);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (require.main !== module) {
|
|
289
|
+
// Export the constructor in compact mode
|
|
290
|
+
/**
|
|
291
|
+
* @param {Partial<utils.AdapterOptions>} [options] - Adapter options
|
|
292
|
+
*/
|
|
293
|
+
module.exports = options => new NebenkostenMonitor(options);
|
|
294
|
+
} else {
|
|
295
|
+
// otherwise start the instance directly
|
|
296
|
+
new NebenkostenMonitor();
|
|
297
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iobroker.utility-monitor",
|
|
3
|
+
"version": "1.4.2",
|
|
4
|
+
"description": "Monitor gas, water, and electricity consumption with cost calculation",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "fischi87",
|
|
7
|
+
"email": "axel.fischer@hotmail.com"
|
|
8
|
+
},
|
|
9
|
+
"contributors": [
|
|
10
|
+
{
|
|
11
|
+
"name": "Axel Fischer"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/fischi87/ioBroker.utility-monitor",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ioBroker",
|
|
18
|
+
"iobroker",
|
|
19
|
+
"utility",
|
|
20
|
+
"electricity",
|
|
21
|
+
"gas",
|
|
22
|
+
"water",
|
|
23
|
+
"costs",
|
|
24
|
+
"energy",
|
|
25
|
+
"monitoring",
|
|
26
|
+
"metering",
|
|
27
|
+
"consumption",
|
|
28
|
+
"home-automation",
|
|
29
|
+
"smart-home"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/fischi87/ioBroker.utility-monitor.git"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">= 20"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@iobroker/adapter-core": "^3.3.2"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@alcalzone/release-script": "~5.0.0",
|
|
43
|
+
"@alcalzone/release-script-plugin-iobroker": "~4.0.0",
|
|
44
|
+
"@alcalzone/release-script-plugin-license": "~4.0.0",
|
|
45
|
+
"@alcalzone/release-script-plugin-manual-review": "~4.0.0",
|
|
46
|
+
"@iobroker/adapter-dev": "~1.5.0",
|
|
47
|
+
"@iobroker/dev-server": "~0.8.0",
|
|
48
|
+
"@iobroker/eslint-config": "~2.2.0",
|
|
49
|
+
"@iobroker/testing": "~5.2.2",
|
|
50
|
+
"@tsconfig/node20": "~20.1.8",
|
|
51
|
+
"@types/iobroker": "npm:@iobroker/types@~7.1.0",
|
|
52
|
+
"@types/node": "~20.19.27",
|
|
53
|
+
"typescript": "~5.9.3"
|
|
54
|
+
},
|
|
55
|
+
"main": "main.js",
|
|
56
|
+
"files": [
|
|
57
|
+
"admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
|
|
58
|
+
"admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
|
|
59
|
+
"lib/",
|
|
60
|
+
"www/",
|
|
61
|
+
"io-package.json",
|
|
62
|
+
"LICENSE",
|
|
63
|
+
"main.js"
|
|
64
|
+
],
|
|
65
|
+
"scripts": {
|
|
66
|
+
"test:js": "mocha --config test/mocharc.custom.json \"{!(node_modules|test)/**/*.test.js,*.test.js,test/**/*.test.js}\"",
|
|
67
|
+
"test:package": "mocha test/package --exit",
|
|
68
|
+
"test:integration": "mocha test/integration --exit",
|
|
69
|
+
"test": "npm run test:js && npm run test:package",
|
|
70
|
+
"check": "tsc --noEmit -p tsconfig.check.json",
|
|
71
|
+
"lint": "eslint -c eslint.config.mjs .",
|
|
72
|
+
"translate": "translate-adapter",
|
|
73
|
+
"release": "release-script",
|
|
74
|
+
"dev-server": "dev-server"
|
|
75
|
+
},
|
|
76
|
+
"bugs": {
|
|
77
|
+
"url": "https://github.com/fischi87/ioBroker.utility-monitor/issues"
|
|
78
|
+
},
|
|
79
|
+
"readmeFilename": "README.md"
|
|
80
|
+
}
|