iobroker.smartloadmanager 0.0.1
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 +353 -0
- package/admin/i18n/de/translations.json +36 -0
- package/admin/i18n/en/translations.json +36 -0
- package/admin/i18n/es/translations.json +36 -0
- package/admin/i18n/fr/translations.json +36 -0
- package/admin/i18n/it/translations.json +36 -0
- package/admin/i18n/nl/translations.json +36 -0
- package/admin/i18n/pl/translations.json +36 -0
- package/admin/i18n/pt/translations.json +36 -0
- package/admin/i18n/ru/translations.json +36 -0
- package/admin/i18n/uk/translations.json +36 -0
- package/admin/i18n/zh-cn/translations.json +36 -0
- package/admin/jsonConfig.json +1099 -0
- package/admin/smartloadmanager.png +0 -0
- package/io-package.json +257 -0
- package/lib/adapter-config.d.ts +20 -0
- package/main.js +1186 -0
- package/package.json +77 -0
package/main.js
ADDED
|
@@ -0,0 +1,1186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const utils = require('@iobroker/adapter-core');
|
|
4
|
+
|
|
5
|
+
class ZeroFeedIn extends utils.Adapter {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super({
|
|
8
|
+
...options,
|
|
9
|
+
name: 'smartloadmanager',
|
|
10
|
+
});
|
|
11
|
+
this.consumerList = [];
|
|
12
|
+
this.feedInDatapoint = null;
|
|
13
|
+
this.batteryControlModeDatapoint = null;
|
|
14
|
+
this.percentTimer = null;
|
|
15
|
+
this.batteryTimer = null;
|
|
16
|
+
this.checkRunning = false;
|
|
17
|
+
|
|
18
|
+
this.on('ready', this.onReady.bind(this));
|
|
19
|
+
this.on('stateChange', this.onStateChange.bind(this));
|
|
20
|
+
this.on('unload', this.onUnload.bind(this));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async onReady() {
|
|
24
|
+
try {
|
|
25
|
+
this.log.info(`=== Adapter started - PID: ${process.pid} ===`);
|
|
26
|
+
|
|
27
|
+
if (!this.config.FeedInDataPoint) {
|
|
28
|
+
this.log.error('No FeedInDataPoint configured!');
|
|
29
|
+
// zusätzlich als alert melden
|
|
30
|
+
await this.sendNotification('smartloadmanager', 'alarm', '❗ Kein FeedInDataPoint konfiguriert!');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.feedInDatapoint = this.config.FeedInDataPoint;
|
|
34
|
+
|
|
35
|
+
this.batteryControlModeDatapoint = this.config.batteryControlModeDatapoint || null;
|
|
36
|
+
this.log.info(`Configured batteryControlModeDatapoint: ${this.batteryControlModeDatapoint}`);
|
|
37
|
+
|
|
38
|
+
this.consumerList = Array.isArray(this.config.consumer)
|
|
39
|
+
? this.config.consumer.filter(
|
|
40
|
+
v =>
|
|
41
|
+
v &&
|
|
42
|
+
v.enabled &&
|
|
43
|
+
(v.ruletype === 'battery' || ((v.datapoint || v.numericDatapoint) && v.performance > 0)),
|
|
44
|
+
)
|
|
45
|
+
: [];
|
|
46
|
+
|
|
47
|
+
this.consumerList.forEach(v => (v.processingLockSwitch = false));
|
|
48
|
+
|
|
49
|
+
this.log.info(`Loaded consumers: ${this.consumerList.map(v => v.name).join(', ')}`);
|
|
50
|
+
|
|
51
|
+
await this.checkAndCreateConsumerObjects();
|
|
52
|
+
await this.createConsumerStates();
|
|
53
|
+
await this.initializeConsumerStatus();
|
|
54
|
+
await this.subscribeStatesAsync('*');
|
|
55
|
+
await this.subscribeForeignStatesAsync(this.feedInDatapoint);
|
|
56
|
+
if (this.batteryControlModeDatapoint) {
|
|
57
|
+
await this.subscribeForeignStatesAsync(this.batteryControlModeDatapoint);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await this.checkConsumers();
|
|
61
|
+
|
|
62
|
+
/* // Info beim erfolgreichen Start
|
|
63
|
+
if (!this.adapterStartedNotified) {
|
|
64
|
+
await this.sendNotification(
|
|
65
|
+
'smartloadmanager',
|
|
66
|
+
'info',
|
|
67
|
+
'ℹ️ SmartLoadManager Adapter erfolgreich gestartet.',
|
|
68
|
+
);
|
|
69
|
+
this.adapterStartedNotified = true;
|
|
70
|
+
} */
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.log.error(`Error in onReady: ${error.message}`);
|
|
73
|
+
// Fehler zusätzlich als alert melden
|
|
74
|
+
await this.sendNotification('smartloadmanager', 'alert', `❌ Fehler in onReady: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async checkAndCreateConsumerObjects() {
|
|
79
|
+
for (const v of this.consumerList) {
|
|
80
|
+
if (v.datapoint) {
|
|
81
|
+
const obj = await this.getForeignObjectAsync(v.datapoint);
|
|
82
|
+
if (!obj) {
|
|
83
|
+
this.log.warn(`Consumer datapoint ${v.datapoint} does not exist!`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (v.numericDatapoint) {
|
|
87
|
+
const objNum = await this.getForeignObjectAsync(v.numericDatapoint);
|
|
88
|
+
if (!objNum) {
|
|
89
|
+
this.log.warn(`Consumer numericDatapoint ${v.numericDatapoint} does not exist!`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async createConsumerStates() {
|
|
96
|
+
for (let i = 0; i < this.consumerList.length; i++) {
|
|
97
|
+
const v = this.consumerList[i];
|
|
98
|
+
const channelId = `consumer.${i}_${v.name.replace(/\s+/g, '_')}`;
|
|
99
|
+
|
|
100
|
+
await this.setObjectNotExistsAsync(channelId, {
|
|
101
|
+
type: 'channel',
|
|
102
|
+
common: { name: v.name },
|
|
103
|
+
native: {},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await this.setObjectNotExistsAsync(`${channelId}.controlMode`, {
|
|
107
|
+
type: 'state',
|
|
108
|
+
common: {
|
|
109
|
+
name: `${v.name} Control Mode`,
|
|
110
|
+
type: 'number',
|
|
111
|
+
role: 'level.mode',
|
|
112
|
+
read: true,
|
|
113
|
+
write: true,
|
|
114
|
+
states: { 0: 'Off', 1: 'Manual On', 2: 'Auto' },
|
|
115
|
+
def: 2,
|
|
116
|
+
},
|
|
117
|
+
native: {},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await this.setObjectNotExistsAsync(`${channelId}.switchOnTime`, {
|
|
121
|
+
type: 'state',
|
|
122
|
+
common: {
|
|
123
|
+
name: `${v.name} Switch On Time (HH:MM)`,
|
|
124
|
+
type: 'string',
|
|
125
|
+
role: 'value.time',
|
|
126
|
+
read: true,
|
|
127
|
+
write: true,
|
|
128
|
+
def: '',
|
|
129
|
+
},
|
|
130
|
+
native: {},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await this.setObjectNotExistsAsync(`${channelId}.switchOffTime`, {
|
|
134
|
+
type: 'state',
|
|
135
|
+
common: {
|
|
136
|
+
name: `${v.name} Switch Off Time (HH:MM)`,
|
|
137
|
+
type: 'string',
|
|
138
|
+
role: 'value.time',
|
|
139
|
+
read: true,
|
|
140
|
+
write: true,
|
|
141
|
+
def: '',
|
|
142
|
+
},
|
|
143
|
+
native: {},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await this.setObjectNotExistsAsync(`${channelId}.alwaysOffAtTime`, {
|
|
147
|
+
type: 'state',
|
|
148
|
+
common: {
|
|
149
|
+
name: `${v.name} Ausschalten nur zur Ausschaltzeit`,
|
|
150
|
+
type: 'boolean',
|
|
151
|
+
role: 'switch',
|
|
152
|
+
read: true,
|
|
153
|
+
write: true,
|
|
154
|
+
def: v.alwaysOffAtTime || false,
|
|
155
|
+
},
|
|
156
|
+
native: {},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await this.setObjectNotExistsAsync(`${channelId}.performance`, {
|
|
160
|
+
type: 'state',
|
|
161
|
+
common: {
|
|
162
|
+
name: `${v.name} Gesamtleistung (Watt)`,
|
|
163
|
+
type: 'number',
|
|
164
|
+
role: 'value.power.consumption',
|
|
165
|
+
read: true,
|
|
166
|
+
write: false,
|
|
167
|
+
def: v.performance || 0,
|
|
168
|
+
unit: 'W',
|
|
169
|
+
},
|
|
170
|
+
native: {},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await this.setObjectNotExistsAsync(`${channelId}.switchOnPoint`, {
|
|
174
|
+
type: 'state',
|
|
175
|
+
common: {
|
|
176
|
+
name: `${v.name} Einschaltpunkt (Watt)`,
|
|
177
|
+
type: 'number',
|
|
178
|
+
role: 'value.power',
|
|
179
|
+
read: true,
|
|
180
|
+
write: false,
|
|
181
|
+
def: v.switchOnPoint || 0,
|
|
182
|
+
unit: 'W',
|
|
183
|
+
},
|
|
184
|
+
native: {},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await this.setObjectNotExistsAsync(`${channelId}.switchOffPoint`, {
|
|
188
|
+
type: 'state',
|
|
189
|
+
common: {
|
|
190
|
+
name: `${v.name} Abschaltpunkt (Watt)`,
|
|
191
|
+
type: 'number',
|
|
192
|
+
role: 'value.power',
|
|
193
|
+
read: true,
|
|
194
|
+
write: false,
|
|
195
|
+
def: v.switchOffPoint || 0,
|
|
196
|
+
unit: 'W',
|
|
197
|
+
},
|
|
198
|
+
native: {},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (v.ruletype === 'battery') {
|
|
202
|
+
await this.setObjectNotExistsAsync(`${channelId}.batterySetpoint`, {
|
|
203
|
+
type: 'state',
|
|
204
|
+
common: {
|
|
205
|
+
name: `${v.name} Batterie Ladeleistungs-Datenpunkt`,
|
|
206
|
+
type: 'string',
|
|
207
|
+
role: 'value',
|
|
208
|
+
read: true,
|
|
209
|
+
write: true,
|
|
210
|
+
def: v.batterySetpoint || '',
|
|
211
|
+
},
|
|
212
|
+
native: {},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await this.setObjectNotExistsAsync(`${channelId}.onTimerRemaining`, {
|
|
217
|
+
type: 'state',
|
|
218
|
+
common: {
|
|
219
|
+
name: `${v.name} Einschalt-Timer Restzeit (s)`,
|
|
220
|
+
type: 'number',
|
|
221
|
+
role: 'value.duration',
|
|
222
|
+
read: true,
|
|
223
|
+
write: false,
|
|
224
|
+
def: 0,
|
|
225
|
+
unit: 's',
|
|
226
|
+
},
|
|
227
|
+
native: {},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await this.setObjectNotExistsAsync(`${channelId}.offTimerRemaining`, {
|
|
231
|
+
type: 'state',
|
|
232
|
+
common: {
|
|
233
|
+
name: `${v.name} Ausschalt-Timer Restzeit (s)`,
|
|
234
|
+
type: 'number',
|
|
235
|
+
role: 'value.duration',
|
|
236
|
+
read: true,
|
|
237
|
+
write: false,
|
|
238
|
+
def: 0,
|
|
239
|
+
unit: 's',
|
|
240
|
+
},
|
|
241
|
+
native: {},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const finalOnTime = v.switchOnTime || '';
|
|
245
|
+
const finalOffTime = v.switchOffTime || '';
|
|
246
|
+
|
|
247
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.switchOnTime`, { val: finalOnTime, ack: true });
|
|
248
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.switchOffTime`, { val: finalOffTime, ack: true });
|
|
249
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.alwaysOffAtTime`, {
|
|
250
|
+
val: v.alwaysOffAtTime || false,
|
|
251
|
+
ack: true,
|
|
252
|
+
});
|
|
253
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.switchOffPoint`, {
|
|
254
|
+
val: v.switchOffPoint || 0,
|
|
255
|
+
ack: true,
|
|
256
|
+
});
|
|
257
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.switchOnPoint`, {
|
|
258
|
+
val: v.switchOnPoint || 0,
|
|
259
|
+
ack: true,
|
|
260
|
+
});
|
|
261
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.performance`, {
|
|
262
|
+
val: v.performance || 0,
|
|
263
|
+
ack: true,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.onTimerRemaining`, { val: 0, ack: true });
|
|
267
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.offTimerRemaining`, { val: 0, ack: true });
|
|
268
|
+
|
|
269
|
+
v.switchOnTime = finalOnTime;
|
|
270
|
+
v.switchOffTime = finalOffTime;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
timeWithinWindow(onTime, offTime, name) {
|
|
275
|
+
if (!onTime || !offTime) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const now = new Date();
|
|
280
|
+
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
281
|
+
const onMinutes = this.parseTimeToMinutes(onTime);
|
|
282
|
+
const offMinutes = this.parseTimeToMinutes(offTime);
|
|
283
|
+
|
|
284
|
+
const within = currentMinutes >= onMinutes && currentMinutes < offMinutes;
|
|
285
|
+
|
|
286
|
+
if (name) {
|
|
287
|
+
this.log.debug(
|
|
288
|
+
`🕒 ${name}: Zeitfenster ${onTime}–${offTime}, aktuell ${now.toTimeString().slice(0, 5)} (${currentMinutes} min) → innerhalb? ${within}`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return within;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async onStateChange(id, state) {
|
|
296
|
+
if (!state) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// FeedIn soll immer reagieren, egal ob ack true/false
|
|
301
|
+
if (id === this.feedInDatapoint) {
|
|
302
|
+
this.log.debug(`📊 FeedIn-Wert aktualisiert: ${state.val} (ack=${state.ack})`);
|
|
303
|
+
await this.checkConsumers();
|
|
304
|
+
|
|
305
|
+
if (this.percentTimer) {
|
|
306
|
+
clearTimeout(this.percentTimer);
|
|
307
|
+
}
|
|
308
|
+
this.percentTimer = setTimeout(
|
|
309
|
+
async () => {
|
|
310
|
+
for (const v of this.consumerList.filter(v => v.ruletype === 'percent')) {
|
|
311
|
+
await this.controlPercentConsumer(v);
|
|
312
|
+
}
|
|
313
|
+
this.percentTimer = null;
|
|
314
|
+
},
|
|
315
|
+
(this.config.delaySecondsProzent || 60) * 1000,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (this.batteryTimer) {
|
|
319
|
+
clearTimeout(this.batteryTimer);
|
|
320
|
+
}
|
|
321
|
+
this.batteryTimer = setTimeout(
|
|
322
|
+
async () => {
|
|
323
|
+
if (!this.feedInDatapoint) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const feedInState = await this.getForeignStateAsync(this.feedInDatapoint);
|
|
327
|
+
let feedIn = Number(feedInState?.val) || 0;
|
|
328
|
+
if (this.config.feedinNegativ) {
|
|
329
|
+
feedIn = -feedIn;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const v of this.consumerList.filter(v => v.ruletype === 'battery')) {
|
|
333
|
+
await this.controlBattery(v, feedIn);
|
|
334
|
+
}
|
|
335
|
+
this.batteryTimer = null;
|
|
336
|
+
},
|
|
337
|
+
(this.config.batteryDelaySeconds || 60) * 1000,
|
|
338
|
+
);
|
|
339
|
+
return; // WICHTIG: damit nicht auch der !state.ack-Teil ausgeführt wird
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Rest bleibt wie gehabt (nur bei ack=false)
|
|
343
|
+
if (state && !state.ack) {
|
|
344
|
+
this.log.debug(`State changed: ${id} => ${state.val}`);
|
|
345
|
+
|
|
346
|
+
const match = id.match(/consumer\.(\d+)_.*?\.controlMode/);
|
|
347
|
+
if (match) {
|
|
348
|
+
const index = parseInt(match[1]);
|
|
349
|
+
const mode = state.val;
|
|
350
|
+
const v = this.consumerList[index];
|
|
351
|
+
|
|
352
|
+
if (mode === 0) {
|
|
353
|
+
await this.switchConsumerWithDelay(v, false);
|
|
354
|
+
} else if (mode === 1) {
|
|
355
|
+
await this.switchConsumerWithDelay(v, true);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const timeMatch = id.match(/consumer\.(\d+)_.*?\.(switchOnTime|switchOffTime)/);
|
|
360
|
+
if (timeMatch) {
|
|
361
|
+
const index = parseInt(timeMatch[1]);
|
|
362
|
+
const type = timeMatch[2];
|
|
363
|
+
this.consumerList[index][type] = state.val || '';
|
|
364
|
+
await this.checkConsumers();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async checkConsumers() {
|
|
370
|
+
if (this.checkRunning) {
|
|
371
|
+
this.checkQueued = true;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
this.checkRunning = true;
|
|
375
|
+
this.checkQueued = false;
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
if (!this.feedInDatapoint) {
|
|
379
|
+
this.log.warn('❗ Kein FeedIn-Datenpunkt gesetzt – checkConsumers() wird übersprungen.');
|
|
380
|
+
await this.sendNotification(
|
|
381
|
+
'smartloadmanager',
|
|
382
|
+
'alert',
|
|
383
|
+
'❗ Kein FeedIn-Datenpunkt gesetzt – Regelung übersprungen.',
|
|
384
|
+
);
|
|
385
|
+
this.checkRunning = false;
|
|
386
|
+
if (this.checkQueued) {
|
|
387
|
+
await this.checkConsumers();
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const feedInState = await this.getForeignStateAsync(this.feedInDatapoint);
|
|
393
|
+
let feedIn = Number(feedInState?.val) || 0;
|
|
394
|
+
if (this.config.feedinNegativ) {
|
|
395
|
+
feedIn = -feedIn;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const baseload = this.config.baseload || 0;
|
|
399
|
+
const gridUsage = feedIn <= baseload;
|
|
400
|
+
|
|
401
|
+
this.log.debug(`📊 FeedIn: ${feedIn} W, Baseload: ${baseload} W, GridUsage: ${gridUsage}`);
|
|
402
|
+
|
|
403
|
+
// ----------------------------------
|
|
404
|
+
// Einschalten binary + heating
|
|
405
|
+
// ----------------------------------
|
|
406
|
+
const sortedOn = [...this.consumerList]
|
|
407
|
+
.filter(v => v.ruletype === 'binary' || v.ruletype === 'heating')
|
|
408
|
+
.sort((a, b) => (a.priority || 1) - (b.priority || 1));
|
|
409
|
+
|
|
410
|
+
this.log.debug(`🔍 Einschalt-Prüfung für ${sortedOn.length} binary/heating-Verbraucher.`);
|
|
411
|
+
|
|
412
|
+
for (const v of sortedOn) {
|
|
413
|
+
const idx = this.consumerList.indexOf(v);
|
|
414
|
+
const id = `${this.namespace}.consumer.${idx}_${v.name.replace(/\s+/g, '_')}`;
|
|
415
|
+
const mode = await this.getStateAsync(`${id}.controlMode`);
|
|
416
|
+
|
|
417
|
+
this.log.debug(`➡️ Prüfe "${v.name}" (Prio ${v.priority}, ${v.performance} W, Typ=${v.ruletype})`);
|
|
418
|
+
|
|
419
|
+
if (!mode || mode.val !== 2) {
|
|
420
|
+
this.log.debug(`⏭️ Manueller Modus aktiv, Überspringe.`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const withinWindow = this.timeWithinWindow(v.switchOnTime, v.switchOffTime, v.name);
|
|
425
|
+
|
|
426
|
+
let isOn = false;
|
|
427
|
+
if (v.datapoint) {
|
|
428
|
+
const dpState = await this.getForeignStateAsync(v.datapoint);
|
|
429
|
+
if (dpState?.val === true) {
|
|
430
|
+
isOn = true;
|
|
431
|
+
}
|
|
432
|
+
} else if (v.numericDatapoint) {
|
|
433
|
+
const numState = await this.getForeignStateAsync(v.numericDatapoint);
|
|
434
|
+
const numVal = Number(numState?.val) || 0;
|
|
435
|
+
if (numVal === 1) {
|
|
436
|
+
isOn = true;
|
|
437
|
+
}
|
|
438
|
+
this.log.debug(`🔢 NumericDatapoint für "${v.name}": ${v.numericDatapoint} = ${numVal}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.log.debug(`💡 "${v.name}" Status: isOn=${isOn}`);
|
|
442
|
+
|
|
443
|
+
// Zusatz-Checks heating
|
|
444
|
+
if (v.ruletype === 'heating' && withinWindow && !isOn) {
|
|
445
|
+
const tempState = v.temperatureDatapoint
|
|
446
|
+
? await this.getForeignStateAsync(v.temperatureDatapoint)
|
|
447
|
+
: null;
|
|
448
|
+
const currentTemp = tempState?.val !== undefined ? Number(tempState.val) : null;
|
|
449
|
+
|
|
450
|
+
if (currentTemp !== null && v.maxTemperature !== undefined && currentTemp >= v.maxTemperature) {
|
|
451
|
+
this.log.warn(
|
|
452
|
+
`🔥 "${v.name}" NICHT eingeschaltet – Temperatur ${currentTemp}°C >= MaxTemp ${v.maxTemperature}°C`,
|
|
453
|
+
);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (currentTemp !== null && v.minTemperature !== undefined && currentTemp < v.minTemperature) {
|
|
457
|
+
this.log.debug(
|
|
458
|
+
`🌡️ "${v.name}" noch nicht einschalten – Temperatur ${currentTemp}°C < MinTemp ${v.minTemperature}°C`,
|
|
459
|
+
);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (v.enableDatapoint) {
|
|
464
|
+
const enableState = await this.getForeignStateAsync(v.enableDatapoint);
|
|
465
|
+
if (!enableState?.val) {
|
|
466
|
+
this.log.debug(`🔒 "${v.name}" NICHT eingeschaltet – Hardwarefreigabe fehlt.`);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (v.heartbeatDatapoint) {
|
|
472
|
+
const hbState = await this.getForeignStateAsync(v.heartbeatDatapoint);
|
|
473
|
+
if (!hbState?.val) {
|
|
474
|
+
this.log.warn(`💔 "${v.name}" NICHT eingeschaltet – Heartbeat fehlt.`);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Timer-Abbruch bei wegfallender Bedingung (Einschalten)
|
|
481
|
+
if (
|
|
482
|
+
v.timer &&
|
|
483
|
+
v.timerEnd &&
|
|
484
|
+
v.pendingAction === true &&
|
|
485
|
+
(!withinWindow || gridUsage || v.performance > feedIn - baseload)
|
|
486
|
+
) {
|
|
487
|
+
clearTimeout(v.timer);
|
|
488
|
+
v.timer = null;
|
|
489
|
+
v.timerEnd = null;
|
|
490
|
+
v.pendingAction = null;
|
|
491
|
+
const channelId = `consumer.${idx}_${v.name.replace(/\s+/g, '_')}`;
|
|
492
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.onTimerRemaining`, { val: 0, ack: true });
|
|
493
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.offTimerRemaining`, { val: 0, ack: true });
|
|
494
|
+
this.log.debug(`⏹️ Einschalt-Timer für "${v.name}" abgebrochen (Bedingung entfallen).`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (withinWindow) {
|
|
498
|
+
if (!gridUsage && v.performance <= feedIn - baseload && !isOn) {
|
|
499
|
+
this.log.debug(
|
|
500
|
+
`✅ Einschalten möglich: Überschuss ${feedIn - baseload} W, innerhalb Zeitfenster (onDelay ${v.switchOnDelay || 0}s).`,
|
|
501
|
+
);
|
|
502
|
+
await this.switchConsumerWithDelay(v, true, v.switchOnDelay || 0);
|
|
503
|
+
feedIn -= v.performance;
|
|
504
|
+
this.log.debug(`🔄 Verbleibender Überschuss nach Zuschaltung: ${feedIn - baseload} W`);
|
|
505
|
+
} else if (gridUsage && !isOn) {
|
|
506
|
+
this.log.debug(`🚫 Nicht eingeschaltet: Netzbezug aktiv.`);
|
|
507
|
+
} else if (!isOn) {
|
|
508
|
+
this.log.debug(
|
|
509
|
+
`⚠️ Nicht eingeschaltet: Überschuss zu gering (${feedIn - baseload} W < ${v.performance} W).`,
|
|
510
|
+
);
|
|
511
|
+
} else {
|
|
512
|
+
this.log.debug(`ℹ️ "${v.name}" ist bereits eingeschaltet – keine Aktion nötig.`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ----------------------------------
|
|
518
|
+
// Ausschalten binary + heating
|
|
519
|
+
// ----------------------------------
|
|
520
|
+
const sortedOff = [...this.consumerList]
|
|
521
|
+
.filter(v => v.ruletype === 'binary' || v.ruletype === 'heating')
|
|
522
|
+
.sort((a, b) => (b.priority || 1) - (a.priority || 1));
|
|
523
|
+
|
|
524
|
+
this.log.debug(`🔍 Ausschalt-Prüfung für ${sortedOff.length} binary/heating-Verbraucher.`);
|
|
525
|
+
|
|
526
|
+
for (const v of sortedOff) {
|
|
527
|
+
const idx = this.consumerList.indexOf(v);
|
|
528
|
+
const id = `${this.namespace}.consumer.${idx}_${v.name.replace(/\s+/g, '_')}`;
|
|
529
|
+
const mode = await this.getStateAsync(`${id}.controlMode`);
|
|
530
|
+
if (!mode || mode.val !== 2) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const withinWindow = this.timeWithinWindow(v.switchOnTime, v.switchOffTime, v.name);
|
|
535
|
+
|
|
536
|
+
let isOn = false;
|
|
537
|
+
if (v.datapoint) {
|
|
538
|
+
const dpState = await this.getForeignStateAsync(v.datapoint);
|
|
539
|
+
if (dpState?.val === true) {
|
|
540
|
+
isOn = true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (v.numericDatapoint) {
|
|
544
|
+
const numState = await this.getForeignStateAsync(v.numericDatapoint);
|
|
545
|
+
const numVal = Number(numState?.val) || 0;
|
|
546
|
+
if (numVal === 1) {
|
|
547
|
+
isOn = true;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// AlwaysOffAtTime erzwingt Ausschalten
|
|
552
|
+
if (v.alwaysOffAtTime && !withinWindow && isOn) {
|
|
553
|
+
if (v.timer && v.timerEnd && v.pendingAction === false) {
|
|
554
|
+
const remaining = Math.max(0, Math.round((v.timerEnd - Date.now()) / 1000));
|
|
555
|
+
this.log.debug(`📋 Aktive Timer → ${v.name}: noch ${remaining}s`);
|
|
556
|
+
} else if (v.timer && v.timerEnd && v.pendingAction === true) {
|
|
557
|
+
clearTimeout(v.timer);
|
|
558
|
+
v.timer = null;
|
|
559
|
+
v.timerEnd = null;
|
|
560
|
+
v.pendingAction = null;
|
|
561
|
+
const channelId = `consumer.${idx}_${v.name.replace(/\s+/g, '_')}`;
|
|
562
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.onTimerRemaining`, {
|
|
563
|
+
val: 0,
|
|
564
|
+
ack: true,
|
|
565
|
+
});
|
|
566
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.offTimerRemaining`, {
|
|
567
|
+
val: 0,
|
|
568
|
+
ack: true,
|
|
569
|
+
});
|
|
570
|
+
this.log.debug(`⏹️ Einschalt-Timer für "${v.name}" abgebrochen (AlwaysOff aktiv).`);
|
|
571
|
+
this.log.debug(
|
|
572
|
+
`⏳ AlwaysOff aktiv: "${v.name}" - Zeitfenster vorbei, offDelay ${v.switchOffDelay || 0}s.`,
|
|
573
|
+
);
|
|
574
|
+
await this.switchConsumerWithDelay(v, false, v.switchOffDelay || 0);
|
|
575
|
+
} else {
|
|
576
|
+
this.log.debug(
|
|
577
|
+
`⏳ AlwaysOff aktiv: "${v.name}" - Zeitfenster vorbei, offDelay ${v.switchOffDelay || 0}s.`,
|
|
578
|
+
);
|
|
579
|
+
await this.switchConsumerWithDelay(v, false, v.switchOffDelay || 0);
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (v.ruletype === 'heating' && isOn) {
|
|
585
|
+
if (v.enableDatapoint) {
|
|
586
|
+
const enableState = await this.getForeignStateAsync(v.enableDatapoint);
|
|
587
|
+
if (!enableState?.val) {
|
|
588
|
+
this.log.warn(`🔒 "${v.name}" wird abgeschaltet – Hardwarefreigabe fehlt.`);
|
|
589
|
+
await this.switchConsumerWithDelay(v, false, 0);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (v.heartbeatDatapoint) {
|
|
594
|
+
const hbState = await this.getForeignStateAsync(v.heartbeatDatapoint);
|
|
595
|
+
if (!hbState?.val) {
|
|
596
|
+
this.log.warn(`💔 "${v.name}" wird abgeschaltet – Heartbeat fehlt.`);
|
|
597
|
+
await this.switchConsumerWithDelay(v, false, 0);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!isOn) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Timer-Abbruch bei wegfallender Bedingung (Ausschalten)
|
|
608
|
+
if (v.timer && v.timerEnd && v.pendingAction === false && withinWindow && !gridUsage) {
|
|
609
|
+
clearTimeout(v.timer);
|
|
610
|
+
v.timer = null;
|
|
611
|
+
v.timerEnd = null;
|
|
612
|
+
v.pendingAction = null;
|
|
613
|
+
const channelId = `consumer.${idx}_${v.name.replace(/\s+/g, '_')}`;
|
|
614
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.onTimerRemaining`, { val: 0, ack: true });
|
|
615
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.offTimerRemaining`, { val: 0, ack: true });
|
|
616
|
+
this.log.debug(`⏹️ Ausschalt-Timer für "${v.name}" abgebrochen (Bedingung entfallen).`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (!v.alwaysOffAtTime && (!withinWindow || gridUsage)) {
|
|
620
|
+
if (v.timer && v.timerEnd && v.pendingAction === false) {
|
|
621
|
+
const remaining = Math.max(0, Math.round((v.timerEnd - Date.now()) / 1000));
|
|
622
|
+
this.log.debug(`📋 Aktive Timer → ${v.name}: noch ${remaining}s`);
|
|
623
|
+
} else {
|
|
624
|
+
this.log.debug(`⏳ Ausschalten: "${v.name}" - offDelay ${v.switchOffDelay || 0}s.`);
|
|
625
|
+
await this.switchConsumerWithDelay(v, false, v.switchOffDelay || 0);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// --- Batterie bleibt unverändert ---
|
|
631
|
+
const batteryConsumers = this.consumerList.filter(c => c.ruletype === 'battery');
|
|
632
|
+
for (const v of batteryConsumers) {
|
|
633
|
+
await this.controlBattery(v, feedIn);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const runningTimers = this.consumerList
|
|
637
|
+
.filter(c => c.timer && c.timerEnd)
|
|
638
|
+
.map(c => {
|
|
639
|
+
const remaining = Math.max(0, Math.round((c.timerEnd - Date.now()) / 1000));
|
|
640
|
+
return `${c.name}: noch ${remaining}s`;
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
if (runningTimers.length > 0) {
|
|
644
|
+
this.log.debug(`📋 Aktive Timer → ${runningTimers.join(' | ')}`);
|
|
645
|
+
} else {
|
|
646
|
+
this.log.debug('📋 Keine aktiven Timer.');
|
|
647
|
+
}
|
|
648
|
+
} catch (err) {
|
|
649
|
+
this.log.error(`Error in checkConsumers: ${err.message}`);
|
|
650
|
+
await this.sendNotification('smartloadmanager', 'alert', `❌ Fehler in checkConsumers: ${err.message}`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
this.checkRunning = false;
|
|
654
|
+
if (this.checkQueued) {
|
|
655
|
+
await this.checkConsumers();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async controlPercentConsumer(v) {
|
|
660
|
+
try {
|
|
661
|
+
const withinWindow = this.timeWithinWindow(v.switchOnTime, v.switchOffTime);
|
|
662
|
+
if (!withinWindow) {
|
|
663
|
+
this.log.debug(`[Percent] ${v.name} außerhalb Zeitfenster – auf 0% setzen`);
|
|
664
|
+
await this.setForeignStateAsync(v.datapoint, 0);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (!this.feedInDatapoint) {
|
|
669
|
+
this.log.error('❌ feedInDatapoint ist nicht gesetzt!');
|
|
670
|
+
await this.sendNotification('smartloadmanager', 'alert', '❌ feedInDatapoint ist nicht gesetzt!');
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const feedInState = await this.getForeignStateAsync(this.feedInDatapoint);
|
|
674
|
+
|
|
675
|
+
const feedInValue = Number(feedInState?.val) || 0;
|
|
676
|
+
|
|
677
|
+
let surplus = this.config.feedinNegativ
|
|
678
|
+
? feedInValue < 0
|
|
679
|
+
? -feedInValue
|
|
680
|
+
: 0
|
|
681
|
+
: feedInValue > 0
|
|
682
|
+
? feedInValue
|
|
683
|
+
: 0;
|
|
684
|
+
|
|
685
|
+
surplus -= this.config.baseload || 0;
|
|
686
|
+
if (surplus < 0) {
|
|
687
|
+
surplus = 0;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const maxPerformance = v.maxPerformance || v.performance || 1000;
|
|
691
|
+
let newPercent = Math.round((surplus / maxPerformance) * 100);
|
|
692
|
+
newPercent = Math.min(100, Math.max(newPercent, v.minPercentStart || 0));
|
|
693
|
+
|
|
694
|
+
const state = await this.getForeignStateAsync(v.datapoint);
|
|
695
|
+
const currentPercent = Number(state?.val) || 0;
|
|
696
|
+
|
|
697
|
+
if (newPercent !== currentPercent) {
|
|
698
|
+
await this.setForeignStateAsync(v.datapoint, newPercent);
|
|
699
|
+
this.log.info(`🔁 ${v.name} wurde auf ${newPercent}% geregelt`);
|
|
700
|
+
|
|
701
|
+
if (this.config.notifyPercent) {
|
|
702
|
+
await this.sendNotification(
|
|
703
|
+
'smartloadmanager',
|
|
704
|
+
'notify',
|
|
705
|
+
`⚙️ ${v.name} wurde auf ${newPercent}% geregelt`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
this.log.debug(`[Percent] ${v.name} bleibt bei ${currentPercent}%`);
|
|
710
|
+
}
|
|
711
|
+
} catch (error) {
|
|
712
|
+
this.log.error(`❌ Fehler in controlPercentConsumer: ${error.message}`);
|
|
713
|
+
await this.sendNotification(
|
|
714
|
+
'smartloadmanager',
|
|
715
|
+
'alert',
|
|
716
|
+
`❌ Fehler in controlPercentConsumer: ${error.message}`,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async controlBattery(v, feedIn) {
|
|
722
|
+
try {
|
|
723
|
+
this.log.debug(`[Battery] Prüfung für ${v.name}`);
|
|
724
|
+
|
|
725
|
+
const withinWindow = this.timeWithinWindow(v.switchOnTime, v.switchOffTime);
|
|
726
|
+
if (!withinWindow) {
|
|
727
|
+
this.log.debug(`[Battery] ${v.name} außerhalb Zeitfenster – auf 0`);
|
|
728
|
+
if (v.batterySetpoint) {
|
|
729
|
+
await this.setForeignStateAsync(v.batterySetpoint, { val: 0, ack: true });
|
|
730
|
+
}
|
|
731
|
+
if (v.batteryControlModeDatapoint) {
|
|
732
|
+
await this.setForeignStateAsync(v.batteryControlModeDatapoint, { val: 0, ack: true });
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (!v.batterySetpoint) {
|
|
738
|
+
this.log.warn(`[Battery] ${v.name} hat keinen batterySetpoint`);
|
|
739
|
+
if (v.batteryControlModeDatapoint) {
|
|
740
|
+
await this.setForeignStateAsync(v.batteryControlModeDatapoint, { val: 0, ack: true });
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let soc = null;
|
|
746
|
+
let targetSoc = null;
|
|
747
|
+
if (v.batterySOC && v.batteryTargetSOC) {
|
|
748
|
+
const socState = await this.getForeignStateAsync(v.batterySOC);
|
|
749
|
+
const targetSocState = await this.getForeignStateAsync(v.batteryTargetSOC);
|
|
750
|
+
soc = Number(socState?.val);
|
|
751
|
+
targetSoc = Number(targetSocState?.val);
|
|
752
|
+
if (isNaN(soc) || isNaN(targetSoc)) {
|
|
753
|
+
this.log.warn(`[Battery] ${v.name}: Ungültiger SOC oder targetSOC`);
|
|
754
|
+
soc = null;
|
|
755
|
+
targetSoc = null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const surplus = (this.config.feedinNegativ ? -feedIn : feedIn) - (this.config.baseload || 0);
|
|
760
|
+
let powerToSet = 0;
|
|
761
|
+
let modeToSet = 0;
|
|
762
|
+
|
|
763
|
+
if (surplus <= 0) {
|
|
764
|
+
powerToSet = 0;
|
|
765
|
+
modeToSet = 0;
|
|
766
|
+
} else if (soc !== null && targetSoc !== null && soc >= targetSoc) {
|
|
767
|
+
powerToSet = 0;
|
|
768
|
+
modeToSet = 1;
|
|
769
|
+
} else {
|
|
770
|
+
powerToSet = Math.min(surplus, v.performance || 1000);
|
|
771
|
+
modeToSet = 2;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const oldPower = Number((await this.getForeignStateAsync(v.batterySetpoint))?.val) || 0;
|
|
775
|
+
const oldMode = this.batteryControlModeDatapoint
|
|
776
|
+
? Number((await this.getForeignStateAsync(this.batteryControlModeDatapoint))?.val) || 0
|
|
777
|
+
: -1;
|
|
778
|
+
|
|
779
|
+
if (powerToSet !== oldPower) {
|
|
780
|
+
await this.setForeignStateAsync(v.batterySetpoint, { val: powerToSet, ack: true });
|
|
781
|
+
this.log.info(`🔁 Batterie ${v.name}: Ladeleistung auf ${powerToSet}W gesetzt`);
|
|
782
|
+
if (this.config.notifyBattery) {
|
|
783
|
+
await this.sendNotification(
|
|
784
|
+
'smartloadmanager',
|
|
785
|
+
'notify',
|
|
786
|
+
`🔋 Batterie ${v.name}: Ladeleistung auf ${powerToSet}W gesetzt`,
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
this.log.debug(`[Battery] ${v.name} Ladeleistung bleibt bei ${oldPower}W`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (this.batteryControlModeDatapoint && modeToSet !== oldMode) {
|
|
794
|
+
await this.setForeignStateAsync(this.batteryControlModeDatapoint, { val: modeToSet, ack: true });
|
|
795
|
+
this.log.debug(`[Battery] ${v.name} Modus geändert: ${oldMode} → ${modeToSet}`);
|
|
796
|
+
if (this.config.notifyBattery) {
|
|
797
|
+
await this.sendNotification(
|
|
798
|
+
'smartloadmanager',
|
|
799
|
+
'notify',
|
|
800
|
+
`🔋 Batterie ${v.name}: Modus geändert auf ${modeToSet}`,
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
} catch (error) {
|
|
805
|
+
this.log.error(`[Battery] Fehler für ${v.name}: ${error.message}`);
|
|
806
|
+
await this.sendNotification(
|
|
807
|
+
'smartloadmanager',
|
|
808
|
+
'alert',
|
|
809
|
+
`❌ Batterie-Fehler für ${v.name}: ${error.message}`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Zentrale Schaltfunktion: setzt sowohl booleanen Schalter-DP als auch optionalen numerischen DP (0/1)
|
|
816
|
+
*
|
|
817
|
+
* @param consumer - Der Verbraucher, der geschaltet werden soll
|
|
818
|
+
* @param turnOn - true zum Einschalten, false zum Ausschalten
|
|
819
|
+
*/
|
|
820
|
+
async doSwitchConsumer(consumer, turnOn) {
|
|
821
|
+
const targetState = !!turnOn;
|
|
822
|
+
|
|
823
|
+
if (consumer.datapoint) {
|
|
824
|
+
await this.setForeignStateAsync(consumer.datapoint, { val: targetState, ack: true });
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (consumer.numericDatapoint) {
|
|
828
|
+
await this.setForeignStateAsync(consumer.numericDatapoint, { val: targetState ? 1 : 0, ack: true });
|
|
829
|
+
this.log.debug(`🔢 NumericDatapoint für "${consumer.name}" gesetzt auf ${targetState ? 1 : 0}`);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const msg = `${consumer.name} wurde ${targetState ? 'eingeschaltet' : 'ausgeschaltet'}`;
|
|
833
|
+
|
|
834
|
+
if (consumer.ruletype === 'heating') {
|
|
835
|
+
if (this.config.notifyHeating) {
|
|
836
|
+
await this.sendNotification('smartloadmanager', 'notify', msg);
|
|
837
|
+
}
|
|
838
|
+
} else if (consumer.ruletype === 'binary') {
|
|
839
|
+
if (this.config.notifyBinary) {
|
|
840
|
+
await this.sendNotification('smartloadmanager', 'notify', msg);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async switchConsumerWithDelay(consumer, turnOn, delaySeconds) {
|
|
846
|
+
const delay =
|
|
847
|
+
delaySeconds !== undefined
|
|
848
|
+
? delaySeconds
|
|
849
|
+
: turnOn
|
|
850
|
+
? consumer.switchOnDelay || 0
|
|
851
|
+
: consumer.switchOffDelay || 0;
|
|
852
|
+
|
|
853
|
+
const idx = this.consumerList.indexOf(consumer);
|
|
854
|
+
const channelId = `consumer.${idx}_${consumer.name.replace(/\s+/g, '_')}`;
|
|
855
|
+
const onTimerState = `${this.namespace}.${channelId}.onTimerRemaining`;
|
|
856
|
+
const offTimerState = `${this.namespace}.${channelId}.offTimerRemaining`;
|
|
857
|
+
|
|
858
|
+
const stopTimerTicker = async () => {
|
|
859
|
+
if (consumer.timerTick) {
|
|
860
|
+
clearInterval(consumer.timerTick);
|
|
861
|
+
consumer.timerTick = null;
|
|
862
|
+
}
|
|
863
|
+
await this.setStateAsync(onTimerState, { val: 0, ack: true });
|
|
864
|
+
await this.setStateAsync(offTimerState, { val: 0, ack: true });
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
const startTimerTicker = async () => {
|
|
868
|
+
const tick = async () => {
|
|
869
|
+
if (
|
|
870
|
+
!consumer.timer ||
|
|
871
|
+
!consumer.timerEnd ||
|
|
872
|
+
consumer.pendingAction === undefined ||
|
|
873
|
+
consumer.pendingAction === null
|
|
874
|
+
) {
|
|
875
|
+
await stopTimerTicker();
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const remaining = Math.max(0, Math.round((consumer.timerEnd - Date.now()) / 1000));
|
|
879
|
+
if (consumer.pendingAction) {
|
|
880
|
+
await this.setStateAsync(onTimerState, { val: remaining, ack: true });
|
|
881
|
+
await this.setStateAsync(offTimerState, { val: 0, ack: true });
|
|
882
|
+
} else {
|
|
883
|
+
await this.setStateAsync(offTimerState, { val: remaining, ack: true });
|
|
884
|
+
await this.setStateAsync(onTimerState, { val: 0, ack: true });
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
await tick();
|
|
888
|
+
if (consumer.timerTick) {
|
|
889
|
+
clearInterval(consumer.timerTick);
|
|
890
|
+
consumer.timerTick = null;
|
|
891
|
+
}
|
|
892
|
+
consumer.timerTick = setInterval(tick, 1000);
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const doSwitch = async () => {
|
|
896
|
+
// nur bei heating: zusätzliche Sicherheitschecks
|
|
897
|
+
if (consumer.ruletype === 'heating' && turnOn) {
|
|
898
|
+
const tempState = consumer.temperatureDatapoint
|
|
899
|
+
? await this.getForeignStateAsync(consumer.temperatureDatapoint)
|
|
900
|
+
: null;
|
|
901
|
+
const currentTemp = tempState?.val !== undefined ? Number(tempState.val) : null;
|
|
902
|
+
|
|
903
|
+
if (
|
|
904
|
+
currentTemp !== null &&
|
|
905
|
+
consumer.maxTemperature !== undefined &&
|
|
906
|
+
currentTemp >= consumer.maxTemperature
|
|
907
|
+
) {
|
|
908
|
+
consumer.timer = null;
|
|
909
|
+
consumer.timerEnd = null;
|
|
910
|
+
consumer.pendingAction = null;
|
|
911
|
+
await stopTimerTicker();
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (
|
|
916
|
+
consumer.minTemperature !== undefined &&
|
|
917
|
+
currentTemp !== null &&
|
|
918
|
+
currentTemp < consumer.minTemperature
|
|
919
|
+
) {
|
|
920
|
+
consumer.timer = null;
|
|
921
|
+
consumer.timerEnd = null;
|
|
922
|
+
consumer.pendingAction = null;
|
|
923
|
+
await stopTimerTicker();
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (consumer.enableDatapoint) {
|
|
928
|
+
const enableState = await this.getForeignStateAsync(consumer.enableDatapoint);
|
|
929
|
+
if (!enableState?.val) {
|
|
930
|
+
consumer.timer = null;
|
|
931
|
+
consumer.timerEnd = null;
|
|
932
|
+
consumer.pendingAction = null;
|
|
933
|
+
await stopTimerTicker();
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (consumer.heartbeatDatapoint) {
|
|
939
|
+
const hbState = await this.getForeignStateAsync(consumer.heartbeatDatapoint);
|
|
940
|
+
if (!hbState?.val) {
|
|
941
|
+
consumer.timer = null;
|
|
942
|
+
consumer.timerEnd = null;
|
|
943
|
+
consumer.pendingAction = null;
|
|
944
|
+
await stopTimerTicker();
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
await this.doSwitchConsumer(consumer, turnOn);
|
|
951
|
+
if (consumer.timer) {
|
|
952
|
+
clearTimeout(consumer.timer);
|
|
953
|
+
}
|
|
954
|
+
consumer.timer = null;
|
|
955
|
+
consumer.timerEnd = null;
|
|
956
|
+
consumer.pendingAction = null;
|
|
957
|
+
await stopTimerTicker();
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// 🟢 Hier ist der Fix:
|
|
961
|
+
// Falls Timer läuft, aber Bedingung sich geändert hat → Timer abbrechen
|
|
962
|
+
if (consumer.timer) {
|
|
963
|
+
// wenn Einschalt-Timer läuft, aber turnOn nicht mehr gilt → abbrechen
|
|
964
|
+
if (consumer.pendingAction === true && !turnOn) {
|
|
965
|
+
clearTimeout(consumer.timer);
|
|
966
|
+
consumer.timer = null;
|
|
967
|
+
consumer.timerEnd = null;
|
|
968
|
+
consumer.pendingAction = null;
|
|
969
|
+
await stopTimerTicker();
|
|
970
|
+
return; // Timer beendet, keine neue Aktion starten
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// wenn Ausschalt-Timer läuft, aber turnOn wieder gilt → abbrechen
|
|
974
|
+
if (consumer.pendingAction === false && turnOn) {
|
|
975
|
+
clearTimeout(consumer.timer);
|
|
976
|
+
consumer.timer = null;
|
|
977
|
+
consumer.timerEnd = null;
|
|
978
|
+
consumer.pendingAction = null;
|
|
979
|
+
await stopTimerTicker();
|
|
980
|
+
return; // Timer beendet, keine neue Aktion starten
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// wenn gleiches Ziel wie bisher → nichts neu starten
|
|
984
|
+
if (consumer.pendingAction === turnOn) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ansonsten Timer neu setzen
|
|
989
|
+
clearTimeout(consumer.timer);
|
|
990
|
+
consumer.timer = null;
|
|
991
|
+
consumer.timerEnd = null;
|
|
992
|
+
consumer.pendingAction = null;
|
|
993
|
+
await stopTimerTicker();
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (delay > 0) {
|
|
997
|
+
consumer.timerEnd = Date.now() + delay * 1000;
|
|
998
|
+
consumer.pendingAction = turnOn;
|
|
999
|
+
consumer.timer = setTimeout(doSwitch, delay * 1000);
|
|
1000
|
+
await startTimerTicker();
|
|
1001
|
+
} else {
|
|
1002
|
+
await doSwitch();
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Zentrale, einheitliche Notification-Funktion
|
|
1007
|
+
// Aufruf: await this.sendNotification('smartloadmanager', 'info'|'notify'|'alert', 'Nachricht');
|
|
1008
|
+
async sendNotification(scope, category, message) {
|
|
1009
|
+
try {
|
|
1010
|
+
const cfg = this.config;
|
|
1011
|
+
|
|
1012
|
+
// 1) Controller-Notification (nur wenn Option aktiv ist)
|
|
1013
|
+
if (cfg.enableNotificationManager) {
|
|
1014
|
+
await this.registerNotification(scope, category, message);
|
|
1015
|
+
this.log.debug(`📨 Controller-Notification [${scope}/${category}]: ${message}`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// 2) Messenger-Nachrichten
|
|
1019
|
+
|
|
1020
|
+
// Telegram
|
|
1021
|
+
if (cfg.telegramInstance) {
|
|
1022
|
+
const recipients = (cfg.telegramRecipients || '')
|
|
1023
|
+
.split(',')
|
|
1024
|
+
.map(r => r.trim())
|
|
1025
|
+
.filter(Boolean);
|
|
1026
|
+
if (recipients.length > 0) {
|
|
1027
|
+
recipients.forEach(user => {
|
|
1028
|
+
this.sendTo(cfg.telegramInstance, { text: message, user });
|
|
1029
|
+
this.log.debug(`📨 Telegram → ${cfg.telegramInstance}/${user}: ${message}`);
|
|
1030
|
+
});
|
|
1031
|
+
} else {
|
|
1032
|
+
this.sendTo(cfg.telegramInstance, message);
|
|
1033
|
+
this.log.debug(`📨 Telegram → ${cfg.telegramInstance}: ${message}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Gotify (hat keine Benutzer, nur Instanz)
|
|
1038
|
+
if (cfg.gotifyInstance) {
|
|
1039
|
+
this.sendTo(cfg.gotifyInstance, {
|
|
1040
|
+
title: 'SmartLoadManager',
|
|
1041
|
+
message: message,
|
|
1042
|
+
});
|
|
1043
|
+
this.log.debug(`📨 Gotify → ${cfg.gotifyInstance}: ${message}`);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// E-Mail
|
|
1047
|
+
if (cfg.emailInstance) {
|
|
1048
|
+
const recipients = (cfg.emailRecipients || '')
|
|
1049
|
+
.split(',')
|
|
1050
|
+
.map(r => r.trim())
|
|
1051
|
+
.filter(Boolean);
|
|
1052
|
+
if (recipients.length > 0) {
|
|
1053
|
+
recipients.forEach(to => {
|
|
1054
|
+
this.sendTo(cfg.emailInstance, {
|
|
1055
|
+
to,
|
|
1056
|
+
text: message,
|
|
1057
|
+
subject: 'SmartLoadManager',
|
|
1058
|
+
});
|
|
1059
|
+
this.log.debug(`📨 Email → ${cfg.emailInstance}/${to}: ${message}`);
|
|
1060
|
+
});
|
|
1061
|
+
} else {
|
|
1062
|
+
this.sendTo(cfg.emailInstance, {
|
|
1063
|
+
text: message,
|
|
1064
|
+
subject: 'SmartLoadManager',
|
|
1065
|
+
});
|
|
1066
|
+
this.log.debug(`📨 Email → ${cfg.emailInstance}: ${message}`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// WhatsApp-cmd
|
|
1071
|
+
if (cfg.whatsappInstance) {
|
|
1072
|
+
const recipients = (cfg.whatsappRecipients || '')
|
|
1073
|
+
.split(',')
|
|
1074
|
+
.map(r => r.trim())
|
|
1075
|
+
.filter(Boolean);
|
|
1076
|
+
if (recipients.length > 0) {
|
|
1077
|
+
recipients.forEach(user => {
|
|
1078
|
+
this.sendTo(cfg.whatsappInstance, { text: message, user });
|
|
1079
|
+
this.log.debug(`📨 WhatsApp → ${cfg.whatsappInstance}/${user}: ${message}`);
|
|
1080
|
+
});
|
|
1081
|
+
} else {
|
|
1082
|
+
this.sendTo(cfg.whatsappInstance, { text: message });
|
|
1083
|
+
this.log.debug(`📨 WhatsApp → ${cfg.whatsappInstance}: ${message}`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Signal-cmd
|
|
1088
|
+
if (cfg.signalInstance) {
|
|
1089
|
+
const recipients = (cfg.signalRecipients || '')
|
|
1090
|
+
.split(',')
|
|
1091
|
+
.map(r => r.trim())
|
|
1092
|
+
.filter(Boolean);
|
|
1093
|
+
if (recipients.length > 0) {
|
|
1094
|
+
recipients.forEach(user => {
|
|
1095
|
+
this.sendTo(cfg.signalInstance, { text: message, user });
|
|
1096
|
+
this.log.debug(`📨 Signal → ${cfg.signalInstance}/${user}: ${message}`);
|
|
1097
|
+
});
|
|
1098
|
+
} else {
|
|
1099
|
+
this.sendTo(cfg.signalInstance, { text: message });
|
|
1100
|
+
this.log.debug(`📨 Signal → ${cfg.signalInstance}: ${message}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Pushover
|
|
1105
|
+
if (cfg.pushoverInstance) {
|
|
1106
|
+
const recipients = (cfg.pushoverRecipients || '')
|
|
1107
|
+
.split(',')
|
|
1108
|
+
.map(r => r.trim())
|
|
1109
|
+
.filter(Boolean);
|
|
1110
|
+
if (recipients.length > 0) {
|
|
1111
|
+
recipients.forEach(user => {
|
|
1112
|
+
this.sendTo(cfg.pushoverInstance, {
|
|
1113
|
+
message: message,
|
|
1114
|
+
title: 'SmartLoadManager',
|
|
1115
|
+
user,
|
|
1116
|
+
});
|
|
1117
|
+
this.log.debug(`📨 Pushover → ${cfg.pushoverInstance}/${user}: ${message}`);
|
|
1118
|
+
});
|
|
1119
|
+
} else {
|
|
1120
|
+
this.sendTo(cfg.pushoverInstance, {
|
|
1121
|
+
message: message,
|
|
1122
|
+
title: 'SmartLoadManager',
|
|
1123
|
+
});
|
|
1124
|
+
this.log.debug(`📨 Pushover → ${cfg.pushoverInstance}: ${message}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
} catch (e) {
|
|
1128
|
+
this.log.warn(`❗ Fehler beim Senden der Notification: ${e.message}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async initializeConsumerStatus() {
|
|
1133
|
+
// Initiale Status-Updates
|
|
1134
|
+
for (const v of this.consumerList) {
|
|
1135
|
+
const channelId = `consumer.${this.consumerList.indexOf(v)}_${v.name.replace(/\s+/g, '_')}`;
|
|
1136
|
+
const modeState = await this.getStateAsync(`${this.namespace}.${channelId}.controlMode`);
|
|
1137
|
+
if (!modeState) {
|
|
1138
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.controlMode`, {
|
|
1139
|
+
val: 2,
|
|
1140
|
+
ack: true,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
sleep(ms) {
|
|
1147
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
parseTimeToMinutes(timeStr) {
|
|
1151
|
+
if (!timeStr || typeof timeStr !== 'string' || !timeStr.includes(':')) {
|
|
1152
|
+
return 0;
|
|
1153
|
+
}
|
|
1154
|
+
const [h, m] = timeStr.split(':').map(Number);
|
|
1155
|
+
if (isNaN(h) || isNaN(m)) {
|
|
1156
|
+
return 0;
|
|
1157
|
+
}
|
|
1158
|
+
return h * 60 + m;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
async onUnload(callback) {
|
|
1162
|
+
try {
|
|
1163
|
+
if (this.percentTimer) {
|
|
1164
|
+
clearTimeout(this.percentTimer);
|
|
1165
|
+
}
|
|
1166
|
+
if (this.batteryTimer) {
|
|
1167
|
+
clearTimeout(this.batteryTimer);
|
|
1168
|
+
}
|
|
1169
|
+
for (const v of this.consumerList) {
|
|
1170
|
+
if (v.timer) {
|
|
1171
|
+
clearTimeout(v.timer);
|
|
1172
|
+
v.timer = null;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
callback();
|
|
1176
|
+
} catch {
|
|
1177
|
+
callback();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (require.main !== module) {
|
|
1183
|
+
module.exports = options => new ZeroFeedIn(options);
|
|
1184
|
+
} else {
|
|
1185
|
+
new ZeroFeedIn();
|
|
1186
|
+
}
|