iobroker.smartloadmanager 0.1.0 → 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 +5 -6
- 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 -23
- package/io-package.json +36 -2
- package/main.js +356 -0
- package/package.json +9 -9
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "smartloadmanager",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.1.2": {
|
|
7
|
+
"en": "Update dependencies",
|
|
8
|
+
"de": "Abhängigkeiten aktualisieren",
|
|
9
|
+
"ru": "Обновление зависимостей",
|
|
10
|
+
"pt": "Atualizar dependências",
|
|
11
|
+
"nl": "Afhankelijkheden bijwerken",
|
|
12
|
+
"fr": "Mise à jour des dépendances",
|
|
13
|
+
"it": "Dipendenze di aggiornamento",
|
|
14
|
+
"es": "Actualizar las dependencias",
|
|
15
|
+
"pl": "Aktualizuj zależności",
|
|
16
|
+
"uk": "Оновлення залежності",
|
|
17
|
+
"zh-cn": "更新依赖关系"
|
|
18
|
+
},
|
|
19
|
+
"0.1.1": {
|
|
20
|
+
"en": "Update dependencies",
|
|
21
|
+
"de": "Abhängigkeiten aktualisieren",
|
|
22
|
+
"ru": "Обновление зависимостей",
|
|
23
|
+
"pt": "Atualizar dependências",
|
|
24
|
+
"nl": "Afhankelijkheden bijwerken",
|
|
25
|
+
"fr": "Mise à jour des dépendances",
|
|
26
|
+
"it": "Dipendenze di aggiornamento",
|
|
27
|
+
"es": "Actualizar las dependencias",
|
|
28
|
+
"pl": "Aktualizuj zależności",
|
|
29
|
+
"uk": "Оновлення залежності",
|
|
30
|
+
"zh-cn": "更新依赖关系"
|
|
31
|
+
},
|
|
6
32
|
"0.1.0": {
|
|
7
33
|
"en": "Added a checkbox for activating consumers from the settings to the object tree",
|
|
8
34
|
"de": "Hinzufügen eines Kontrollkästchens zur Aktivierung von Verbrauchern von den Einstellungen zum Objektbaum",
|
|
@@ -235,7 +261,15 @@
|
|
|
235
261
|
"whatsappRecipients": "",
|
|
236
262
|
"signalRecipients": "",
|
|
237
263
|
"pushoverRecipients": "",
|
|
238
|
-
"enableNotificationManager": false
|
|
264
|
+
"enableNotificationManager": false,
|
|
265
|
+
"enableForecast": false,
|
|
266
|
+
"forecastDefaultMinMinutes": 0,
|
|
267
|
+
"enableNotifications": false,
|
|
268
|
+
"notifyForecastBinary": false,
|
|
269
|
+
"notifyForecastPercent": false,
|
|
270
|
+
"notifyForecastBattery": false,
|
|
271
|
+
"forecastJsonDatapoint": "",
|
|
272
|
+
"notifyForecast": ""
|
|
239
273
|
},
|
|
240
274
|
"objects": [],
|
|
241
275
|
"instanceObjects": []
|
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,6 +78,10 @@ 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 =>
|
|
@@ -87,6 +106,12 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
87
106
|
|
|
88
107
|
await this.checkAndCreateConsumerObjects();
|
|
89
108
|
await this.createConsumerStates();
|
|
109
|
+
|
|
110
|
+
// 🔮 Forecast states (adapter-internal)
|
|
111
|
+
if (this.forecastEnabled) {
|
|
112
|
+
await this.createForecastStates();
|
|
113
|
+
}
|
|
114
|
+
|
|
90
115
|
await this.initializeConsumerStatus();
|
|
91
116
|
await this.subscribeStatesAsync('*');
|
|
92
117
|
await this.subscribeForeignStatesAsync(this.feedInDatapoint);
|
|
@@ -94,6 +119,12 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
94
119
|
await this.subscribeForeignStatesAsync(this.batteryControlModeDatapoint);
|
|
95
120
|
}
|
|
96
121
|
|
|
122
|
+
// 🔮 Forecast subscribe + initial parse
|
|
123
|
+
if (this.forecastEnabled && this.forecastJsonDatapoint) {
|
|
124
|
+
await this.subscribeForeignStatesAsync(this.forecastJsonDatapoint);
|
|
125
|
+
await this.processPvforecastJson();
|
|
126
|
+
}
|
|
127
|
+
|
|
97
128
|
await this.checkConsumers();
|
|
98
129
|
} catch (error) {
|
|
99
130
|
this.log.error(`Error in onReady: ${error.message}`);
|
|
@@ -102,6 +133,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
102
133
|
}
|
|
103
134
|
}
|
|
104
135
|
|
|
136
|
+
// =====================================================================
|
|
137
|
+
// ===================== Consumer object checks ========================
|
|
138
|
+
// =====================================================================
|
|
105
139
|
async checkAndCreateConsumerObjects() {
|
|
106
140
|
for (const v of this.consumerList) {
|
|
107
141
|
if (v.datapoint) {
|
|
@@ -119,6 +153,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
119
153
|
}
|
|
120
154
|
}
|
|
121
155
|
|
|
156
|
+
// =====================================================================
|
|
157
|
+
// ========================= createConsumerStates ======================
|
|
158
|
+
// =====================================================================
|
|
122
159
|
async createConsumerStates() {
|
|
123
160
|
// Übergeordnetes Device erstellen - löst "intermediate objects missing" Problem
|
|
124
161
|
await this.setObjectNotExistsAsync('consumer', {
|
|
@@ -250,6 +287,47 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
250
287
|
native: {},
|
|
251
288
|
});
|
|
252
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
|
+
|
|
253
331
|
if (v.ruletype === 'battery') {
|
|
254
332
|
await this.setObjectNotExistsAsync(`${channelId}.batterySetpoint`, {
|
|
255
333
|
type: 'state',
|
|
@@ -328,11 +406,226 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
328
406
|
|
|
329
407
|
await this.setStateAsync(`${channelId}.active`, { val: v.enabled || false, ack: true });
|
|
330
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
|
+
|
|
331
417
|
v.switchOnTime = finalOnTime;
|
|
332
418
|
v.switchOffTime = finalOffTime;
|
|
333
419
|
}
|
|
334
420
|
}
|
|
335
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
|
+
// =====================================================================
|
|
336
629
|
timeWithinWindow(onTime, offTime, name) {
|
|
337
630
|
if (!onTime || !offTime) {
|
|
338
631
|
return true;
|
|
@@ -356,11 +649,23 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
356
649
|
return within;
|
|
357
650
|
}
|
|
358
651
|
|
|
652
|
+
// =====================================================================
|
|
653
|
+
// ========================= onStateChange =============================
|
|
654
|
+
// =====================================================================
|
|
359
655
|
async onStateChange(id, state) {
|
|
360
656
|
if (!state) {
|
|
361
657
|
return;
|
|
362
658
|
}
|
|
363
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
|
+
|
|
364
669
|
// FeedIn Datapoint - fremder State, nur bei ack=true reagieren
|
|
365
670
|
if (id === this.feedInDatapoint) {
|
|
366
671
|
// Nur verarbeiten wenn der Wert vom anderen Adapter bestätigt wurde
|
|
@@ -450,6 +755,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
450
755
|
}
|
|
451
756
|
}
|
|
452
757
|
|
|
758
|
+
// =====================================================================
|
|
759
|
+
// ============================ checkConsumers =========================
|
|
760
|
+
// =====================================================================
|
|
453
761
|
async checkConsumers() {
|
|
454
762
|
if (this.checkRunning) {
|
|
455
763
|
this.checkQueued = true;
|
|
@@ -590,6 +898,20 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
590
898
|
await this.switchConsumerWithDelay(v, true, v.switchOnDelay || 0);
|
|
591
899
|
feedIn -= v.performance;
|
|
592
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;
|
|
593
915
|
} else if (gridUsage && !isOn) {
|
|
594
916
|
this.log.debug(`Not switched on: grid usage active.`);
|
|
595
917
|
} else if (!isOn) {
|
|
@@ -733,6 +1055,21 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
733
1055
|
const shouldSwitchOff = !withinWindow || gridUsage || surplus < v.switchOffPoint;
|
|
734
1056
|
|
|
735
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
|
+
|
|
736
1073
|
if (v.timer && v.timerEnd && v.pendingAction === false) {
|
|
737
1074
|
const remaining = Math.max(0, Math.round((v.timerEnd - Date.now()) / 1000));
|
|
738
1075
|
this.log.debug(`Active timer for ${v.name}: ${remaining}s remaining`);
|
|
@@ -741,6 +1078,7 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
741
1078
|
`Switching off: "${v.name}" - surplus ${surplus}W < switchOffPoint ${v.switchOffPoint}W, offDelay ${v.switchOffDelay || 0}s.`,
|
|
742
1079
|
);
|
|
743
1080
|
await this.switchConsumerWithDelay(v, false, v.switchOffDelay || 0);
|
|
1081
|
+
await this.setForecastState(v, idx, false, '', null, null);
|
|
744
1082
|
}
|
|
745
1083
|
}
|
|
746
1084
|
}
|
|
@@ -778,6 +1116,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
778
1116
|
}
|
|
779
1117
|
}
|
|
780
1118
|
|
|
1119
|
+
// =====================================================================
|
|
1120
|
+
// ========================= controlPercentConsumer ====================
|
|
1121
|
+
// =====================================================================
|
|
781
1122
|
async controlPercentConsumer(v) {
|
|
782
1123
|
try {
|
|
783
1124
|
const withinWindow = this.timeWithinWindow(v.switchOnTime, v.switchOffTime);
|
|
@@ -840,6 +1181,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
840
1181
|
}
|
|
841
1182
|
}
|
|
842
1183
|
|
|
1184
|
+
// =====================================================================
|
|
1185
|
+
// =============================== controlBattery ======================
|
|
1186
|
+
// =====================================================================
|
|
843
1187
|
async controlBattery(v, feedIn) {
|
|
844
1188
|
try {
|
|
845
1189
|
this.log.debug(`[Battery] Checking for ${v.name}`);
|
|
@@ -996,6 +1340,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
996
1340
|
}
|
|
997
1341
|
}
|
|
998
1342
|
|
|
1343
|
+
// =====================================================================
|
|
1344
|
+
// ========================= switchConsumerWithDelay ===================
|
|
1345
|
+
// =====================================================================
|
|
999
1346
|
async switchConsumerWithDelay(consumer, turnOn, delaySeconds) {
|
|
1000
1347
|
const delay =
|
|
1001
1348
|
delaySeconds !== undefined
|
|
@@ -1160,6 +1507,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1160
1507
|
}
|
|
1161
1508
|
}
|
|
1162
1509
|
|
|
1510
|
+
// =====================================================================
|
|
1511
|
+
// ========================= sendNotification ==========================
|
|
1512
|
+
// =====================================================================
|
|
1163
1513
|
// Zentrale, einheitliche Notification-Funktion
|
|
1164
1514
|
// Aufruf: await this.sendNotification('smartloadmanager', 'info'|'notify'|'alert', 'Nachricht');
|
|
1165
1515
|
async sendNotification(scope, category, message) {
|
|
@@ -1286,6 +1636,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1286
1636
|
}
|
|
1287
1637
|
}
|
|
1288
1638
|
|
|
1639
|
+
// =====================================================================
|
|
1640
|
+
// ========================= initializeConsumerStatus ==================
|
|
1641
|
+
// =====================================================================
|
|
1289
1642
|
async initializeConsumerStatus() {
|
|
1290
1643
|
// Initiale Status-Updates
|
|
1291
1644
|
for (const v of this.consumerList) {
|
|
@@ -1316,6 +1669,9 @@ class ZeroFeedIn extends utils.Adapter {
|
|
|
1316
1669
|
return h * 60 + m;
|
|
1317
1670
|
}
|
|
1318
1671
|
|
|
1672
|
+
// =====================================================================
|
|
1673
|
+
// =============================== onUnload ============================
|
|
1674
|
+
// =====================================================================
|
|
1319
1675
|
async onUnload(callback) {
|
|
1320
1676
|
try {
|
|
1321
1677
|
if (this.percentTimer) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.smartloadmanager",
|
|
3
|
-
"version": "0.1.
|
|
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": "^5.
|
|
32
|
-
"@alcalzone/release-script-plugin-iobroker": "^
|
|
33
|
-
"@alcalzone/release-script-plugin-license": "^
|
|
34
|
-
"@alcalzone/release-script-plugin-manual-review": "^
|
|
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
35
|
"@iobroker/adapter-dev": "^1.5.0",
|
|
36
36
|
"@iobroker/eslint-config": "^2.2.0",
|
|
37
|
-
"@iobroker/testing": "^5.
|
|
38
|
-
"@tsconfig/node20": "^20.1.
|
|
39
|
-
"@types/node": "^
|
|
40
|
-
"typescript": "~
|
|
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": [
|