iobroker.smartloadmanager 0.0.8 → 0.1.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 +1 -1
- package/README.md +9 -11
- package/admin/i18n/de/translations.json +104 -90
- package/admin/i18n/en/translations.json +10 -2
- package/admin/i18n/es/translations.json +104 -90
- package/admin/i18n/fr/translations.json +104 -90
- package/admin/i18n/it/translations.json +104 -90
- package/admin/i18n/nl/translations.json +104 -90
- package/admin/i18n/pl/translations.json +104 -90
- package/admin/i18n/pt/translations.json +104 -90
- package/admin/i18n/ru/translations.json +104 -90
- package/admin/i18n/uk/translations.json +104 -90
- package/admin/i18n/zh-cn/translations.json +104 -90
- package/admin/jsonConfig.json +117 -40
- package/io-package.json +49 -2
- package/main.js +413 -9
- package/package.json +11 -11
package/main.js
CHANGED
|
@@ -18,10 +18,21 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
18
18
|
this.batteryTimer = null;
|
|
19
19
|
this.checkRunning = false;
|
|
20
20
|
|
|
21
|
+
// 🔮 Forecast (PV forecast integration)
|
|
22
|
+
this.forecastEnabled = false;
|
|
23
|
+
this.forecastJsonDatapoint = null;
|
|
24
|
+
this.forecast = {
|
|
25
|
+
power: 0,
|
|
26
|
+
surplus: 0,
|
|
27
|
+
minutes: 0,
|
|
28
|
+
lastUpdate: 0,
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
this.on('ready', this.onReady.bind(this));
|
|
22
32
|
this.on('stateChange', this.onStateChange.bind(this));
|
|
23
33
|
this.on('unload', this.onUnload.bind(this));
|
|
24
34
|
}
|
|
35
|
+
|
|
25
36
|
name2id(pName) {
|
|
26
37
|
return (pName || '')
|
|
27
38
|
.replace(this.FORBIDDEN_CHARS, '_') // ioBroker verbotene Zeichen
|
|
@@ -31,6 +42,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
31
42
|
.replace(/_+/g, '_') // mehrere Unterstriche zusammenfassen
|
|
32
43
|
.replace(/^_+|_+$/g, ''); // führende/trailing _ weg
|
|
33
44
|
}
|
|
45
|
+
|
|
34
46
|
// Wrapper für sichere Timer
|
|
35
47
|
safeSetTimeout(fn, ms, ...args) {
|
|
36
48
|
const safeMs = Math.min(Math.max(ms, 0), this.MAX_TIMEOUT);
|
|
@@ -48,6 +60,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
48
60
|
return this.setInterval(fn, safeMs, ...args);
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
// =====================================================================
|
|
64
|
+
// ============================ onReady ================================
|
|
65
|
+
// =====================================================================
|
|
51
66
|
async onReady() {
|
|
52
67
|
try {
|
|
53
68
|
this.log.info(`Adapter started, PID: ${process.pid}`);
|
|
@@ -63,21 +78,40 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
63
78
|
this.batteryControlModeDatapoint = this.config.batteryControlModeDatapoint || null;
|
|
64
79
|
this.log.info(`Configured batteryControlModeDatapoint: ${this.batteryControlModeDatapoint}`);
|
|
65
80
|
|
|
81
|
+
// 🔮 Forecast config (PV forecast integration)
|
|
82
|
+
this.forecastEnabled = !!this.config.enableForecast;
|
|
83
|
+
this.forecastJsonDatapoint = this.config.forecastJsonDatapoint || null;
|
|
84
|
+
|
|
66
85
|
this.consumerList = Array.isArray(this.config.consumer)
|
|
67
86
|
? this.config.consumer.filter(
|
|
68
87
|
v =>
|
|
69
|
-
v &&
|
|
70
|
-
v.enabled &&
|
|
71
|
-
(v.ruletype === 'battery' || ((v.datapoint || v.numericDatapoint) && v.performance > 0)),
|
|
88
|
+
v && (v.ruletype === 'battery' || ((v.datapoint || v.numericDatapoint) && v.performance > 0)),
|
|
72
89
|
)
|
|
73
90
|
: [];
|
|
74
91
|
|
|
75
92
|
this.consumerList.forEach(v => (v.processingLockSwitch = false));
|
|
76
93
|
|
|
94
|
+
for (let i = 0; i < this.consumerList.length; i++) {
|
|
95
|
+
const v = this.consumerList[i];
|
|
96
|
+
const safeName = this.name2id(v.name);
|
|
97
|
+
const id = `${this.namespace}.consumer.${i}_${safeName}.active`;
|
|
98
|
+
const state = await this.getStateAsync(id);
|
|
99
|
+
if (state && state.val !== null) {
|
|
100
|
+
v.enabled = state.val;
|
|
101
|
+
this.log.debug(`"${v.name}": Active status taken from object tree (${state.val})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
77
105
|
this.log.info(`Loaded consumers: ${this.consumerList.map(v => v.name).join(', ')}`);
|
|
78
106
|
|
|
79
107
|
await this.checkAndCreateConsumerObjects();
|
|
80
108
|
await this.createConsumerStates();
|
|
109
|
+
|
|
110
|
+
// 🔮 Forecast states (adapter-internal)
|
|
111
|
+
if (this.forecastEnabled) {
|
|
112
|
+
await this.createForecastStates();
|
|
113
|
+
}
|
|
114
|
+
|
|
81
115
|
await this.initializeConsumerStatus();
|
|
82
116
|
await this.subscribeStatesAsync('*');
|
|
83
117
|
await this.subscribeForeignStatesAsync(this.feedInDatapoint);
|
|
@@ -85,6 +119,12 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
85
119
|
await this.subscribeForeignStatesAsync(this.batteryControlModeDatapoint);
|
|
86
120
|
}
|
|
87
121
|
|
|
122
|
+
// 🔮 Forecast subscribe + initial parse
|
|
123
|
+
if (this.forecastEnabled && this.forecastJsonDatapoint) {
|
|
124
|
+
await this.subscribeForeignStatesAsync(this.forecastJsonDatapoint);
|
|
125
|
+
await this.processPvforecastJson();
|
|
126
|
+
}
|
|
127
|
+
|
|
88
128
|
await this.checkConsumers();
|
|
89
129
|
} catch (error) {
|
|
90
130
|
this.log.error(`Error in onReady: ${error.message}`);
|
|
@@ -93,6 +133,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
93
133
|
}
|
|
94
134
|
}
|
|
95
135
|
|
|
136
|
+
// =====================================================================
|
|
137
|
+
// ===================== Consumer object checks ========================
|
|
138
|
+
// =====================================================================
|
|
96
139
|
async checkAndCreateConsumerObjects() {
|
|
97
140
|
for (const v of this.consumerList) {
|
|
98
141
|
if (v.datapoint) {
|
|
@@ -110,6 +153,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
110
153
|
}
|
|
111
154
|
}
|
|
112
155
|
|
|
156
|
+
// =====================================================================
|
|
157
|
+
// ========================= createConsumerStates ======================
|
|
158
|
+
// =====================================================================
|
|
113
159
|
async createConsumerStates() {
|
|
114
160
|
// Übergeordnetes Device erstellen - löst "intermediate objects missing" Problem
|
|
115
161
|
await this.setObjectNotExistsAsync('consumer', {
|
|
@@ -227,6 +273,61 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
227
273
|
native: {},
|
|
228
274
|
});
|
|
229
275
|
|
|
276
|
+
// === NEUER STATE: Aktiv-Status ============================
|
|
277
|
+
await this.setObjectNotExistsAsync(`${channelId}.active`, {
|
|
278
|
+
type: 'state',
|
|
279
|
+
common: {
|
|
280
|
+
name: `${v.name} aktiv`,
|
|
281
|
+
type: 'boolean',
|
|
282
|
+
role: 'switch',
|
|
283
|
+
read: true,
|
|
284
|
+
write: true,
|
|
285
|
+
def: v.enabled || false,
|
|
286
|
+
},
|
|
287
|
+
native: {},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// 🔮 Forecast options per consumer
|
|
291
|
+
await this.setObjectNotExistsAsync(`${channelId}.useForecast`, {
|
|
292
|
+
type: 'state',
|
|
293
|
+
common: {
|
|
294
|
+
name: 'Use forecast',
|
|
295
|
+
type: 'boolean',
|
|
296
|
+
role: 'switch',
|
|
297
|
+
read: true,
|
|
298
|
+
write: true,
|
|
299
|
+
def: v.useForecast || false,
|
|
300
|
+
},
|
|
301
|
+
native: {},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await this.setObjectNotExistsAsync(`${channelId}.forecastMinMinutes`, {
|
|
305
|
+
type: 'state',
|
|
306
|
+
common: {
|
|
307
|
+
name: 'Forecast min minutes',
|
|
308
|
+
type: 'number',
|
|
309
|
+
role: 'value.interval',
|
|
310
|
+
read: true,
|
|
311
|
+
write: true,
|
|
312
|
+
def: v.forecastMinMinutes || 0,
|
|
313
|
+
unit: 'min',
|
|
314
|
+
},
|
|
315
|
+
native: {},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await this.setObjectNotExistsAsync(`${channelId}.forecastStatus`, {
|
|
319
|
+
type: 'state',
|
|
320
|
+
common: {
|
|
321
|
+
name: 'Forecast status',
|
|
322
|
+
type: 'string',
|
|
323
|
+
role: 'text',
|
|
324
|
+
read: true,
|
|
325
|
+
write: false,
|
|
326
|
+
def: '',
|
|
327
|
+
},
|
|
328
|
+
native: {},
|
|
329
|
+
});
|
|
330
|
+
|
|
230
331
|
if (v.ruletype === 'battery') {
|
|
231
332
|
await this.setObjectNotExistsAsync(`${channelId}.batterySetpoint`, {
|
|
232
333
|
type: 'state',
|
|
@@ -303,11 +404,228 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
303
404
|
await this.setStateAsync(`${this.namespace}.${channelId}.onTimerRemaining`, { val: 0, ack: true });
|
|
304
405
|
await this.setStateAsync(`${this.namespace}.${channelId}.offTimerRemaining`, { val: 0, ack: true });
|
|
305
406
|
|
|
407
|
+
await this.setStateAsync(`${channelId}.active`, { val: v.enabled || false, ack: true });
|
|
408
|
+
|
|
409
|
+
// 🔮 Forecast states init per consumer
|
|
410
|
+
await this.setStateAsync(`${channelId}.useForecast`, { val: v.useForecast || false, ack: true });
|
|
411
|
+
await this.setStateAsync(`${channelId}.forecastMinMinutes`, {
|
|
412
|
+
val: v.forecastMinMinutes || 0,
|
|
413
|
+
ack: true,
|
|
414
|
+
});
|
|
415
|
+
await this.setStateAsync(`${channelId}.forecastStatus`, { val: '', ack: true });
|
|
416
|
+
|
|
306
417
|
v.switchOnTime = finalOnTime;
|
|
307
418
|
v.switchOffTime = finalOffTime;
|
|
308
419
|
}
|
|
309
420
|
}
|
|
310
421
|
|
|
422
|
+
// =====================================================================
|
|
423
|
+
// ========================= Forecast states ===========================
|
|
424
|
+
// =====================================================================
|
|
425
|
+
async createForecastStates() {
|
|
426
|
+
await this.setObjectNotExistsAsync('forecast', {
|
|
427
|
+
type: 'channel',
|
|
428
|
+
common: { name: 'Forecast' },
|
|
429
|
+
native: {},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await this.setObjectNotExistsAsync('forecast.power', {
|
|
433
|
+
type: 'state',
|
|
434
|
+
common: {
|
|
435
|
+
name: 'Forecast Power',
|
|
436
|
+
type: 'number',
|
|
437
|
+
role: 'value.power',
|
|
438
|
+
read: true,
|
|
439
|
+
write: false,
|
|
440
|
+
unit: 'W',
|
|
441
|
+
def: 0,
|
|
442
|
+
},
|
|
443
|
+
native: {},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
await this.setObjectNotExistsAsync('forecast.surplus', {
|
|
447
|
+
type: 'state',
|
|
448
|
+
common: {
|
|
449
|
+
name: 'Forecast Surplus',
|
|
450
|
+
type: 'number',
|
|
451
|
+
role: 'value.power',
|
|
452
|
+
read: true,
|
|
453
|
+
write: false,
|
|
454
|
+
unit: 'W',
|
|
455
|
+
def: 0,
|
|
456
|
+
},
|
|
457
|
+
native: {},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
await this.setObjectNotExistsAsync('forecast.minutes', {
|
|
461
|
+
type: 'state',
|
|
462
|
+
common: {
|
|
463
|
+
name: 'Forecast Minutes',
|
|
464
|
+
type: 'number',
|
|
465
|
+
role: 'value.interval',
|
|
466
|
+
read: true,
|
|
467
|
+
write: false,
|
|
468
|
+
unit: 'min',
|
|
469
|
+
def: 0,
|
|
470
|
+
},
|
|
471
|
+
native: {},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await this.setObjectNotExistsAsync('forecast.lastUpdate', {
|
|
475
|
+
type: 'state',
|
|
476
|
+
common: {
|
|
477
|
+
name: 'Forecast Last Update',
|
|
478
|
+
type: 'number',
|
|
479
|
+
role: 'value.time',
|
|
480
|
+
read: true,
|
|
481
|
+
write: false,
|
|
482
|
+
def: 0,
|
|
483
|
+
},
|
|
484
|
+
native: {},
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// =====================================================================
|
|
489
|
+
// ========================= Forecast JSON parse =======================
|
|
490
|
+
// =====================================================================
|
|
491
|
+
async processPvforecastJson() {
|
|
492
|
+
try {
|
|
493
|
+
if (!this.forecastEnabled || !this.forecastJsonDatapoint) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const state = await this.getForeignStateAsync(this.forecastJsonDatapoint);
|
|
498
|
+
if (!state || !state.val) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
let data;
|
|
503
|
+
try {
|
|
504
|
+
data = typeof state.val === 'string' ? JSON.parse(state.val) : state.val;
|
|
505
|
+
} catch {
|
|
506
|
+
this.log.warn('[Forecast] Invalid JSON in pvforecast datapoint');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
let best = null;
|
|
516
|
+
|
|
517
|
+
for (const entry of data) {
|
|
518
|
+
if (!entry || typeof entry.t !== 'number' || typeof entry.y !== 'number') {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (entry.t < now) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
best = entry;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!best) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// pvforecast liefert kW → wir rechnen auf W
|
|
533
|
+
const power = Math.round(best.y * 1000);
|
|
534
|
+
const minutes = Math.max(0, Math.round((best.t - now) / 60000));
|
|
535
|
+
const surplus = power - (this.config.baseload || 0);
|
|
536
|
+
|
|
537
|
+
this.forecast.power = power;
|
|
538
|
+
this.forecast.surplus = surplus > 0 ? surplus : 0;
|
|
539
|
+
this.forecast.minutes = minutes;
|
|
540
|
+
this.forecast.lastUpdate = Date.now();
|
|
541
|
+
|
|
542
|
+
await this.setStateAsync(`${this.namespace}.forecast.power`, { val: power, ack: true });
|
|
543
|
+
await this.setStateAsync(`${this.namespace}.forecast.surplus`, {
|
|
544
|
+
val: this.forecast.surplus,
|
|
545
|
+
ack: true,
|
|
546
|
+
});
|
|
547
|
+
await this.setStateAsync(`${this.namespace}.forecast.minutes`, { val: minutes, ack: true });
|
|
548
|
+
await this.setStateAsync(`${this.namespace}.forecast.lastUpdate`, {
|
|
549
|
+
val: this.forecast.lastUpdate,
|
|
550
|
+
ack: true,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
this.log.debug(
|
|
554
|
+
`[Forecast] JSON parsed → power=${power}W, surplus=${this.forecast.surplus}W, minutes=${minutes}`,
|
|
555
|
+
);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
this.log.error(`[Forecast] Error processing pvforecast JSON: ${error.message}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// =====================================================================
|
|
562
|
+
// ========================= Forecast helpers ==========================
|
|
563
|
+
// =====================================================================
|
|
564
|
+
getForecastPower(_v) {
|
|
565
|
+
return this.forecast?.power || 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
getForecastMinutes(_v) {
|
|
569
|
+
return this.forecast?.minutes || 0;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
isForecastAllowingSwitchOn(v) {
|
|
573
|
+
if (!this.forecastEnabled || !v.useForecast) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
const minMinutes = v.forecastMinMinutes || 0;
|
|
577
|
+
if (this.forecast.minutes > minMinutes) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
if (this.forecast.surplus >= v.performance) {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
isForecastDelayingSwitchOff(v, _currentSurplus) {
|
|
587
|
+
if (!this.forecastEnabled || !v.useForecast) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
const minMinutes = v.forecastMinMinutes || 0;
|
|
591
|
+
if (this.forecast.minutes > minMinutes) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
if (this.forecast.surplus >= v.switchOnPoint) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async setForecastState(v, idx, active, reason, power, minutes) {
|
|
601
|
+
try {
|
|
602
|
+
const safeName = this.name2id(v.name);
|
|
603
|
+
const channelId = `consumer.${idx}_${safeName}`;
|
|
604
|
+
const text = active
|
|
605
|
+
? `${reason || 'forecast'} (${power || this.forecast.power || 0}W / ${minutes || this.forecast.minutes || 0}min)`
|
|
606
|
+
: '';
|
|
607
|
+
await this.setStateAsync(`${this.namespace}.${channelId}.forecastStatus`, {
|
|
608
|
+
val: text,
|
|
609
|
+
ack: true,
|
|
610
|
+
});
|
|
611
|
+
} catch (err) {
|
|
612
|
+
this.log.warn(`[Forecast] Failed to set forecastStatus: ${err.message}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async sendForecastNotification(type, message) {
|
|
617
|
+
try {
|
|
618
|
+
if (this.config.notifyForecast) {
|
|
619
|
+
await this.sendNotification('smartloadmanager', 'notify', message);
|
|
620
|
+
}
|
|
621
|
+
} catch (err) {
|
|
622
|
+
this.log.warn(`[Forecast] Failed to send forecast notification: ${err.message}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// =====================================================================
|
|
627
|
+
// ========================= Time window helper ========================
|
|
628
|
+
// =====================================================================
|
|
311
629
|
timeWithinWindow(onTime, offTime, name) {
|
|
312
630
|
if (!onTime || !offTime) {
|
|
313
631
|
return true;
|
|
@@ -331,11 +649,23 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
331
649
|
return within;
|
|
332
650
|
}
|
|
333
651
|
|
|
652
|
+
// =====================================================================
|
|
653
|
+
// ========================= onStateChange =============================
|
|
654
|
+
// =====================================================================
|
|
334
655
|
async onStateChange(id, state) {
|
|
335
656
|
if (!state) {
|
|
336
657
|
return;
|
|
337
658
|
}
|
|
338
659
|
|
|
660
|
+
// 🔮 Forecast JSON datapoint changed
|
|
661
|
+
if (this.forecastEnabled && this.forecastJsonDatapoint && id === this.forecastJsonDatapoint) {
|
|
662
|
+
if (state.ack === true) {
|
|
663
|
+
await this.processPvforecastJson();
|
|
664
|
+
await this.checkConsumers();
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
339
669
|
// FeedIn Datapoint - fremder State, nur bei ack=true reagieren
|
|
340
670
|
if (id === this.feedInDatapoint) {
|
|
341
671
|
// Nur verarbeiten wenn der Wert vom anderen Adapter bestätigt wurde
|
|
@@ -387,6 +717,19 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
387
717
|
if (state && !state.ack) {
|
|
388
718
|
this.log.debug(`State changed: ${id} => ${state.val}`);
|
|
389
719
|
|
|
720
|
+
// --- Aktiv-Status manuell umschalten ---------------------------------
|
|
721
|
+
if (id.match(/consumer\.(\d+)_.*?\.active/)) {
|
|
722
|
+
const match = id.match(/consumer\.(\d+)_.*?\.active/);
|
|
723
|
+
const index = parseInt(match[1]);
|
|
724
|
+
const v = this.consumerList[index];
|
|
725
|
+
if (v) {
|
|
726
|
+
v.enabled = state.val;
|
|
727
|
+
this.log.info(`Consumer "${v.name}" became ${state.val ? 'activated' : 'deactivated'}.`);
|
|
728
|
+
await this.setStateAsync(id, { val: state.val, ack: true });
|
|
729
|
+
}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
390
733
|
// ControlMode Änderungen
|
|
391
734
|
const match = id.match(/consumer\.(\d+)_.*?\.controlMode/);
|
|
392
735
|
if (match) {
|
|
@@ -412,6 +755,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
412
755
|
}
|
|
413
756
|
}
|
|
414
757
|
|
|
758
|
+
// =====================================================================
|
|
759
|
+
// ============================ checkConsumers =========================
|
|
760
|
+
// =====================================================================
|
|
415
761
|
async checkConsumers() {
|
|
416
762
|
if (this.checkRunning) {
|
|
417
763
|
this.checkQueued = true;
|
|
@@ -423,11 +769,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
423
769
|
try {
|
|
424
770
|
if (!this.feedInDatapoint) {
|
|
425
771
|
this.log.warn('No FeedIn datapoint set – skipping checkConsumers()');
|
|
426
|
-
await this.sendNotification(
|
|
427
|
-
'smartloadmanager',
|
|
428
|
-
'alert',
|
|
429
|
-
'❗ Kein FeedIn-Datenpunkt gesetzt – Regelung übersprungen.',
|
|
430
|
-
);
|
|
772
|
+
await this.sendNotification('smartloadmanager', 'alert', '❗ No FeedIn data point set – rule skipped.');
|
|
431
773
|
this.checkRunning = false;
|
|
432
774
|
if (this.checkQueued) {
|
|
433
775
|
await this.checkConsumers();
|
|
@@ -456,6 +798,11 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
456
798
|
this.log.debug(`Switch-on check for ${sortedOn.length} binary/heating-consumers.`);
|
|
457
799
|
|
|
458
800
|
for (const v of sortedOn) {
|
|
801
|
+
// Überspringe deaktivierte Verbraucher
|
|
802
|
+
if (!v.enabled) {
|
|
803
|
+
this.log.debug(`"${v.name}" is deactivated – skipped (power-on check).`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
459
806
|
const idx = this.consumerList.indexOf(v);
|
|
460
807
|
const safeName = this.name2id(v.name);
|
|
461
808
|
const id = `${this.namespace}.consumer.${idx}_${safeName}`;
|
|
@@ -510,7 +857,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
510
857
|
if (v.enableDatapoint) {
|
|
511
858
|
const enableState = await this.getForeignStateAsync(v.enableDatapoint);
|
|
512
859
|
if (!enableState?.val) {
|
|
513
|
-
this.log.debug(`"${v.name}"
|
|
860
|
+
this.log.debug(`"${v.name}" NOT switched on – Hardware release missing.`);
|
|
514
861
|
continue;
|
|
515
862
|
}
|
|
516
863
|
}
|
|
@@ -551,6 +898,20 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
551
898
|
await this.switchConsumerWithDelay(v, true, v.switchOnDelay || 0);
|
|
552
899
|
feedIn -= v.performance;
|
|
553
900
|
this.log.debug(`Remaining surplus after switch-on: ${feedIn - baseload} W`);
|
|
901
|
+
await this.setForecastState(v, idx, false, '', null, null);
|
|
902
|
+
} else if (!gridUsage && !isOn && this.isForecastAllowingSwitchOn(v)) {
|
|
903
|
+
const forecastPower = this.getForecastPower(v);
|
|
904
|
+
const minutes = this.getForecastMinutes(v);
|
|
905
|
+
this.log.debug(
|
|
906
|
+
`[Forecast] Early switch-on allowed for "${v.name}": ${forecastPower} W in ${minutes} min.`,
|
|
907
|
+
);
|
|
908
|
+
await this.setForecastState(v, idx, true, 'forecastSwitchOn', forecastPower, minutes);
|
|
909
|
+
await this.sendForecastNotification(
|
|
910
|
+
v.ruletype === 'percent' ? 'percent' : 'binary',
|
|
911
|
+
`🔮 Forecast: "${v.name}" will be switched on in ${minutes} minutes (${forecastPower} W expected).`,
|
|
912
|
+
);
|
|
913
|
+
await this.switchConsumerWithDelay(v, true, v.switchOnDelay || 0);
|
|
914
|
+
feedIn -= v.performance;
|
|
554
915
|
} else if (gridUsage && !isOn) {
|
|
555
916
|
this.log.debug(`Not switched on: grid usage active.`);
|
|
556
917
|
} else if (!isOn) {
|
|
@@ -577,6 +938,11 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
577
938
|
const safeName = this.name2id(v.name);
|
|
578
939
|
const id = `${this.namespace}.consumer.${idx}_${safeName}`;
|
|
579
940
|
const mode = await this.getStateAsync(`${id}.controlMode`);
|
|
941
|
+
// Überspringe deaktivierte Verbraucher
|
|
942
|
+
if (!v.enabled) {
|
|
943
|
+
this.log.debug(`"${v.name}" is deactivated – will be skipped (power-off check).`);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
580
946
|
if (!mode || mode.val !== 2) {
|
|
581
947
|
continue;
|
|
582
948
|
}
|
|
@@ -689,6 +1055,21 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
689
1055
|
const shouldSwitchOff = !withinWindow || gridUsage || surplus < v.switchOffPoint;
|
|
690
1056
|
|
|
691
1057
|
if (!v.alwaysOffAtTime && shouldSwitchOff) {
|
|
1058
|
+
// 🔮 Forecast delay switch-off (PV forecast integration)
|
|
1059
|
+
if (this.isForecastDelayingSwitchOff(v, surplus)) {
|
|
1060
|
+
const forecastPower = this.getForecastPower(v);
|
|
1061
|
+
const minutes = this.getForecastMinutes(v);
|
|
1062
|
+
this.log.debug(
|
|
1063
|
+
`[Forecast] Switch-off delayed for "${v.name}": ${forecastPower} W in ${minutes} min.`,
|
|
1064
|
+
);
|
|
1065
|
+
await this.setForecastState(v, idx, true, 'forecastDelayOff', forecastPower, minutes);
|
|
1066
|
+
await this.sendForecastNotification(
|
|
1067
|
+
v.ruletype === 'percent' ? 'percent' : 'binary',
|
|
1068
|
+
`🔮 Forecast: "${v.name}" remains active (${forecastPower} W expected in ${minutes} min).`,
|
|
1069
|
+
);
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
692
1073
|
if (v.timer && v.timerEnd && v.pendingAction === false) {
|
|
693
1074
|
const remaining = Math.max(0, Math.round((v.timerEnd - Date.now()) / 1000));
|
|
694
1075
|
this.log.debug(`Active timer for ${v.name}: ${remaining}s remaining`);
|
|
@@ -697,6 +1078,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
697
1078
|
`Switching off: "${v.name}" - surplus ${surplus}W < switchOffPoint ${v.switchOffPoint}W, offDelay ${v.switchOffDelay || 0}s.`,
|
|
698
1079
|
);
|
|
699
1080
|
await this.switchConsumerWithDelay(v, false, v.switchOffDelay || 0);
|
|
1081
|
+
await this.setForecastState(v, idx, false, '', null, null);
|
|
700
1082
|
}
|
|
701
1083
|
}
|
|
702
1084
|
}
|
|
@@ -704,6 +1086,10 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
704
1086
|
// --- Batterie unverändert ---
|
|
705
1087
|
const batteryConsumers = this.consumerList.filter(c => c.ruletype === 'battery');
|
|
706
1088
|
for (const v of batteryConsumers) {
|
|
1089
|
+
if (!v.enabled) {
|
|
1090
|
+
this.log.debug(`"${v.name}" is deactivated – battery control skipped.`);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
707
1093
|
await this.controlBattery(v, feedIn);
|
|
708
1094
|
}
|
|
709
1095
|
|
|
@@ -730,6 +1116,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
730
1116
|
}
|
|
731
1117
|
}
|
|
732
1118
|
|
|
1119
|
+
// =====================================================================
|
|
1120
|
+
// ========================= controlPercentConsumer ====================
|
|
1121
|
+
// =====================================================================
|
|
733
1122
|
async controlPercentConsumer(v) {
|
|
734
1123
|
try {
|
|
735
1124
|
const withinWindow = this.timeWithinWindow(v.switchOnTime, v.switchOffTime);
|
|
@@ -792,6 +1181,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
792
1181
|
}
|
|
793
1182
|
}
|
|
794
1183
|
|
|
1184
|
+
// =====================================================================
|
|
1185
|
+
// =============================== controlBattery ======================
|
|
1186
|
+
// =====================================================================
|
|
795
1187
|
async controlBattery(v, feedIn) {
|
|
796
1188
|
try {
|
|
797
1189
|
this.log.debug(`[Battery] Checking for ${v.name}`);
|
|
@@ -948,6 +1340,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
948
1340
|
}
|
|
949
1341
|
}
|
|
950
1342
|
|
|
1343
|
+
// =====================================================================
|
|
1344
|
+
// ========================= switchConsumerWithDelay ===================
|
|
1345
|
+
// =====================================================================
|
|
951
1346
|
async switchConsumerWithDelay(consumer, turnOn, delaySeconds) {
|
|
952
1347
|
const delay =
|
|
953
1348
|
delaySeconds !== undefined
|
|
@@ -1112,6 +1507,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1112
1507
|
}
|
|
1113
1508
|
}
|
|
1114
1509
|
|
|
1510
|
+
// =====================================================================
|
|
1511
|
+
// ========================= sendNotification ==========================
|
|
1512
|
+
// =====================================================================
|
|
1115
1513
|
// Zentrale, einheitliche Notification-Funktion
|
|
1116
1514
|
// Aufruf: await this.sendNotification('smartloadmanager', 'info'|'notify'|'alert', 'Nachricht');
|
|
1117
1515
|
async sendNotification(scope, category, message) {
|
|
@@ -1238,6 +1636,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1238
1636
|
}
|
|
1239
1637
|
}
|
|
1240
1638
|
|
|
1639
|
+
// =====================================================================
|
|
1640
|
+
// ========================= initializeConsumerStatus ==================
|
|
1641
|
+
// =====================================================================
|
|
1241
1642
|
async initializeConsumerStatus() {
|
|
1242
1643
|
// Initiale Status-Updates
|
|
1243
1644
|
for (const v of this.consumerList) {
|
|
@@ -1268,6 +1669,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1268
1669
|
return h * 60 + m;
|
|
1269
1670
|
}
|
|
1270
1671
|
|
|
1672
|
+
// =====================================================================
|
|
1673
|
+
// =============================== onUnload ============================
|
|
1674
|
+
// =====================================================================
|
|
1271
1675
|
async onUnload(callback) {
|
|
1272
1676
|
try {
|
|
1273
1677
|
if (this.percentTimer) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.smartloadmanager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "This adapter allows devices to be controlled via PV surplus and thus achieve zero feed-in.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "quorle",
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
"translate": "^3.0.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@alcalzone/release-script": "^
|
|
32
|
-
"@alcalzone/release-script-plugin-iobroker": "^
|
|
33
|
-
"@alcalzone/release-script-plugin-license": "^
|
|
34
|
-
"@alcalzone/release-script-plugin-manual-review": "^
|
|
35
|
-
"@iobroker/adapter-dev": "^1.
|
|
36
|
-
"@iobroker/eslint-config": "^2.
|
|
37
|
-
"@iobroker/testing": "^5.
|
|
38
|
-
"@tsconfig/node20": "^20.1.
|
|
39
|
-
"@types/node": "^
|
|
40
|
-
"typescript": "~
|
|
31
|
+
"@alcalzone/release-script": "^5.1.1",
|
|
32
|
+
"@alcalzone/release-script-plugin-iobroker": "^5.1.2",
|
|
33
|
+
"@alcalzone/release-script-plugin-license": "^5.1.1",
|
|
34
|
+
"@alcalzone/release-script-plugin-manual-review": "^5.1.1",
|
|
35
|
+
"@iobroker/adapter-dev": "^1.5.0",
|
|
36
|
+
"@iobroker/eslint-config": "^2.2.0",
|
|
37
|
+
"@iobroker/testing": "^5.2.2",
|
|
38
|
+
"@tsconfig/node20": "^20.1.9",
|
|
39
|
+
"@types/node": "^25.6.0",
|
|
40
|
+
"typescript": "~6.0.2"
|
|
41
41
|
},
|
|
42
42
|
"main": "main.js",
|
|
43
43
|
"files": [
|