-
Notifications
You must be signed in to change notification settings - Fork 14
/
Tibber_Ver_1.3.12.js
1876 lines (1706 loc) · 103 KB
/
Tibber_Ver_1.3.12.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'use strict';
//------------------------------------------------------------------------------------------------------
//++++++++++++++++++++++++++++++++++++++++++ USER ANPASSUNGEN +++++++++++++++++++++++++++++++++++++++++
const instanz = '0_userdata.0'; // Instanz Script
const PfadEbene1 = 'TibberSkript'; // Pfad innerhalb der Instanz
const PfadEbene2 = ['Anzeige_VIS','OutputSignal','History','USER_ANPASSUNGEN'] // Pfad innerhalb PfadEbene1
const instanzE3DC_RSCP = 'e3dc-rscp.0'
const DebugAusgabeDetail = true;
const hystereseReichweite_h = 0.5; // Hysterese-Schwelle von ±30 Minuten
const hystereseBatterie_pro = 2; // Hysterese-Schwelle von ±2 %
const hystereseKapazitaet = 2; // Hysterese-Schwelle von ±2 kWh
//++++++++++++++++++++++++++++++++++++++++ ENDE USER ANPASSUNGEN +++++++++++++++++++++++++++++++++++++++
//------------------------------------------------------------------------------------------------------
//******************************************************************************************************
//**************************************** Deklaration Variablen ***************************************
//******************************************************************************************************
const scriptVersion = 'Version 1.3.12'
log(`-==== Tibber Skript ${scriptVersion} ====-`);
// IDs Script Charge_Control
const sID_Autonomiezeit =`${instanz}.Charge_Control.Allgemein.Autonomiezeit`;
const sID_arrayHausverbrauch =`${instanz}.Charge_Control.Allgemein.arrayHausverbrauchDurchschnitt`;
const sID_maxEntladetiefeBatterie =`${instanz}.Charge_Control.USER_ANPASSUNGEN.10_maxEntladetiefeBatterie`
const sID_PrognoseAuto_kWh =`${instanz}.Charge_Control.History.PrognoseAuto_kWh`
// IDs des Adapters e3dc-rscp
const sID_Batterie_SOC =`${instanzE3DC_RSCP}.EMS.BAT_SOC`; // aktueller Batterie_SOC
const sID_Bat_Charge_Limit =`${instanzE3DC_RSCP}.EMS.SYS_SPECS.maxBatChargePower`; // Batterie Ladelimit
const sID_SPECIFIED_Battery_Capacity_0 =`${instanzE3DC_RSCP}.BAT.BAT_0.SPECIFIED_CAPACITY`; // Installierte Batterie Kapazität Batteriekreis 0
const sID_SPECIFIED_Battery_Capacity_1 =`${instanzE3DC_RSCP}.BAT.BAT_1.SPECIFIED_CAPACITY`; // Installierte Batterie Kapazität Batteriekreis 1
const sID_BAT0_Alterungszustand =`${instanzE3DC_RSCP}.BAT.BAT_0.ASOC`; // Batterie ASOC e3dc-rscp
const sID_Power_Bat_W = `${instanzE3DC_RSCP}.EMS.POWER_BAT`; // aktuelle Batterie_Leistung'
const sID_Power_Grid = `${instanzE3DC_RSCP}.EMS.POWER_GRID` // Leistung aus Netz
const sID_Notrom_Status =`${instanzE3DC_RSCP}.EMS.EMERGENCY_POWER_STATUS`; // 0= nicht möglich 1=Aktiv 2= nicht Aktiv 3= nicht verfügbar 4=Inselbetrieb
// IDs des Script Tibber
const sID_aktuellerEigenverbrauch = `${instanz}.${PfadEbene1}.${PfadEbene2[0]}.aktuellerEigenverbrauch`; // Anzeige in VIS durchschnittlicher Eigenverbrauch
const sID_status = `${instanz}.${PfadEbene1}.${PfadEbene2[0]}.status`; // Anzeige in VIS Status
const sID_ladezeitBatterie = `${instanz}.${PfadEbene1}.${PfadEbene2[0]}.ladezeitBatterie`; // Anzeige in VIS Prognose Ladezeit Batterie bei aktuellen Einstellungen
const sID_timerAktiv = `${instanz}.${PfadEbene1}.${PfadEbene2[0]}.timerAktiv`; // Anzeige in VIS Status Timer um Batterie zu laden
const sID_StrompreisBatterie = `${instanz}.${PfadEbene1}.${PfadEbene2[0]}.strompreisBatterie` // Anzeige in VIS aktueller Strompreis Batterie
const sID_Spitzenstrompreis = `${instanz}.${PfadEbene1}.${PfadEbene2[0]}.Spitzenstrompreis` // Anzeige in VIS aktueller Strompreis Batterie
const sID_BatterieLaden =`${instanz}.${PfadEbene1}.${PfadEbene2[1]}.BatterieLaden`; // Schnittstelle zu Charge-Control für die Ladefreigabe
const sID_eAutoLaden = `${instanz}.${PfadEbene1}.${PfadEbene2[1]}.eAutoLaden`; // Schnittstelle zu E3DC_Wallbox Script Auto laden
const sID_BatterieEntladesperre =`${instanz}.${PfadEbene1}.${PfadEbene2[1]}.BatterieEntladesperre`; // Schnittstelle zu Charge-Control für die Entladesperre
const sID_DiagramJosonChart =`${instanz}.${PfadEbene1}.${PfadEbene2[2]}.JSON_Chart`; // JSON für Diagramm Tibber Preise in VIS
const sID_BatterieLadedaten = `${instanz}.${PfadEbene1}.${PfadEbene2[2]}.BatterieLadedaten` // JSON zum berechnen vom Batterie Strompreis
const sID_maxSoC =`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.maxSOC_Batterie`;
const sID_maxLadeleistungUser_W =`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.maxLadeleistung`;
const sID_hoherSchwellwertStrompreis = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}.hoherSchwellwertStrompreis`;
const sID_niedrigerSchwellwertStrompreis = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}.niedrigerSchwellwertStrompreis`;
const sID_Schneebedeckt = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}.pvSchneebedeckt`;
const sID_autPreisanpassung = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}.automPreisanpassung`;
const sID_Systemwirkungsgrad = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}.Systemwirkungsgrad`
const sID_BatteriepreisAktiv = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}.BatteriepreisAktiv` // Auswahl in VIS ob aktueller Strompreis Batterie brücksichtigt werden soll
const sID_Stromgestehungskosten = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}.stromgestehungskosten`
const sID_TibberLinkID = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}.tibberLinkId`
// IDs des Adapters TibberLink, Zuweisung in Funktion ScriptStart() wegen persönlicher ID
let tibberLinkId = getState(sID_TibberLinkID).val // TibberLink ID auslesen
const sID_PricesTodayJSON = `tibberlink.0.Homes.${tibberLinkId}.PricesToday.json` // Strompreise für aktuellen Tag
const sID_PricesTomorrowJSON = `tibberlink.0.Homes.${tibberLinkId}.PricesTomorrow.json` // Strompreise für nächsten Tag
const arrayID_TibberPrices =[sID_PricesTodayJSON,sID_PricesTomorrowJSON];
let maxBatterieSoC = 0, aktuelleBatterieSoC_Pro,aktuelleBatterieSoC_alt = 0, ladeZeit_h, maxLadeleistungUser_W, stromgestehungskosten;
let benoetigteKapazitaetAktuell_kWh_alt = 0, benoetigteKapazitaetPrognose_kWh_alt = 0;
let batterieKapazitaet_kWh = 0, minStrompreis_48h = 0, nReichweite_alt = 0, LogProgrammablauf = "";
let batterieSOC_alt = null, aktuellerPreisTibber = null ;
let hoherSchwellwert = 0, niedrigerSchwellwert = 0, peakSchwellwert = 0, systemwirkungsgrad = 0;
let dateBesteReichweiteLadezeit_alt = new Date();
let strompreisBatterie, bruttoPreisBatterie;
let bNachladenPeak = false, bLock = false, schneeBedeckt = false, autPreisanpassung = false, notstromAktiv = false, batteriepreisAktiv = false, bStart = true;
let battLaden = false, autoLaden = false, battSperre = false, battSperrePrio = false, statusText = ``;
let timerIds = [], timerTarget = [], timerObjektID = [],timerState =[], batterieLadedaten = [],datenHeute =[], datenMorgen = [], datenTibberLink48h = [];
//***************************************************************************************************
//**************************************** Function Bereich *****************************************
//***************************************************************************************************
// Alle nötigen Objekt ID's anlegen
async function createState(){
const createStatePromises = [
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[0]}.aktuellerEigenverbrauch`, { 'def': '', 'name': 'Anzeige in VIS durchschnittlicher Eigenverbrauch', 'type': 'string' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[0]}.status`, { 'def': '', 'name': 'Anzeige in VIS Status', 'type': 'string' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[0]}.ladezeitBatterie`, { 'def': 0, 'name': 'Anzeige in VIS Prognose Ladezeit Batterie bei aktuellen Einstellungen', 'type': 'number', 'unit': 'h' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[0]}.timerAktiv`, { 'def': false, 'name': 'Anzeige in VIS Status Timer um Batterie zu laden', 'type': 'boolean' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[0]}.strompreisBatterie`, { 'def': 0, 'name': 'Anzeige in VIS aktueller Strompreis Batterie', 'type': 'number', 'unit': '€' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[0]}.Spitzenstrompreis`, { 'def': 0, 'name': 'Anzeige in VIS Schwellwert Spitzenstrompreis', 'type': 'number', 'unit': '€' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[1]}.BatterieLaden`, { 'def': false, 'name': 'Schnittstelle zu Charge-Control laden', 'type': 'boolean' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[1]}.eAutoLaden`, { 'def': false, 'name': 'Schnittstelle zu E3DC_Wallbox Script Auto laden', 'type': 'boolean' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[1]}.BatterieEntladesperre`, { 'def': false, 'name': 'Schnittstelle zu Charge-Control Entladesperre', 'type': 'boolean' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[2]}.JSON_Chart`, { 'def': '[]', 'name': 'JSON für materialdesign json chart', 'type': 'string' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[2]}.BatterieLadedaten`, { 'def': [], 'name': 'Batterie Start SOC mit Strompreis', 'type': 'string' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.maxLadeleistung`, { 'def': 0, 'name': 'max Ladeleistung mit der die Batterie geladen wird', 'type': 'number', 'unit': 'W' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.hoherSchwellwertStrompreis`, { 'def': 0.24, 'name': 'Strompreisgrenze für Hochpreisphase', 'type': 'number', 'unit': '€' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.niedrigerSchwellwertStrompreis`, { 'def': 0.2, 'name': 'Strompreisgrenze für Niedrigpreisphase', 'type': 'number', 'unit': '€' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.pvSchneebedeckt`, { 'def': false, 'name': 'Kann in VIS manuell auf true gesetzt werden,wenn Schnee auf den PV Modulen liegt', 'type': 'boolean' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.automPreisanpassung`, { 'def': false, 'name': 'Kann in VIS manuell auf true gesetzt werden,wenn die Preisgrenzen automatisch angepasst werden sollen', 'type': 'boolean' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.maxSOC_Batterie`, { 'def': 80, 'name': 'max SOC in % der Batterie bis zu dem aus dem Netz geladen werden soll', 'type': 'number', 'unit': '%' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.Systemwirkungsgrad`, { 'def': 88, 'name': 'max Wirkungsgrad inkl. Batterie', 'type': 'number', 'unit': '%' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.BatteriepreisAktiv`, { 'def': false, 'name': 'Anwahl in VIS ob Batteriepreis berücksichtigt wird', 'type': 'boolean' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.tibberLinkId`, { 'def': '', 'name': 'Persönliche ID TibberLink Adapter', 'type': 'string' }),
createStateAsync(`${instanz}.${PfadEbene1}.${PfadEbene2[3]}.stromgestehungskosten`, { 'def': 0.1057, 'name': 'alle Kosten, die innerhalb der vorgesehenen Laufzeit (20 Jahre) entstehen addiert, dividiert durch den Ertrag an Solarstrom', 'type': 'number' }),
];
await Promise.all(createStatePromises);
}
// Wird nur beim Start vom Script aufgerufen
async function ScriptStart()
{
LogProgrammablauf += '0,';
// Erstelle die Objekt IDs
await createState();
log('-==== alle Objekt ID\'s angelegt ====-');
await CheckState();
// User Anpassungen parallel abrufen
const results = await Promise.all([
getStateAsync(sID_BatterieLadedaten),
getStateAsync(sID_Batterie_SOC),
getStateAsync(sID_SPECIFIED_Battery_Capacity_0),
getStateAsync(sID_maxEntladetiefeBatterie),
getStateAsync(sID_BAT0_Alterungszustand)
]).then(states => states.map(state => state.val));
[
batterieLadedaten, aktuelleBatterieSoC_Pro
] = results;
const batteryCapacity0 = results[2];
const entladetiefe_Pro = results[3];
const aSOC_Bat_Pro = results[4];
aktuelleBatterieSoC_alt = aktuelleBatterieSoC_Pro
batterieSOC_alt = aktuelleBatterieSoC_Pro
if (existsState(sID_SPECIFIED_Battery_Capacity_1)){
const batteryCapacity1 = (await getStateAsync(sID_SPECIFIED_Battery_Capacity_1)).val
batterieKapazitaet_kWh = (batteryCapacity0 + batteryCapacity1) / 1000;
}else{
batterieKapazitaet_kWh = batteryCapacity0 / 1000;
}
[datenHeute, datenMorgen] = await Promise.all([
getStateAsync(sID_PricesTodayJSON),
getStateAsync(sID_PricesTomorrowJSON)
]).then(states => states.map(state => JSON.parse(state.val)));
datenTibberLink48h = [...datenHeute, ...datenMorgen];
batterieLadedaten = JSON.parse(batterieLadedaten)
batterieKapazitaet_kWh = batterieKapazitaet_kWh * (entladetiefe_Pro/100);
batterieKapazitaet_kWh = round(((batterieKapazitaet_kWh/100)*aSOC_Bat_Pro),0);
aktuellerPreisTibber = await getCurrentPrice()
// Erstelle das Tibber Diagramm
await createDiagramm();
// Strompreis Batterie berechnen
await berechneBattPrice();
// Schwellwert setzen
if(autPreisanpassung){await autoPreisanpassung(minStrompreis_48h)}
// Tibber-Steuerung starten
await tibberSteuerungHauskraftwerk()
bStart = false;
}
// Alle User Eingaben prüfen ob Werte eingetragen wurden und Werte zuweisen
async function CheckState() {
const idUSER_ANPASSUNGEN = `${instanz}.${PfadEbene1}.${PfadEbene2[3]}`;
const objekte = [
{ id: 'maxLadeleistung', varName: 'maxLadeleistungUser_W', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen' },
{ id: 'hoherSchwellwertStrompreis', varName: 'hoherSchwellwert', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen' },
{ id: 'niedrigerSchwellwertStrompreis', varName: 'niedrigerSchwellwert', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen' },
{ id: 'pvSchneebedeckt', varName: 'schneeBedeckt', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen' , min: false, max: true, errorMsg: 'pvSchneebedeckt kann nur true oder false sein' },
{ id: 'automPreisanpassung', varName: 'autPreisanpassung', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen' , min: false, max: true, errorMsg: 'pvSchneebedeckt kann nur true oder false sein' },
{ id: 'maxSOC_Batterie', varName: 'maxBatterieSoC', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen', min: 0, max: 100, errorMsg: 'max Batterie SoC muss zwischen 0% und 100% sein' },
{ id: 'Systemwirkungsgrad', varName: 'systemwirkungsgrad', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen', min: 0, max: 100, errorMsg: 'Systemwirkungsgrad muss zwischen 0% und 100% sein' },
{ id: 'BatteriepreisAktiv', varName: 'batteriepreisAktiv', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen', min: false, max: true, errorMsg: 'BatteriepreisAktiv kann nur true oder false sein' },
{ id: 'stromgestehungskosten', varName: 'stromgestehungskosten', beschreibung: 'enthält keinen gültigen Wert, bitte prüfen'}
];
for (const obj of objekte) {
const value = (await getStateAsync(`${idUSER_ANPASSUNGEN}.${obj.id}`)).val;
if (value === undefined || value === null) {
log(`Die Objekt ID = ${idUSER_ANPASSUNGEN}.${obj.id} ${obj.beschreibung} `, 'error');
} else {
eval(`${obj.varName} = value`);
if (obj.min !== undefined && (value < obj.min || value > obj.max)) {
console.error(obj.errorMsg);
}
}
}
// Pfadangaben zu den Modulen Modbus und e3dc-rscp überprüfen
const PruefeID = [
sID_Batterie_SOC, sID_Bat_Charge_Limit,sID_Notrom_Status, sID_SPECIFIED_Battery_Capacity_0,
sID_SPECIFIED_Battery_Capacity_1, sID_BAT0_Alterungszustand, sID_Power_Bat_W, sID_Power_Grid, sID_Notrom_Status
];
for (const id of PruefeID) {
if (!existsObject(id)) {
log(`${id} existiert nicht, bitte prüfen`,'error');
}
}
}
// Ablaufsteuerung zum regeln der Batterieladung bei günstigen Tibber Preise
async function tibberSteuerungHauskraftwerk() {
try {
LogProgrammablauf += '1,';
[battLaden,autoLaden,statusText,battSperre,peakSchwellwert] = await Promise.all([
getStateAsync(sID_BatterieLaden),
getStateAsync(sID_eAutoLaden),
getStateAsync(sID_status),
getStateAsync(sID_BatterieEntladesperre),
getStateAsync(sID_Spitzenstrompreis)
]).then(states => states.map(state => state.val));
const [stunden, minuten] = (await getStateAsync(sID_Autonomiezeit)).val.split(' / ')[1].split(' ')[0].split(':').map(Number);
let reichweite_h = round(stunden + (minuten /60),2)
const datejetzt = new Date();
// Hysterese-Schwelle von ±30 Minuten dass kleine zeitliche Unterschiede nicht zu einem häufigen Wechsel führen
if(Math.abs(reichweite_h-nReichweite_alt) >= hystereseReichweite_h){
nReichweite_alt = reichweite_h
}else{
reichweite_h = nReichweite_alt
}
const endZeitBatterie = new Date(datejetzt.getTime() + reichweite_h * 3600000);
const pvLeistungAusreichend = await pruefePVLeistung(reichweite_h);
const ergebnis = await findeergebnisphasen(datenTibberLink48h, hoherSchwellwert, niedrigerSchwellwert);
const naechsteNiedrigphase = findeNaechstePhase(ergebnis.lowPhases);
//log(`ergebnis.lowPhases = ${JSON.stringify(ergebnis.normalPhases)}`,'warn')
//log(`naechsteNiedrigphase = ${JSON.stringify(naechsteNiedrigphase)}`,'warn')
// Batterieschwankungen +-2% ignorieren
const diffBatSOC = Math.abs(aktuelleBatterieSoC_Pro - aktuelleBatterieSoC_alt)
if(diffBatSOC >= hystereseBatterie_pro || ladeZeit_h == undefined){
ladeZeit_h = await berechneLadezeitBatterie(null,aktuelleBatterieSoC_Pro)
}
const aktivePhase = ergebnis.aktivePhase;
if (!aktivePhase) {log('aktivePhase ist null oder undefined', 'warn');return;}
let preisBatterie = 0;
let spitzenSchwellwert = round(hoherSchwellwert * (1 / (systemwirkungsgrad / 100)), 4);
// Tibber Preis aktualisieren
aktuellerPreisTibber = await getCurrentPrice();
if(batteriepreisAktiv){preisBatterie = bruttoPreisBatterie }else{preisBatterie = strompreisBatterie}
// Funktion prüfe Freigabe laden vom E-Auto aufrufen
await EAutoLaden(naechsteNiedrigphase);
// Kann die Batterie mit PV-Leistung geladen werden wenn ja dann Funktion beenden
if (pvLeistungAusreichend.state) {
LogProgrammablauf += '2,';
await loescheAlleTimer('Laden')
await loescheAlleTimer('Entladesperre');
battSperrePrio = false;
battLaden ? await setStateAsync(sID_BatterieLaden,false): null;
battSperre ? await setStateAsync(sID_BatterieEntladesperre,false): null;
let message = `Laden mit PV-Leistung (aktive Phase: ${aktivePhase.type})`;
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
// Entladen der Batterie sperren wenn Batteriepreis höher als Tibberpreis und sperre nicht über Timer gesetzt wurde
if (!battSperrePrio) {
if (preisBatterie > aktuellerPreisTibber) {
if(!battSperre){await setStateAsync(sID_BatterieEntladesperre, true);}
} else if (battSperre) {
await setStateAsync(sID_BatterieEntladesperre, false);
}
}
// Wenn max SOC erreicht wurde Funktion beenden -2% um pendeln zu verhindern
if (aktuelleBatterieSoC_Pro >= maxBatterieSoC -2){
LogProgrammablauf += '3,';
await loescheAlleTimer('Laden');
battLaden ? await setStateAsync(sID_BatterieLaden,false): null;
// Entladen der Batterie sperren wenn Batteriepreis höher als Tibberpreis oder gleich letzter Ladepreis
let message = `max SOC erreicht. Laden beendet (aktive Phase: ${aktivePhase.type})`;
statusText != message ? await setStateAsync(sID_status,message): null;
await loescheAlleTimer('Laden')
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
const naechsteNormalphase = findeNaechstePhase(ergebnis.normalPhases);
const naechstePhase0 = ergebnis?.naechstePhasen[0] // @ts-ignore
const dauerAktivePhase_h = aktivePhase ? round((new Date(aktivePhase.end) - new Date()) / (1000 * 60 * 60),2):null
const aktivePhaseType = aktivePhase.type;
peakSchwellwert != spitzenSchwellwert ? await setStateAsync(sID_Spitzenstrompreis, spitzenSchwellwert):null;
// Prüfe ob die aktive Phase === 'peak' ist, dann wird nicht geladen
if (aktivePhaseType === 'peak'){
LogProgrammablauf += '10,';
// Laden stoppen
// Preisunterschied innerhalb der Peakphase prüfen und wenn Differenz > 0,3€, Reichweite Batterie prüfen
const ergebnisPreisvergleich = await preisUnterschiedPruefen(aktivePhase.end,0.3)
if(ergebnisPreisvergleich.state){
LogProgrammablauf += '10/1,';
// Preissteigerung > 0.3 € in der aktuellen Peak Phase.Prüfen ob Batteriereichweite ausreicht
const dauerEndePreissteigerung_h = round(((ergebnisPreisvergleich.peakZeit.getTime() - new Date().getTime()) / (1000 * 60 * 60))+ ergebnisPreisvergleich.dauerInStunden,0)
if(Math.floor(reichweite_h) < dauerEndePreissteigerung_h){
LogProgrammablauf += '10/2,';
// Batterie reicht nicht aus.
// Kann die Preissteigerung überbrückt werden wenn das entladen der Batterie bis zur Preissteigerung gesperrt wird
const dauerBisPreissteigerung_h = dauerEndePreissteigerung_h - ergebnisPreisvergleich.dauerInStunden
if(Math.floor(dauerBisPreissteigerung_h + reichweite_h) < dauerEndePreissteigerung_h && !bNachladenPeak){
LogProgrammablauf += '10/3,';
// Batteriesperre reicht nicht aus nachladen
bNachladenPeak = true // weiteres Nachladen in der Peakphase verhindern.
const vonTime = new Date()
const bisTime = new Date(ergebnisPreisvergleich.peakZeit)
// günstigste Startzeit zum Laden suchen
const dateBesteStartLade = bestLoadTime(vonTime,bisTime,ladeZeit_h)
await setStateAtSpecificTime(dateBesteStartLade.zeit, sID_BatterieLaden, true);
await setStateAtSpecificTime(bisTime, sID_BatterieLaden, false);
// Nach der Preissteigerung Merker zurücksetzen.
const dauerEndePreissteigerung_ms = dauerEndePreissteigerung_h * 1000 * 60 * 60;
setTimeout(() => {bNachladenPeak = false;}, dauerEndePreissteigerung_ms);
const startTime = dateBesteStartLade.zeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = bisTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Nachladen während Peakphase von ${startTime} bis ${endeTime} (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}else if(!bNachladenPeak){
LogProgrammablauf += '10/4,';
// Batteriesperre reicht aus, sofort entladen sperren.
bNachladenPeak = true; // weiteres Nachladen in der Peakphase verhindern.
const vonTime = new Date()
const bisTime = new Date(ergebnisPreisvergleich.peakZeit)
await loescheAlleTimer('Entladesperre')
await setStateAtSpecificTime(vonTime, sID_BatterieEntladesperre, true);
await setStateAtSpecificTime(bisTime, sID_BatterieEntladesperre, false);
// Nach der Preissteigerung Merker zurücksetzen.
const dauerEndePreissteigerung_ms = dauerEndePreissteigerung_h * 1000 * 60 * 60;
setTimeout(() => {bNachladenPeak = false;}, dauerEndePreissteigerung_ms);
const startTime = vonTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = bisTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Batteriesperre während Peakphase von ${startTime} bis ${endeTime} (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
}
}
if(!bNachladenPeak){
await loescheAlleTimer('Laden');
battLaden ? await setStateAsync(sID_BatterieLaden,false): null;
let message = `Aktuell Strompreis zu hoch, es wird nicht geladen (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
}
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
if (aktivePhaseType === 'high'){
LogProgrammablauf += '11,';
// ist innerhalb der Reichweite Batterie eine low Preisphase
if (naechsteNiedrigphase.state){
if(naechsteNiedrigphase.startzeit < endZeitBatterie){
// nicht laden
LogProgrammablauf += '11/1,';
// dauer bis zur nächsten lowphase
const vonTime = new Date(naechsteNiedrigphase.startzeit)
const bisTime = new Date(naechsteNiedrigphase.endzeit)
// günstigste Startzeit zum Laden suchen
const dateBesteStartLade = bestLoadTime(vonTime,bisTime,ladeZeit_h)
// Prüfen ob Startzeit low Preisphase bereits erreicht ist und wenn nein die Ladefreigabe entfernen
if(naechsteNiedrigphase.startzeit.getTime() > new Date().getTime()){
if(battLaden){
await loescheAlleTimer('Laden');
await setStateAsync(sID_BatterieLaden,false)
}
}
const hours = dateBesteStartLade.zeit.getHours().toString().padStart(2, '0');
const minutes = dateBesteStartLade.zeit.getMinutes().toString().padStart(2, '0');
let message = `warte auf Niedrigpreisphase. Start laden ${hours}:${minutes} Uhr (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
}
// ist innerhalb der Reichweite Batterie eine normal Preisphase
if (naechsteNormalphase.state){
if(naechsteNormalphase.startzeit < endZeitBatterie){
// nicht laden
LogProgrammablauf += '11/2,';
const vonTime = new Date(naechsteNormalphase.startzeit)
const bisTime = new Date(naechsteNormalphase.endzeit)
// günstigste Startzeit zum Laden suchen
const dateBesteStartLade = bestLoadTime(vonTime,bisTime,ladeZeit_h)
// Prüfen ob der Zeitraum größer ist als die benötigte Zeit
const difference_ms = bisTime.getTime() - vonTime.getTime();
let diffZeit_h = Math.min(difference_ms / (1000 * 60 * 60), ladeZeit_h);
const dateBesteEndeLadezeit = new Date(dateBesteStartLade.zeit.getTime() + diffZeit_h * 60 * 60 * 1000);
const startTime = dateBesteStartLade.zeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = dateBesteEndeLadezeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Nächste Normalpreisphase von ${startTime} bis ${endeTime} (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
}
// ist die nächste Phase eine Normal Phase. Eventuell muss das noch geprüft werden.
// ist die nächste Phase eine peak Phase
if(naechstePhase0.type == 'peak'){
LogProgrammablauf += '11/3,';
// reicht die Batterie um diese zu überbrücken
if(naechstePhase0.end.getTime() > endZeitBatterie.getTime()){
// Batterie reicht nicht aus
const dauerPeakPhase_h = round((naechstePhase0.end.getTime() - naechstePhase0.start.getTime()) / (1000 * 60 * 60),2)
// Kann die Peak Phase überbrückt werden wenn das entladen der Batterie gesperrt wird
if(reichweite_h - (dauerPeakPhase_h +1) >= 0){
//Reichweite Batt reicht zum Abfragezeitraum noch aus.
LogProgrammablauf += '11/4,';
// Berechnen wann das Entladen der Batterie gesperrt werden muss um über die Peakphase zu kommen
let zeitBisSperre_h = reichweite_h - (dauerPeakPhase_h +1)
const vonTime = new Date(new Date().setMinutes(0, 0, 0));
vonTime.setHours(vonTime.getHours() + zeitBisSperre_h);
const bisTime = new Date(naechstePhase0.start)
await loescheAlleTimer('Entladesperre')
await setStateAtSpecificTime(vonTime, sID_BatterieEntladesperre, true);
await setStateAtSpecificTime(bisTime, sID_BatterieEntladesperre, false);
const startTime = vonTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = bisTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Batterie sperre von ${startTime} bis ${endeTime} um Peakphase zu überbrücken (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}else if (!battSperrePrio && reichweite_h - dauerPeakPhase_h < 0) {
LogProgrammablauf += '11/5,';
// high Phase und als nächstes kommt eine peak Phase die nur mit Batt Sperre nicht überbrückt werden kann.
const aktuelleZeit_ms = Date.now();
const sunriseHeute_ms = getAstroDate("sunrise", datejetzt).getTime(); // Sonnenaufgang
const sunsetHeute_ms = getAstroDate("sunset", datejetzt).getTime() - 2 * 3600000; // 2 Stunden Puffer // Sonnenuntergang -2 h
// Nur aus Netz laden Laden wenn Preis höher 0,1€ als aktueller Preis ist und nur soviel um diese phase zu überbrücken
// und nur Nachts wenn keine PV-Leistung möglich ist oder Module Schneebedeckt sind.(Absicherung wenn PV-Prognose falsch ist)
if (aktuelleZeit_ms < sunriseHeute_ms || aktuelleZeit_ms >= sunsetHeute_ms || schneeBedeckt) {
const ergebnisPreisvergleich = await preisUnterschiedPruefen(naechstePhase0.end,0.1)
if(ergebnisPreisvergleich.state == true){
const ladezeitBatt_h = await berechneLadezeitBatterie(dauerPeakPhase_h,null)
const bisTime = new Date(ergebnisPreisvergleich.peakZeit)
const vonTime = new Date(aktivePhase.start)
const start = bestLoadTime(vonTime,bisTime,ladezeitBatt_h)
const endeZeit = new Date(start.zeit.getTime() + ladezeitBatt_h * 60 * 60 * 1000);
await setStateAtSpecificTime(new Date(start.zeit), sID_BatterieLaden, true);
await setStateAtSpecificTime(new Date(endeZeit), sID_BatterieLaden, false);
// Batterie sperren ab Leidezeitpunkt bis zum Start der Peak Phase
await setStateAtSpecificTime(new Date(start.zeit), sID_BatterieEntladesperre, true);
await setStateAtSpecificTime(new Date(naechstePhase0.start), sID_BatterieEntladesperre, false);
const startTime = start.zeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = endeZeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Batterie laden von ${startTime} bis ${endeTime} um Peakphase zu überbrücken (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
}
}
}else{
// Batterieladung reicht um die phase zu überbrücken
LogProgrammablauf += '11/6,';
await loescheAlleTimer('Laden');
let message = `Batterie SOC reicht um nächste Peak Phase zu überbrücken (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
battLaden ? await setStateAsync(sID_BatterieLaden,false): null;
if(preisBatterie < aktuellerPreisTibber){
await loescheAlleTimer('Entladesperre')
battSperrePrio = false;
battSperre ? await setStateAsync(sID_BatterieEntladesperre,false):null;
}
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
}
}
if (aktivePhaseType === 'normal'){
LogProgrammablauf += '12,';
const vonTime = new Date(aktivePhase.start)
const bisTime = new Date(aktivePhase.end)
// ist innerhalb der Reichweite Batterie eine low Preisphase
if (naechsteNiedrigphase.state){
if(naechsteNiedrigphase.startzeit < endZeitBatterie){
// nicht laden
LogProgrammablauf += '12/1,';
// dauer bis zur nächsten lowphase
const vonTime = new Date(naechsteNiedrigphase.startzeit)
const bisTime = new Date(naechsteNiedrigphase.endzeit)
// günstigste Startzeit zum Laden suchen
const dateBesteStartLade = bestLoadTime(vonTime,bisTime,ladeZeit_h)
// Prüfen ob der Zeitraum größer ist als die benötigte Zeit
const difference_ms = bisTime.getTime() - vonTime.getTime();
let diffZeit_h = Math.min(difference_ms / (1000 * 60 * 60), ladeZeit_h);
const dateBesteEndeLadezeit = new Date(dateBesteStartLade.zeit.getTime() + diffZeit_h * 60 * 60 * 1000);
// Prüfen ob Startzeit low Preisphase bereits erreicht ist und wenn nein die Ladefreigabe entfernen
if(naechsteNiedrigphase.startzeit.getTime() > new Date().getTime()){
if(battLaden){
LogProgrammablauf += '12/2,';
await loescheAlleTimer('Laden');
await setStateAsync(sID_BatterieLaden,false)
}
}
const startTime = dateBesteStartLade.zeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = dateBesteEndeLadezeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `warte auf Niedrigpreisphase von ${startTime} bis ${endeTime} (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
}
// günstigste Startzeit suchen um auf max SOC zu laden
const dateBestePhaseStartLade = bestLoadTime(vonTime,bisTime,ladeZeit_h)
const dateBesteReichweiteLade = bestLoadTime(new Date(),endZeitBatterie,ladeZeit_h)
const dateBesteEndeLadezeit = new Date(dateBestePhaseStartLade.zeit.getTime() + ladeZeit_h * 60 * 60 * 1000);
// Differenz berechnen und nur wenn > 1 das Laden stoppen. Verhindert ein ständiges ein und ausschalten
const diffBesteLadezeit_h = Math.abs((dateBestePhaseStartLade.zeit.getTime()-dateBesteReichweiteLade.zeit.getTime())/ (1000 * 60 * 60))
// Prüfen ob es einen günstigerern Ladezeitraum innerhalb der Batteriereichweite gibt
if(dateBestePhaseStartLade.preis > dateBesteReichweiteLade.preis && diffBesteLadezeit_h > 1 && dateBesteReichweiteLade.zeit > new Date()){
LogProgrammablauf += '12/4,';
// Beste Ladezeit noch nicht erreicht Laden sperren
if(battLaden){
await loescheAlleTimer('Laden');
await setStateAsync(sID_BatterieLaden,false);
}
const dateReichweiteEndeLadezeit = new Date(dateBesteReichweiteLade.zeit.getTime() + ladeZeit_h * 60 * 60 * 1000);
await setStateAtSpecificTime(dateBesteReichweiteLade.zeit, sID_BatterieLaden, true);
await setStateAtSpecificTime(dateReichweiteEndeLadezeit, sID_BatterieLaden, false);
const startTime = dateBestePhaseStartLade.zeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = dateReichweiteEndeLadezeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Laden von ${startTime} bis ${endeTime} (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
// Prüfen ob der günstigste Ladezeitraum noch nicht abgelaufen ist und Batterie laden bereits aktiv ist
}else if(dateBesteEndeLadezeit > new Date() && !battLaden){
// günstigster Ladezeitraum noch nicht abgelaufen und Laden nicht aktiv
LogProgrammablauf += '12/5,';
// setze Timer
if(dateBestePhaseStartLade.zeit.getTime() < new Date().getTime() && dateBesteEndeLadezeit.getTime() > new Date().getTime() ){
// Start Zeit bereits abgelaufen und Ende Zeit noch nicht erreicht sofort laden.
LogProgrammablauf += '12/6,';
await setStateAsync(sID_BatterieLaden,true)
await setStateAtSpecificTime(dateBesteEndeLadezeit, sID_BatterieLaden, false);
}else{
// Noch mal nachladen
await setStateAtSpecificTime(dateBestePhaseStartLade.zeit, sID_BatterieLaden, true);
await setStateAtSpecificTime(dateBesteEndeLadezeit, sID_BatterieLaden, false);
}
const startTime = dateBestePhaseStartLade.zeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = dateBesteEndeLadezeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Laden von ${startTime} bis ${endeTime} (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
// Prüfen ob der günstigste Ladezeitraum bereits abgelaufen ist aber max SOC noch nicht erreicht wurde
// Nur zulassen wenn der SOC um 10% unter max SOC ist, um ein/aus schalten zu verhindern.
if(dateBesteEndeLadezeit < new Date() && (aktuelleBatterieSoC_Pro < (maxBatterieSoC - 10) || dateBesteReichweiteLade.zeit > new Date())){
LogProgrammablauf += '12/7,';
// Beste Ladezeit bereits abgelaufen, es muss aber noch nachgeladen werden
if(dateBesteReichweiteLade.zeit > new Date()){
// günstigere Ladezeit innerhalb Batteriereichweite gefunden
LogProgrammablauf += '12/8,';
const dateBesteEndeLadezeit = new Date(dateBesteReichweiteLade.zeit.getTime() + ladeZeit_h * 60 * 60 * 1000);
await setStateAtSpecificTime(dateBesteReichweiteLade.zeit, sID_BatterieLaden, true);
await setStateAtSpecificTime(dateBesteEndeLadezeit, sID_BatterieLaden, false);
const startTime = dateBesteReichweiteLade.zeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
const endeTime = dateBesteEndeLadezeit.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Beste Ladezeit abgelaufen. Nachladen von ${startTime} bis ${endeTime} (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
await loescheAlleTimer('Laden');
// bis zum ende der Normal Phase Laden freigeben
await setStateAsync(sID_BatterieLaden,true)
await setStateAtSpecificTime(bisTime, sID_BatterieLaden, false);
const endeTimePhase = bisTime.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' Uhr';
let message = `Beste Ladezeit abgelaufen. Nachladen bis max. ${endeTimePhase} (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
LogProgrammablauf += '12/9,';
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
if (aktivePhaseType === 'low'){
LogProgrammablauf += '13,';
// Laden bis max SOC
// günstigste Ladezeit suchen
const bisTime = new Date(aktivePhase.end)
const vonTime = new Date(aktivePhase.start)
log(`Programmablauf 13 vonTime = ${vonTime} bisTime = ${bisTime} ladeZeit_h = ${ladeZeit_h}`)
const dateBesteStartLade = bestLoadTime(vonTime,bisTime,ladeZeit_h)
const dateEndeLadezeit = new Date(dateBesteStartLade.zeit);
dateEndeLadezeit.setHours(dateEndeLadezeit.getHours() + ladeZeit_h);
await setStateAtSpecificTime(new Date(dateBesteStartLade.zeit), sID_BatterieLaden, true);
await setStateAtSpecificTime(new Date(dateEndeLadezeit), sID_BatterieLaden, false);
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
return;
}
LogProgrammablauf += '14,';
let message = `Nicht laden (aktive Phase: ${aktivePhase.type})`
statusText != message ? await setStateAsync(sID_status,message): null;
await DebugLog(ergebnis,spitzenSchwellwert,pvLeistungAusreichend.state);
LogProgrammablauf = '';
} catch (error) {
log(`Fehler in Funktion tibberSteuerungHauskraftwerk: ${error.message}`, 'error');
}
}
// Funktionen Freigabe E-Auto laden
async function EAutoLaden(naechsteNiedrigphase) {
try {
const datejetzt = new Date();
const endZeit = new Date(datejetzt.getTime() + 5 * 3600000) //aktuelle Zeit plus 5h
if (naechsteNiedrigphase.startzeit?.getTime() < endZeit.getTime()) {
LogProgrammablauf += '20,';
// Innerhalb 5 h kommt eine Niedrigphase mit dem Laden E-Auto warten.
if(naechsteNiedrigphase.startzeit.getTime() > datejetzt.getTime()){await setStateAsync(sID_eAutoLaden, false);}
await setStateAtSpecificTime(new Date(naechsteNiedrigphase.startzeit), sID_eAutoLaden, true);
}else{
//log(`aktuellerPreisTibber = ${aktuellerPreisTibber} hoherSchwellwert = ${hoherSchwellwert}`,'warn')
if (aktuellerPreisTibber !== null && aktuellerPreisTibber < hoherSchwellwert) {
LogProgrammablauf += '21,';
await setStateAsync(sID_eAutoLaden, true);
} else {
LogProgrammablauf += '22,';
await loescheAlleTimer('Auto')
await setStateAsync(sID_eAutoLaden, false);
}
}
} catch (error) {
log(`Fehler in Funktion EAutoLaden(): ${error.message}`, 'error');
}
}
// Funktion sucht entweder die aktuelle Phase oder die nächste Phase mit Start- und Endzeit.
function findeNaechstePhase(arrayPhases) {
try {
const jetzt = new Date();
// suche die nächste Phase mit Start- und Endzeit in der Zukunft
const naechstePhase = arrayPhases
.filter(phase => new Date(phase.start) > jetzt) // @ts-ignore Nur zukünftige Phasen
.sort((a, b) => new Date(a.start) - new Date(b.start))[0]; // Sortiere nach Startzeit
if (naechstePhase) {
return {
state: true,
startzeit: new Date(naechstePhase.start),
endzeit: new Date(naechstePhase.end),
startzeitLokal: new Date(naechstePhase.start).toLocaleString(),
endzeitLokal: new Date(naechstePhase.end).toLocaleString()
};
}
// Falls keine passende Phase gefunden wird, gib null zurück
LogProgrammablauf += '19,';
return {
state: false,
startzeit: null,
endzeit: null,
startzeitLokal: null,
endzeitLokal: null
};
} catch (error) {
log(`Fehler in Funktion findeNaechstePhase(): ${error.message}`, 'error');
}
}
// Die Funktion pruefePVLeistung prüft, ob die Batterie durch die erwartete PV-Leistung aufgeladen werden kann
// und ob die reichweiteStunden ausreicht, um bis zum nächsten Sonnenaufgang zu kommen.
async function pruefePVLeistung(reichweiteStunden) {
try {
LogProgrammablauf += '18,';
// Konvertiere und validiere die Reichweite in Stunden
let nreichweiteStunden = parseFloat(reichweiteStunden);
// Prüfung: Ist reichweiteStunden eine gültige Zahl?
if (isNaN(nreichweiteStunden)) {
log(`function pruefePVLeistung(): reichweiteStunden ist keine gültige Zahl`, 'error');
return {state: false};
}
// auf die nächstkleinere Ganzzahl abrunden
nreichweiteStunden = Math.floor(nreichweiteStunden)
// Wenn die PV-Module schneebedeckt sind, abbrechen
if (schneeBedeckt) {
LogProgrammablauf += '18/1,';
return {state: false};
}
const heute = new Date();
const morgen = new Date(new Date().setDate(heute.getDate() + 1));
const aktuelleZeit_ms = Date.now();
// Sonnenaufgang und Sonnenuntergang heute und morgen
const sonnenaufgangHeute_ms = getAstroDate("sunrise", heute).getTime(); // Sonnenaufgang
const sonnenuntergangHeute_ms = getAstroDate("sunset", heute).getTime() - 2 * 3600000; // Sonnenuntergang - 2 Stunden Puffer
const sonnenaufgangMorgen_ms = getAstroDate("sunrise", morgen).getTime(); // Sonnenaufgang nächster Tag
// PV-Prognosen für heute und morgen in kWh
let arrayPrognoseAuto_kWh = (await getStateAsync(sID_PrognoseAuto_kWh)).val;
let heuteErwartetePVLeistung_kWh = parseFloat(arrayPrognoseAuto_kWh[heute.getDate()]);
let morgenErwartetePVLeistung_kWh = parseFloat(arrayPrognoseAuto_kWh[morgen.getDate()]);
// Benötigte Kapazität, um die Batterie auf maximalen SOC zu laden
const progBattSoC = await prognoseBatterieSOC(nreichweiteStunden);
let benoetigteKapazitaetPrognose_kWh = (100 - progBattSoC.soc) / 100 * batterieKapazitaet_kWh;
let benoetigteKapazitaetAktuell_kWh = (100 - aktuelleBatterieSoC_Pro) / 100 * batterieKapazitaet_kWh;
// Hysterese-Schwelle von ±2 kWh dass kleine Unterschiede nicht zu einem häufigen Wechsel führen
if(Math.abs(benoetigteKapazitaetPrognose_kWh-benoetigteKapazitaetPrognose_kWh_alt) >= hystereseKapazitaet){
benoetigteKapazitaetPrognose_kWh_alt = benoetigteKapazitaetPrognose_kWh
}else{
benoetigteKapazitaetPrognose_kWh = benoetigteKapazitaetPrognose_kWh_alt
}
if(Math.abs(benoetigteKapazitaetAktuell_kWh-benoetigteKapazitaetAktuell_kWh_alt) >= hystereseKapazitaet){
benoetigteKapazitaetAktuell_kWh_alt = benoetigteKapazitaetAktuell_kWh
}else{
benoetigteKapazitaetAktuell_kWh = benoetigteKapazitaetAktuell_kWh_alt
}
// Berechnung der Zeit bis zum nächsten Sonnenaufgang
let stundenBisSunrise = 0;
if (aktuelleZeit_ms < sonnenaufgangHeute_ms) {
// Vor Sonnenaufgang heute
stundenBisSunrise = round((sonnenaufgangHeute_ms - aktuelleZeit_ms) / (1000 * 60 * 60),0);
} else if (aktuelleZeit_ms >= sonnenuntergangHeute_ms) {
// Nach Sonnenuntergang -> Nächster Sonnenaufgang ist morgen
stundenBisSunrise = round((sonnenaufgangMorgen_ms - aktuelleZeit_ms) / (1000 * 60 * 60),0);
} else {
// Zwischen Sonnenaufgang und Sonnenuntergang heute
const verbleibendeSonnenstunden = (sonnenuntergangHeute_ms - aktuelleZeit_ms) / (1000 * 60 * 60);
const gesamteSonnenstunden = (sonnenuntergangHeute_ms - sonnenaufgangHeute_ms) / (1000 * 60 * 60);
// Berechne die PV-Leistung bis zum Sonnenuntergang
const PVLeistungBisSonnenuntergang = (heuteErwartetePVLeistung_kWh / gesamteSonnenstunden)* verbleibendeSonnenstunden;
// Prüfen, ob die PV-Leistung bis Sonnenuntergang ausreicht
if (PVLeistungBisSonnenuntergang >= benoetigteKapazitaetAktuell_kWh) {
const state = nreichweiteStunden >= verbleibendeSonnenstunden;
LogProgrammablauf += '18/2,';
return {state};
} else {
LogProgrammablauf += '18/3,';
return {state: false};
}
}
// Prüfung, ob die Reichweite bis zum Sonnenaufgang ausreicht
if (nreichweiteStunden < stundenBisSunrise) {
LogProgrammablauf += '18/4,';
return {state: false};
}
// Prüfung der PV-Prognose für morgen
if (aktuelleZeit_ms >= sonnenuntergangHeute_ms) {
const prognoseBattSOCMorgen = await prognoseBatterieSOC(stundenBisSunrise);
const benoetigteKapazitaetMorgen_kWh = (100 - prognoseBattSOCMorgen.soc) / 100 * batterieKapazitaet_kWh;
if (morgenErwartetePVLeistung_kWh >= benoetigteKapazitaetMorgen_kWh) {
LogProgrammablauf += '18/5,';
return {state: true};
}
} else {
// Prüfung der PV-Leistung für heute
if (heuteErwartetePVLeistung_kWh >= benoetigteKapazitaetPrognose_kWh) {
LogProgrammablauf += '18/6,';
return {state: true};
}
}
LogProgrammablauf += '18/7,';
return {state: false};
} catch (error) {
log(`Fehler in Funktion pruefePVLeistung(): ${error.message}`, 'error');
}
}
// Funktion berechnet den Batterie SOC nach einer variablen Zeit in h bei einem berechnetem Durchschnittsverbrauch.
async function prognoseBatterieSOC(entladezeitStunden) {
try {
entladezeitStunden = round(+entladezeitStunden,0) || 0;
let hausverbrauch_day_kWh
let hausverbrauch_night_kWh
// Leistungsdaten vom aktuellen Tag abrufen
const hausverbrauch = JSON.parse((await getStateAsync(sID_arrayHausverbrauch)).val);
// Aktuellen Wochentag und Zeitintervall (Tag/Nacht) bestimmen
const now = new Date();
const currentDay = now.toLocaleDateString('de-DE', { weekday: 'long' });
hausverbrauch_day_kWh = hausverbrauch[currentDay]['day'] / 1000;
hausverbrauch_night_kWh = hausverbrauch[currentDay]['night'] / 1000;
const entladeneEnergie_kWh = round(((hausverbrauch_day_kWh + hausverbrauch_night_kWh)/2)*entladezeitStunden,2);
await setStateAsync(sID_aktuellerEigenverbrauch,`${round(hausverbrauch_day_kWh*1000,0)} W / ${round(hausverbrauch_night_kWh*1000,0)} W`);
// Neuen Batterie-SOC berechnen
let neueSoC = Math.floor(aktuelleBatterieSoC_Pro - (entladeneEnergie_kWh / batterieKapazitaet_kWh) * 100);
//log(`Batterie-SOC nach Reichweite: ${neueSoC}% aktuelleSoC =${aktuelleBatterieSoC_Pro} entladeneEnergie_kWh = ${entladeneEnergie_kWh} batterieKapazitaet_kWh = ${batterieKapazitaet_kWh} `,'warn');
neueSoC = Math.max(neueSoC, 0);
return {
state:true,
soc: neueSoC
};
} catch (error) {
log(`Fehler in Funktion prognoseBatterieSOC: ${error.message}`, 'error');
}
}
// Aufruf mit startSOC: Berechnet die Ladezeit basierend auf der aktuellen Batterieladung und dem maximalen Ladezustand.
// Aufruf mit dauer_h: Berechnet die Ladezeit auf Basis des Hausverbrauchs und der Ladeleistung nach ablauf von dauer_h.
async function berechneLadezeitBatterie(dauer_h = null, startSOC = null) {
try {
// Prüfen, ob beide Parameter gesetzt sind
if (dauer_h !== null && startSOC !== null) {
throw new Error("Es darf entweder 'dauer_h' oder 'startSOC' gesetzt sein, nicht beide gleichzeitig.");
}
// Leistungsdaten vom aktuellen Tag abrufen
const hausverbrauch = JSON.parse((await getStateAsync(sID_arrayHausverbrauch)).val);
const [maxLadeleistungE3DC_W] = await Promise.all([
getStateAsync(sID_Bat_Charge_Limit)
]).then(states => states.map(state => state.val));
// Aktuelle Ladeleistung ermitteln (minimale Ladeleistung zwischen User und E3DC)
const maxLadeleistung = Math.min(maxLadeleistungUser_W, maxLadeleistungE3DC_W);
const maxLadeleistung_kW = (maxLadeleistung-200) / 1000; // 200W abziehen da E3DC nicht auf eingestellten Wert regelt sondern drunter bleibt
// Aktuellen Wochentag und Zeitintervall (Tag/Nacht) bestimmen
const now = new Date();
const currentDay = now.toLocaleDateString('de-DE', { weekday: 'long' });
// Hausverbrauch berechnen
const hausverbrauch_day_kWh = hausverbrauch[currentDay]['day'] / 1000;
const hausverbrauch_night_kWh = hausverbrauch[currentDay]['night'] / 1000;
// Durchschnittlicher Verbrauch in kWh pro Stunde
const durchschnittlicherVerbrauch_kWh = (hausverbrauch_day_kWh + hausverbrauch_night_kWh) / 2;
let ladezeitInStunden = 0;
// Berechnung auf Basis der Dauer in Stunden (dauer_h)
if (dauer_h !== null ) {
// Benötigte Energie in kWh für die gegebene Dauer
const benoetigteEnergie_kWh = round(durchschnittlicherVerbrauch_kWh * dauer_h, 2);
// Ladezeit basierend auf der Energie und Ladeleistung berechnen
ladezeitInStunden = benoetigteEnergie_kWh / maxLadeleistung_kW;
}
// Berechnung auf Basis des Start-Ladezustands (startSOC)
if (startSOC !== null) {
const zuLadendeProzent = maxBatterieSoC - startSOC;
if (zuLadendeProzent > 0) {
// Berechnung der zu ladenden Kapazität in kWh
const zuLadendeKapazitaet_kWh = (batterieKapazitaet_kWh * zuLadendeProzent) / 100;
// Berechnung der Ladezeit in Stunden
ladezeitInStunden = zuLadendeKapazitaet_kWh / maxLadeleistung_kW;
}
}
// Ladezeit aufrunden und in den Status setzen (nur wenn SoC genutzt wird)
if (startSOC !== null) {
await setStateAsync(sID_ladezeitBatterie, Math.ceil(ladezeitInStunden));
}
return Math.ceil(ladezeitInStunden);
} catch (error) {
log(`Fehler in Funktion berechneLadezeit: ${error.message}`, 'error');
}
}
// Setzt Timer Batterie Laden für Startzeit und Endzeit
async function setStateAtSpecificTime(targetTime, stateID, state) {
try {
LogProgrammablauf += '29,';
const currentTime = new Date(); // Aktuelle Zeit abrufen
const targetDate = new Date(targetTime);
if (!Array.isArray(timerObjektID) || !Array.isArray(timerState)) {
throw new Error("timerObjektID oder timerState ist kein gültiges Array");
}
// Prüfen ob timer bereits gesetzt wurde
const z = timerTarget.findIndex(time => time.getTime() === targetDate.getTime());
if(z > 0){
if(timerObjektID[z] == stateID && timerState[z] == state){
LogProgrammablauf += '29/1,';
return;
}
}
// Prüfen ob Objekt ID mit gleichem State bereits gesetz wurde, dann vorher löschen
if (!Array.isArray(timerObjektID) || !Array.isArray(timerState)) {
throw new Error("timerObjektID oder timerState ist kein gültiges Array");
}
const objektID = {
[sID_BatterieEntladesperre]: 'Entladesperre',
[sID_BatterieLaden]: 'Laden',
[sID_eAutoLaden]: 'Auto'
}[stateID] || '';
const matchingIndices = timerObjektID
.map((id, index) => (id === objektID && timerState[index] === state ? index : -1))
.filter(index => index !== -1);
if (matchingIndices.length > 0) {
for (const index of matchingIndices) {
clearTimeout(timerIds[index]);
timerObjektID.splice(index, 1);
timerIds.splice(index, 1);
timerTarget.splice(index, 1);
timerState.splice(index, 1);
// Bedingung prüfen und setStateAsync aufrufen
if (objektID === 'Laden' && state === true) {
await setStateAsync(sID_timerAktiv, false);
}
}
}
if (!(targetTime instanceof Date) || isNaN(targetDate.getTime())) {
LogProgrammablauf += '29/2,';
log(`Fehler in function setStateAtSpecificTime,targetTime ist kein date Objekt / targetTime = ${targetTime} `, 'warn')
return;
}
// @ts-ignore Zeitdifferenz berechnen
let timeDiff = targetDate.getTime() - currentTime.getTime();
// Wenn Startzeit in der vergangeheit, ignorieren
if(timeDiff > 0){
// Timeout setzen, um den State nach der Zeitdifferenz zu ändern
LogProgrammablauf += '29/3,';
let id = setTimeout(async () => {
try {
await setStateAsync(stateID, state);
if (stateID === sID_BatterieLaden && state === false) {await setStateAsync(sID_timerAktiv, false);}
if (stateID === sID_BatterieEntladesperre && state === false) {battSperrePrio = false;}
if (stateID === sID_BatterieEntladesperre && state === true) {battSperrePrio = true;}
log(`State ${stateID} wurde durch Timer um ${targetTime.toLocaleTimeString()} auf ${state} gesetzt.`, 'warn');
} catch (error) {
log(`Fehler im Timer function setStateAtSpecificTime: ${error.message}`, 'error');
}
}, timeDiff);
timerObjektID.push(objektID);
timerIds.push(id);
timerTarget.push(targetTime)
timerState.push(state)
if(objektID == 'Laden'){await setStateAsync(sID_timerAktiv, true);}
}
} catch (error) {
log(`Fehler in Funktion setStateAtSpecificTime: ${error.message}`, 'error');
}
}
// Die Funktion bestLoadTime dient dazu, innerhalb eines bestimmten Zeitraums den günstigsten Startzeitpunkt
// für eine Ladezeit (in Stunden) zu ermitteln, basierend auf den Preisdaten Tibber.
function bestLoadTime(dateStartTime, dateEndTime, nladezeit_h) {
try {
// Variablen für günstigsten Ladezeitblock initialisieren
let billigsterBlockPreis = Infinity;
let billigsterPreis = Infinity;
let billigsteZeit = null;
// Konvertiere Start- und Endzeit zu Datumsobjekten, falls notwendig
dateStartTime = new Date(dateStartTime);
dateEndTime = new Date(dateEndTime);
// Überprüfe, ob Start- und Endzeiten gültig sind
if (isNaN(dateStartTime) || isNaN(dateEndTime)) {
log(`function bestLoadTime: Ungültiges Start- oder Enddatum`, 'error');
return null;
}
// Validierung der globalen Variable datenTibberLink48h
if (!Array.isArray(datenTibberLink48h) || datenTibberLink48h.length === 0) {
log(`function bestLoadTime: datenTibberLink48h ist keine gültiges Array oder enthält keine Werte.`, 'error');