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/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
+ }