Combinatie node-red/HASS en veel javascript. Ik heb wel eens alfaversie gedeeld in het andere forum, maar ben nu nog de foutjes er uit aan het halen en de code wat aan het opschonen.
Verdeel functie targetpower over n accu's:
code:
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
| //**************** alles in 1 functie voor NOM, (bij)kopen en verkopen ************************* */
function distributePower(batteries, targetPower, gridPower, mode, xomL1, xomL2) {
// Accu's compenseren de gridPower binnen toegestane targetPower limiet
let remainingPower = 0;
let totalPower = 0;
let buySell = 0;
let totalAvailable = Math.min(targetPower, batteries.reduce((sum, b) => sum + (mode === 'charge' ? b.maxCharge : b.maxDischarge), 0));
let powerPlan = batteries.map(battery => ({ battery: battery.id, power: 0, xom: 'power', soc: battery.soc }));
//Sorteer de accu's op maximale SOC bij ontladen en minimale SOC bij laden als verschil 10% is
const getActiveBattery = (batteries, mode, delta, last) => {
//Werkt SOC, maxCharge en MaxDischarge van vorige volgorde bij met actuele waarden.
const updatedLast = last.map(b => {const fresh = batteries.find(c => c.id === b.id); return fresh ? { ...b, ...fresh } : b; });
const sorted = [...batteries].sort((a, b) => mode === 'charge' ? a.soc - b.soc : b.soc - a.soc);
const diff = Math.abs((updatedLast[0]?.soc ?? 0) - (updatedLast[1]?.soc ?? 0));
const useNewOrder = !last.length || diff >= delta;
const order = useNewOrder ? sorted : updatedLast;
return { activeAccu: order[0].id, mode: mode, order };
};
let lastFlow = flow.get('Last');
let last = (lastFlow && lastFlow.order) ? lastFlow.order : []; //vorige volgorde en actieve accu
last = getActiveBattery(batteries, charge_discharge, 10, last);
batteries = last.order;
flow.set('Last',last);
// verdelen beschikbaar vermogen over de accu's
for (let i = 0; i < batteries.length && totalAvailable > 0; i++) {
let battery = batteries[i];
let maxPossible = Math.min((mode === 'charge' ? battery.maxCharge : battery.maxDischarge), totalAvailable);
let j = battery.id; //vul power in juiste accu (powerPlan heeft normale volgeorde 0,1,2)
powerPlan[j].power = maxPossible;
totalAvailable -= maxPossible;
}
// level1: zorgt ervoor dat vermogen 0W is (idle < 80% efficiency)
// Level2: Zorg dat elk vermogen minstens bv 200W is (XOM > 85/90% efficiency )
powerPlan = powerPlan.map(p => ({
battery: p.battery,
power: p.power != 0 && p.power < xomL2 ? (p.power < xomL1 ? 0 : xomL2) : p.power,
xom: p.power != 0 && p.power < xomL2 ? (p.power < xomL1 ? 'XOM0' : 'XOM'+xomL2) : p.xom,
soc: p.soc
}));
for (let i = 0; i < batteries.length ; i++) {
totalPower += powerPlan[i].power;
}
//sorteer de accus op battery (=id) //waarschijnlijk niet nodig
powerPlan.sort((a, b) => a.battery - b.battery);
// laden/ontladen
totalPower = (mode === 'charge' ? totalPower : -1 * totalPower);
// buy of sell
buySell = gridPower + totalPower;
//remaining power
remainingPower = gridPower + totalPower;
//mode idle als totalPower == 0
mode = (totalPower === 0 ? 'idle' : mode);
//powerPlan: per accu
//buySell: kopen/verkopen
//remainingPower: hoofdmeter na charge/discharge
//totalPower: geladen/ontladen
//Mode: charge/discharge/idle
// waardes kunnen positief en negatief zijn
totalPower = Math.abs(totalPower);
//buySell = Math.abs(buySell);
return { powerPlan, gridPower, remainingPower, mode, totalPower, buySell};
} |
Functie kopen. bijkopen, verkopen, nom, nom+, nom-:
code:
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
| //************************************************************************************ */
//functie was bedoeld voor marges in €. Dit leidt tot schijnnauwkeurigheid en grotere complaxiteit.
//functie werkt nu op basis prijsLevel=prijsNu/prijsGemiddeld: -6 (zeer goedkoop),-5,-4,-3,-2 (zeer duur). Werkt met/zonder saldering
// PV heeft voorrang
// bijkopen als vulgraad tussen accu.min_level en dyn_min_level (wat nodig is voor eigen verbruik vanaf accu.mim_level).
// kopen/verkopen als vulgraad tussen dyn_min_level en dyn_max_level (tot accu.inhoud ruimte voor PV).
//NB: met kleine accu's zijn dynamische levels niet erg zinvol.
// accu obj accusTotaal met som accu's (inhoud, minimum_level, vulgraad).
function maakBeslissing(accu, prijslevelKoopNu, prijslevelVerkoopNu, prijsLevelkoop, prijsLevelverkoop, strategie, dynMinLevel, pvVoldoende) {
let beslissing = "geen actie";
let reden = 'Alles OK';
let nom = false;
let bijkopen = false;
let kopenVerkopen = false;
let minLevel = dynMinLevel;
switch (strategie) {
case 0: nom = true; break;
case 1: nom = true; bijkopen = true; break;
case 2: nom = true; bijkopen = true; kopenVerkopen = true; break;
case 3: kopenVerkopen = true; minLevel = accu.minimum_level; break;
}
if (prijslevelKoopNu <= prijsLevelkoop && accu.vulgraad > minLevel && accu.vulgraad < accu.inhoud && kopenVerkopen) {
beslissing = 'kopen';
} else if (prijslevelVerkoopNu >= prijsLevelverkoop && accu.vulgraad > minLevel && accu.vulgraad < accu.inhoud && kopenVerkopen) {
beslissing = 'verkopen';
} else if (prijslevelKoopNu <= prijsLevelkoop && accu.vulgraad < minLevel && bijkopen) {
beslissing = 'bijkopen';
} else if (nom) {
beslissing = 'NOM';
}
// ✳️ Blokkeer verkopen als zelfverbruik prioriteit heeft
if (beslissing === 'verkopen' && strategie < 3) {
reden = 'Niet verkopen, zelfverbruik gaat voor';
beslissing = 'geen actie';
}
// ✳️ Blokkeer (bij)kopen als er voldoende PV-opwek is
if ((beslissing === 'kopen' || beslissing === 'bijkopen') && pvVoldoende === true) {
reden = 'Niet (bij)kopen, voldoende PV opbrengst';
beslissing = nom ? 'NOM' : 'geen actie';
}
// ➕ Do-nothing zone: geen actie als prijs neutraal is bij strategie 3 (arbitrage)
if (strategie === 3 && beslissing === 'geen actie' &&
prijslevelKoopNu > prijsLevelkoop &&
prijslevelKoopNu < prijsLevelverkoop) {
reden = 'Do-nothing zone: prijs is neutraal';
beslissing = 'geen actie';
}
// ✳️ NOM+: alleen laden, NOM−: alleen ontladen
if (beslissing === 'NOM' && prijslevelKoopNu <= prijsLevelkoop) {
reden = 'Alleen laden';
beslissing = 'NOM+';
} else if (beslissing === 'NOM' && prijslevelVerkoopNu >= prijsLevelverkoop) {
reden = 'Alleen ontladen';
beslissing = 'NOM-';
}
// ➕ ochtendreserve handhaven tbv WP in dure ochtenduren
//if (accu.vulgraad < ochtendReserve) {
//reden = 'Niet ontladen, ochtendreserve behouden';
//beslissing = 'geen actie';
return { beslissing, reden };
} |
Onderdeel main programma met aanroep beide functies
Hoofdmeter (=grid uit P1) bepalen
code:
1
2
3
4
5
| if (firsttime) {
hoofdmeter = 0
} else {
hoofdmeter += (planLasttime.mode == 'charge' ? -1 * planLasttime.totalPower : planLasttime.totalPower)
} |
De variabele targetPower is afhankelijk aantal accu's en hoe aangesloten. Indien iedere accu op een eigen groep dan kan targetpower =7500 (= n * max W (ont)laden).
Zoals ik nu (tijdelijk) 3 accu's op 1 lege groep heb staan beperk ik targetPower = 3600. Een vraag (ont)laden van 3000W wordt dan door de functie distributePower verdeeld over 2 van de 3 accu's: 2500W en 500W.
Verder zorgt deze functie er ook voor dat de onderlinge verschillen in SOC niet groter dan 10% wordt door het wisselen van actieve accu. Hierdoor worden de accu's minder heet.
Functies aanroepen:
code:
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
| //functie aanroep voor bepalen (bij)kopen (<= -5: goedkoop), verkopen (>= -3: duur) of NOM/NOM+/NOM- (-4: gemiddeld). marge is dan meestal minimaal €0,06
var status = maakBeslissing(AccuTotaal, prijslevel_koop, prijslevel_verkoop, -5, -3, strategie, dynMinLevel, pv_voldoende);
//targetPower bepalen voor verdeling over de accu's
if (status.beslissing == "kopen" || status.beslissing == 'bijkopen') { charge_discharge = 'charge'; targetPower = targetPowerLaden }
if (status.beslissing == "verkopen" ) { charge_discharge = 'discharge'; targetPower = targetPowerOntladen }
if (status.beslissing == 'NOM' || status.beslissing == 'NOM+' || status.beslissing == 'NOM-'){
if (hoofdmeter <= 0 ){
targetPower = Math.min(targetPowerLaden, -1 * hoofdmeter);
charge_discharge = 'charge'
if (status.beslissing == 'NOM-') { targetPower = 0 }
} else {
targetPower = Math.min(targetPowerOntladen, (hoofdmeter - reserveGridPower)); //reserveGridPower (bv autoladen) buiten ontladen accu houden
targetPower = targetPower <= 0 ? 0 : targetPower;
charge_discharge = 'discharge'
if (status.beslissing == 'NOM+') { targetPower = 0 }
}
}
if (status.beslissing == 'geen actie'){ charge_discharge = 'charge'; targetPower = 0 }
//** zorgt ervoor dat eerste de 1e accu optimaal (met maxontladen vermogen) wordt geladen/ontladen en daarna de 2e met het restvermogen tot targetpower*/
//** met bv vermogen_laden/ontladen = targetPower/3 worden beide accu's gelijkmatig geladen/ontladen */
for (let i = 0; i < aantal_accus; i++) {
//vermogen (ont)laden bepalen
Marstek[i].vermogen_ontladen = (Marstek[i].vulgraad <= Marstek[i].minimum_level ? 0 : Marstek_vermogen_ontladen_max);
Marstek[i].vermogen_laden = (Marstek[i].vulgraad >= Marstek[i].inhoud ? 0 : Marstek_vermogen_laden_max);
//** correcties */
//bij >95% SOC wordt Marstek_vermogen_laden_max getrottled tot 1200W
Marstek[i].vermogen_laden = (Marstek[i].soc >= 95 && Marstek[i].vermogen_laden > 1200 ? 1200 : Marstek[i].vermogen_laden);
//bij <15% SOC wordt Marstek_vermogen_ontladen_max getrottled tot 1200W
Marstek[i].vermogen_ontladen = (Marstek[i].soc <= 15 && Marstek[i].vermogen_ontladen > 1200 ? 1200 : Marstek[i].vermogen_ontladen);
}
//** Functie aanroep distributieplan met verdeling targetPower: laden/ontladen per accu, remaining power en buy/sell */
//** Houdt rekening met XOM levels*/
var batteries = [];
for (let i = 0; i < aantal_accus; i++){
batteries.push({ id:i, maxCharge: Marstek[i].vermogen_laden, maxDischarge: Marstek[i].vermogen_ontladen, soc: Marstek[i].soc })
}
var plan = distributePower(batteries, targetPower, hoofdmeter, charge_discharge, xom_level1, xom_level2); |
Dit levert het volgende plan-object op, waarbij "power" forcible charge of forcible discharge is in combinatie met "mode" (charge, discharge of idle):
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| object
powerPlan: array[3]
0: object
battery: 0
power: 0
xom: "power"
soc: 100
1: object
battery: 1
power: 251
xom: "power"
soc: 94
2: object
battery: 2
power: 0
xom: "power"
soc: 100
gridPower: -251
remainingPower: 0
mode: "charge"
totalPower: 251
buySell: 0 |
Aansturing lilygo's via HASS:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| //*********************** aansturing **************************************/
var correctionDischarge = 0; // indien niet leeg.
var correctionCharge = 0; // indien niet vol.
msg1.payload = (plan.mode == 'charge' && plan.powerPlan[0].power > 0 ? plan.powerPlan[0].power + correctionCharge : 0);
msg2.payload = (plan.mode == 'discharge' && plan.powerPlan[0].power > 0 ? plan.powerPlan[0].power + correctionDischarge: 0);
msg3.payload = (plan.mode == 'charge' && plan.powerPlan[1].power > 0 ? plan.powerPlan[1].power + correctionCharge : 0);
msg4.payload = (plan.mode == 'discharge' && plan.powerPlan[1].power > 0 ? plan.powerPlan[1].power + correctionDischarge: 0);
msg5.payload = (plan.mode == 'charge' && plan.powerPlan[2].power > 0 ? plan.powerPlan[2].power + correctionCharge : 0);
msg6.payload = (plan.mode == 'discharge' && plan.powerPlan[2].power > 0 ? plan.powerPlan[2].power + correctionDischarge: 0);
//************************************************************************** */
msg7.payload = (plan.mode == 'charge' ? plan.totalPower : -plan.totalPower); //geladen/ontladen == accu_in/accu_uit ( > 0 icm charge/discharge)
msg8.payload = plan.mode; // charge/dicharge/idle
msg9.payload = plan.buySell; //kopen/vekopen (buy >0 ; sell <0)
msg10.payload = 'M'+activeAccu; // P1 DN |
[
Voor 148% gewijzigd door
JanAllElectric op 28-07-2025 20:11
]
Panasonic TCAP 12kW J-versie + Heishamon/HA/Node-Red/Grafana/InfluxDB; Atlantic v3 200L; 5* jaga strada 21 & zelfbouw DBE; 3*2400Wp (O,Z,W); KIA EV6 77kWh RWD + EVCC/cFos Wallbox solar; 3* Marstek 5kWh (v151)+CT003 (v114)+ modbus/lilygo/node-red/HA