EcoFlow River 2 (Pro) als USV und Batteriespeicher
Nachdem meine USV von CyberPower nach ungefähr 5 Jahren nicht mehr wollte, kam eine E-Mail von EcoFlow gerade zur rechten Zeit in mein Postfach. Ich dachte, dass es doch eine ganz coole Idee ist, die tragbaren Batteriespeicher als USV zu nutzen. Dank Integration per API in das SmartHome-System, könnte man sogar ein Teil der Akkukapazität für Überschussladen per PV-Anlage nutzen. Also den Eigenverbrauch optimieren. So finanziert sich das Gerät ja theoretisch mit der Zeit selbst. Welche USV macht das schon? Ob das klappt, erfährst Du in diesem Beitrag.
Generell gibt es verschiedene Geräte von EcoFlow, welche eigentlich als tragbare Powerstation konzipiert wurden. Die erste Serie nennt sich Delta und wird auf der Webseite teilweise auch als “Smart Home Ökosystem”-Lösung auf der Webseite präsentiert. Jetzt frage ich mich, ob das auch für die kleineren und günstigeren Geräte der River Reihe gilt. Mir wurde als für diesen Beitrag eine River 2 Pro zur Verfügung gestellt.
Als Schnittstellen zur Powerstation gibt es Bluetooth und WiFi. Über beide Schnittstellen kann mit der App auf das Gerät zugegriffen werden. Die Frage ist: Wie offen ist diese Schnittstelle und ist für die Verbindung unbedingt eine Cloud erforderlich? Das finden wir gemeinsam im folgenden YouTube-Video heraus.
Video
Verwendung als USV?
Laut Webseite können die Geräte für eine “Notstromfunktion” genutzt werden. Das heißt, dass man einfach an die Steckdosen etwas anschließen kann, was dann weiterhin mit Energie versorgt wird, wenn der Strom ausfallen sollte. Die EcoFlow Powerstation hängt also zwischen Gerät und Steckdose und dient damit als USV (Unterbrechungsfreie Stromversorgung). Und genau das möchte ich mit meinem Rack machen, um die CyberPower USV zu ersetzen. Ob das klappt? Werden wir sehen.
EcoFlow zeigt in ihrem Blog selbst auf, was genau die Unterschiede zu einer USV sind.
In meinem Szenario hat die Umschaltung bisher immer gut geklappt und die Geräte bleiben an. Das heißt aber nicht, dass das optimal ist. Selbst der Hersteller schlägt vor, lieber eine “richtige” USV zu verwenden. Aber eventuell möchtest Du ja trotzdem die Powerstation mit PV-Überschuss laden?
Produkt-Links
Die API
Wenn man beim Support des Herstellers anfragt, bekommt man einen API-Key und ein API-Secret zur Verfügung gestellt. Mit diesen Informationen kann man dann (offiziell) nur diesen Request ausführen:
curl -X GET -H 'Content-Type: application/json' -H 'appKey: xxx' -H 'secretKey: yyy' 'https://api.ecoflow.com/iot-service/open/api/device/queryDeviceQuota?sn=R6XXXXXXXXXXXXXXX'
Als Response erhalte ich z.B. gerade folgendes:
{
"code": "0",
"message": "Success",
"data": {
"soc": 81,
"remainTime": 5999,
"wattsOutSum": 0,
"wattsInSum": 0
}
}
Man bekommt also nur:
- Den Ladestand (SoC = State of Charge)
- Die Restlaufzeit bei aktueller Ausgangs-Leistung
- Die aktuelle Ausgangsleistung
- Die aktuelle Eingangsleistung
Das war es auch schon. Dank einiger Nutzer im ioBroker Forum wurde aber noch viel mehr herausgefunden. Ein dickes Dankeschön an Netfreak25 aus dem ioBroker-Forum!
Leider (?) sind viele API-Requests aus dem Beitrag nicht mehr aktuell, weil EcoFlow auf MQTT umgestellt hat. Das finde ich super - wenn man nur einfach an die Zugangsdaten käme. Hier jedenfalls noch die Requests, welche für mich weiterhin funktioniert haben. Ich probiere das alles mal etwas sauberer zu dokumentieren und zusammenzufassen.
Geräte-Klassen
- Url:
https://api.ecoflow.com/app/productClass/classTree
- Auth: Nicht notwendig
Liefert eine Liste aller existierenden (?) EcoFlow-Geräteklassen (Power Stations, Smartdevices, Power Solutions, …).
Beispiel-Response (nur ein Auszug aus der Liste für Portable Power Stations):
{
"id": "1268653908146913280",
"pid": "0",
"name": "Portable Power Stations",
"langInput": {
"zh_CN": "移动储能",
"en_US": "Portable Power Stations",
"fr_FR": "Stations électriques portables",
"de_DE": "Tragbare Powerstations",
"ja_JP": "ポータブル電源",
"es_ES": "Estaciones de energía portátiles",
"ko_KR": "휴대용 파워뱅크",
"ru_RU": "Портативные энергетические станции",
"it_IT": "Power station portatili",
"nl_NL": "Draagbare laadstations"
},
"sort": 10,
"createTime": "2022-09-06 09:12:33"
}
Device-List
- Url:
https://api.ecoflow.com/app/productClass/productUsingList
- Auth: Nicht notwendig
Liefert eine Liste aller existierenden (?) EcoFlow-Produkte.
Beispiel-Response (nur ein Auszug aus der Liste für meine River 2 Pro):
{
"id": "1268655875241934848",
"skuIdDTOList": [
{
"skuId": "1515325392569892865",
"skuName": "RIVER 2 Pro",
"spuId": "1515325392553115650",
"productType": 72,
"model": 1
}
],
"skuClassId": "1268653908146913280",
"sort": 10,
"status": 1,
"createTime": "2022-09-06 09:16:23",
"updateTime": "2022-10-28 14:09:45"
}
Hier taucht auch wieder die Class-ID skuClassId
von oben auf.
Upgrade-Status
- Url:
https://api.ecoflow.com/iot-service/device/upgradeStatus?sn=R6XXXXXXXXXXXXXXX"
- Auth: Mit
appKey
undsecretKey
-Header vom Support (siehe oben)
Beispiel-Response:
{
"code": "0",
"message": "Success",
"data": {
"deviceUpgradeStatus": 0,
"wifiUpgradeStatus": 0,
"wifiCode": "10002",
"wifiAbleRetry": 1,
"code": "10002",
"ableRetry": 1
}
}
MQTT
Wie oben schon erwähnt, läuft ein Großteil der Kommunikation mittlerweile über MQTT (meinem Lieblings-Protokoll!). Um an die Zugangsdaten zu kommen, braucht es mehrere Schritte.
Schritt 1: User-ID und Token ermitteln
Als Erstes brauchen wir die User-ID und das Token des Cloud-Benutzers. Dafür führst Du folgenden Request durch (email
und password
muss angepasst werden):
Das Passwort muss Base64-Encodiert übergeben werden! Dafür kannst Du Seiten wie base64encode.org nutzen (oder die gängigen Tools unter Linux). Hier der Aufruf mit curl
von einem Linux-System, welcher Dir die nötigen Informationen zurückliefert:
curl -H 'content-type: application/json' --data-binary '{"appVersion":"4.1.2.02","email":"blabla@bla.de","os":"android","osVersion":"30","password":"PASSWORDBASE64=","scene":"IOT_APP","userType":"ECOFLOW"}' --compressed 'https://api.ecoflow.com/auth/login'
Beispiel-Response mit userid
und token
:
{
"code": "0",
"message": "Success",
"data": {
"user": {
"userId": "123123123123123123",
"email": "blabla@bla.de",
"name": "EcoFlow User",
"icon": "https://websiteoss.ecoflow.com/user/icon/20220802/deltamax.png",
"state": 0,
"regtype": "email",
"destroyed": "NORMAL",
"registerLang": "en_US",
"administrator": false,
"appid": 60
},
"token": "TOKEN..."
}
}
Schritt 2: MQTT-Zugangsdaten vom Server abfragen
Jetzt können wir die MQTT-Zugangsdaten abholen. Dafür führst Du folgenden Request durch (TOKEN
und userId
muss natürlich angepasst werden - siehe Schritt 1):
curl -H "authorization: Bearer TOKEN" --compressed 'https://api.ecoflow.com/iot-auth/app/certification?userId=123123123123123123'
Beispiel-Response mit certificateAccount
und certificatePassword
:
{
"code": "0",
"message": "Success",
"data": {
"url": "mqtt.ecoflow.com",
"port": "8883",
"protocol": "mqtts",
"certificateAccount": "app-xxx",
"certificatePassword": "yyy"
}
}
Wichtig ist, dass man sich mit der richtigen Client-ID mit dem MQTT-Broker verbindet - ansonsten wird die Verbindung (seit März 2023) abgelehnt! Diese setzt sich wie folgt zusammen:
ANDROID_<UUID>_<userId>
Also zum Beispiel:
ANDROID_FF35C21C-D9D2-4709-9664-DF617C905138_123123123123123123
Mit den Daten kann man sich per MQTTS am Broker anmelden. Danach stehen folgende Topics zur Verfügung (Wildcards funktionieren wohl nicht so gut oder gar nicht):
/app/device/property/R6XXXXXXXXXXXXXXX
/app/<userid>/R6XXXXXXXXXXXXXXX/thing/property/set
(Setzen von Eigenschaften)/app/<userid>/R6XXXXXXXXXXXXXXX/thing/property/get
(Lesen von Eigenschaften)
Das erste Topic kann man zum Beispiel mit mosquitto_sub
abonnieren, um weitere Infos zu bekommen:
mosquitto_sub -h mqtt.ecoflow.com -p 8883 -t /app/device/property/R6XXXXXXXXXXXXXXX -u app-xxx -P yyy -i ANDROID_FF35C21C-D9D2-4709-9664-DF617C905138_123123123123123123
Beispiel-Ausgabe (Module-Type 1):
{
"id": 1364004048192540306,
"version": "1.0",
"timestamp": 1673526970,
"moduleType": "1",
"params": {
"pd.typec1Temp": 0,
"pd.qcUsb1Watts": 0,
"pd.wattsInSum": 0,
"pd.dcInUsedTime": 0,
"pd.wifiVer": 0,
"pd.ext3p8Port": 0,
"pd.dsgPowerDC": 44,
"pd.chgPowerDC": 0,
"pd.model": 0,
"pd.wifiAutoRcvy": 0,
"pd.standbyMin": 30,
"pd.beepMode": 0,
"pd.remainTime": 5999,
"pd.typecUsedTime": 1613,
"pd.typec2Watts": 0,
"pd.ext4p8Port": 0,
"pd.brightLevel": 100,
"pd.typecChaWatts": 0,
"pd.usbqcUsedTime": 0,
"pd.dcOutState": 0,
"pd.chgSunPower": 0,
"pd.wattsOutSum": 0,
"pd.carTemp": 0,
"pd.usbUsedTime": 0,
"pd.mpptUsedTime": 0,
"pd.icoBytes": [0, 0, 128, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"pd.usb1Watts": 0,
"pd.dsgPowerAC": 498,
"pd.qcUsb2Watts": 0,
"pd.wifiRssi": 0,
"pd.wireWatts": 0,
"pd.chgPowerAC": 1573,
"pd.lcdOffSec": 300,
"pd.extRj45Port": 0,
"pd.errCode": 0,
"pd.sysVer": 33620278,
"pd.carWatts": 0,
"pd.typec2Temp": 0,
"pd.carUsedTime": 154867,
"pd.typec1Watts": 0,
"pd.chgDsgState": 0,
"pd.usb2Watts": 0,
"pd.soc": 81,
"pd.carState": 0,
"pd.invUsedTime": 24115
}
}
Beispiel-Ausgabe (Module-Type 2):
{
"id": 1364008446239051239,
"version": "1.0",
"timestamp": 1673527482,
"moduleType": "2",
"params": {
"bms_bmsStatus.sysVer": 50397504,
"bms_bmsStatus.minCellTemp": 25,
"bms_bmsStatus.designCap": 40000,
"bms_bmsStatus.temp": 25,
"bms_bmsStatus.minCellVol": 3308,
"bms_bmsStatus.cycles": 1,
"bms_bmsStatus.f32ShowSoc": 80.7,
"bms_bmsStatus.outputWatts": 0,
"bms_bmsStatus.maxCellVol": 3308,
"bms_bmsStatus.type": 1,
"bms_bmsStatus.soh": 0,
"bms_bmsStatus.maxCellTemp": 25,
"bms_bmsStatus.remainCap": 29065,
"bms_bmsStatus.cellId": 2,
"bms_bmsStatus.minMosTemp": 25,
"bms_bmsStatus.vol": 19,
"bms_bmsStatus.remainTime": 0,
"bms_bmsStatus.fullCap": 35984,
"bms_bmsStatus.bqSysStatReg": 0,
"bms_bmsStatus.openBmsIdx": 1,
"bms_bmsStatus.amp": 0,
"bms_bmsStatus.num": 0,
"bms_bmsStatus.bmsFault": 0,
"bms_bmsStatus.soc": 81,
"bms_bmsStatus.errCode": 0,
"bms_bmsStatus.inputWatts": 0,
"bms_bmsStatus.tagChgAmp": 0,
"bms_bmsStatus.maxMosTemp": 25
}
}
Beispiel-Ausgabe (Module-Type 3):
{
"id": 1364008446031038526,
"version": "1.0",
"timestamp": 1673527482,
"moduleType": "3",
"params": {
"inv.dcInVol": 0,
"inv.cfgAcWorkMode": 0,
"inv.SlowChgWatts": 0,
"inv.dcInAmp": 0,
"inv.cfgAcOutFreq": 2,
"inv.outputWatts": 0,
"inv.errCode": 0,
"inv.dcInTemp": 0,
"inv.invOutFreq": 0,
"inv.chargerType": 1,
"inv.reserved": [0, 0, 0, 0, 0, 0, 0, 0],
"inv.acInAmp": 96,
"inv.fanState": 0,
"inv.cfgAcXboost": 0,
"inv.cfgAcEnabled": 0,
"inv.outTemp": 34,
"inv.invType": 0,
"inv.cfgAcOutVol": 0,
"inv.acDipSwitch": 0,
"inv.acInVol": 236104,
"inv.invOutVol": 0,
"inv.FastChgWatts": 0,
"inv.inputWatts": 0,
"inv.standbyMins": 0,
"inv.chgPauseFlag": 0,
"inv.acInFreq": 50,
"inv.dischargeType": 0,
"inv.invOutAmp": 0,
"inv.sysVer": 67174695
}
}
Beispiel-Ausgabe (Module-Type 5):
{
"id": 1364008446127901215,
"version": "1.0",
"timestamp": 1673527482,
"moduleType": "5",
"params": {
"mppt.carOutVol": 0,
"mppt.carState": 0,
"mppt.dischargeType": 0,
"mppt.faultCode": 4096,
"mppt.dc24vState": 0,
"mppt.cfgAcXboost": 1,
"mppt.carTemp": 27,
"mppt.outWatts": 0,
"mppt.swVer": 50397504,
"mppt.x60ChgType": 0,
"mppt.carOutAmp": 0,
"mppt.outAmp": 0,
"mppt.chgPauseFlag": 0,
"mppt.dcdc12vWatts": 0,
"mppt.acStandbyMins": 360,
"mppt.powStandbyMin": 0,
"mppt.inWatts": 0,
"mppt.dcdc12vVol": 0,
"mppt.inAmp": 0,
"mppt.scrStandbyMin": 300,
"mppt.inVol": 2862,
"mppt.carOutWatts": 0,
"mppt.mpptTemp": 0,
"mppt.outVol": 19850,
"mppt.cfgAcEnabled": 0,
"mppt.chgType": 0,
"mppt.res": [0, 0, 0, 0, 0],
"mppt.dcdc12vAmp": 0,
"mppt.beepState": 0,
"mppt.cfgAcOutVol": 230,
"mppt.cfgChgType": 0,
"mppt.dc24vTemp": 0,
"mppt.carStandbyMin": 0,
"mppt.dcChgCurrent": 8000,
"mppt.chgState": 0,
"mppt.cfgChgWatts": 100,
"mppt.cfgAcOutFreq": 50
}
}
Wie man sieht, gibt es mehrere moduleType
. Aus meiner Sicht sind diese wie folgt aufgebaut:
- 1:
PD
(Power Delivery ?) - 2:
BMS
(Battery Management System) - 3:
INV
(Inverter / Wechselrichter) - 4:
BMS_SLAVE
- 5:
MPPT
(Maximum Power Point Tracking)
Es sind nicht immer alle Eigenschaften in jeder Nachricht enthalten.
Spannend sind für mein Vorhaben:
mppt.cfgChgWatts
- Maximale AC-Ladeleistung des Akkus in Watt (100 W bis 940 W)mppt.chgPauseFlag
- Laden pausiertbms_emsStatus.maxChargeSoc
- Maximaler Ladestand (50 % bis 100 %)bms_emsStatus.minDsgSoc
- Minimaler Ladestand (0 % bis 30 %)bms_bmsStatus.cycles
- Zyklen des Akkusbms_bmsStatus.temp
- Temperatur des Akkus (?)bms_emsStatus.fanLevel
- Lüfter (abhängig von Temperatur)pd.soc
oderbms_bmsStatus.soc
- Aktueller Ladestand in Prozent (0 bis 100)pd.remainTime
- Restliche Zeit in Minuten bis oberes Ladelimit erreicht istpd.soc
- Aktueller Ladestand in Prozent (0 bis 100)pd.wattsInSum
- Aktuelle Eingangsleistungpd.wattsOutSum
- Aktuelle Ausgangsleistung
Zusätzlich kann man sich noch das Topic /app/<userid>/R6XXXXXXXXXXXXXXX/thing/property/get
anschauen, hier bekomme ich aber nur folgende Nachricht, wenn ich die iOS-App starte und das Gerät auswähle:
{
"from": "iOS",
"operateType": "latestQuotas",
"id": "758907181",
"lang": "en-us",
"params": {},
"version": "1.0"
}
Schritt 3: Eigenschaften per MQTT setzen
Dafür muss man die Informationen an das richtige Topic (/app/<userid>/R6XXXXXXXXXXXXXXX/thing/property/set
) publishen. Auch hier muss wieder die userid
und die Seriennummer angepasst werden. Generell ist jede Nachricht wie folgt aufgebaut - nur die Parameter ändern sich (wahrscheinlich wäre es sinnvoll, die ID bei jedem Request hochzuzählen).
Wie habe ich die Nachrichten rausbekommen? Ich habe einfach das Set-Topic (siehe oben) abonniert und dann mit der App die Aktionen ausgelöst. So bekommt nicht nur die PowerStation als Client den Befehl, sondern logischerweise (danke an MQTT), auch unser zweiter Client, der das Topic abonniert hat. Beispiel:
mosquitto_sub -h mqtt.ecoflow.com -p 8883 -t /app/<userid>/R6XXXXXXXXXXXXXXX/thing/property/set -u app-xxx -P yyy -i ANDROID_FF35C21C-D9D2-4709-9664-DF617C905138_123123123123123123
Beispiel-Payload AC-Output aktivieren:
{
"params": {
"enabled": 1,
"out_freq": 255,
"out_voltage": 4294967295,
"xboost": 255
},
"from": "iOS",
"lang": "en-us",
"id": "132790809",
"moduleSn": "R6XXXXXXXXXXXXXXX",
"moduleType": 5,
"operateType": "acOutCfg",
"version": "1.0"
}
Beispiel-Payload 12V DC aktivieren:
{
"params": {
"enabled": 1
},
"from": "iOS",
"lang": "en-us",
"id": "824661624",
"moduleSn": "R6XXXXXXXXXXXXXXX",
"moduleType": 5,
"operateType": "mpptCar",
"version": "1.0"
}
Beispiel-Payload Charging-Speed setzen:
{
"params": {
"chgWatts": 940,
"chgPauseFlag": 0
},
"from": "iOS",
"lang": "en-us",
"id": "901577499",
"moduleSn": "R6XXXXXXXXXXXXXXX",
"moduleType": 5,
"operateType": "acChgCfg",
"version": "1.0"
}
Beispiel-Payload Charging-Limit auf 90% setzen:
{
"params": {
"maxChgSoc": 90
},
"from": "iOS",
"lang": "en-us",
"id": "956431191",
"moduleSn": "R6XXXXXXXXXXXXXXX",
"moduleType": 2,
"operateType": "upsConfig",
"version": "1.0"
}
Beispiel-Payload Entlade-Grenze auf 5% setzen:
{
"params": {
"minDsgSoc": 5
},
"from": "iOS",
"lang": "en-us",
"id": "477950683",
"moduleSn": "R6XXXXXXXXXXXXXXX",
"moduleType": 2,
"operateType": "dsgCfg",
"version": "1.0"
}
ioBroker-Integration
Starten wir mit der offiziellen API, welche ja nur relativ wenige Daten bereitstellt (siehe Oben). Dazu habe ich einfach ein Script geschrieben, welches per HTTP alle 5 Minuten die Daten abholt. Dazu wird die Seriennummer des Gerätes benötigt, der appkey und der secretkey (beim Support angefragt und bekommen).
const axios = require('axios').default;
// Change me
const prefix = '0_userdata.0.energy.powerstation';
const serialNumber = 'R6XXXXXXXXXXXXXXX';
const appKey = 'app-xxx';
const secretKey = 'yyy';
// Create states
createState(`${prefix}.soc`, {
name: 'State of charge',
role: 'state',
type: 'number',
read: true,
write: false,
unit: '%'
});
createState(`${prefix}.remainTime`, {
name: 'Charge time remaining',
role: 'state',
type: 'number',
read: true,
write: false,
unit: 'min.'
});
createState(`${prefix}.wattsInSum`, {
name: 'Power in (sum)',
role: 'state',
type: 'number',
read: true,
write: false,
unit: 'W'
});
createState(`${prefix}.wattsOutSum`, {
name: 'Power out (sum)',
role: 'state',
type: 'number',
read: true,
write: false,
unit: 'W'
});
schedule('*/5 * * * *', async () => {
try {
const ecoflowResponse = await axios.get(
`https://api.ecoflow.com/iot-service/open/api/device/queryDeviceQuota?sn=${serialNumber}`, {
headers: {
'Content-Type': 'application/json',
appKey,
secretKey
}
});
if (ecoflowResponse.status === 200) {
const resultObj = ecoflowResponse.data;
if (resultObj.code === '0') {
await setStateAsync(`${prefix}.soc`, { val: resultObj.data.soc, ack: true });
await setStateAsync(`${prefix}.remainTime`, { val: resultObj.data.remainTime, ack: true });
await setStateAsync(`${prefix}.wattsInSum`, { val: resultObj.data.wattsInSum, ack: true });
await setStateAsync(`${prefix}.wattsOutSum`, { val: resultObj.data.wattsOutSum, ack: true });
}
}
} catch (err) {
console.error(err);
}
});
Der inoffizielle Weg mit mehr Möglickeiten
Für die inoffizielle Variante brauchst Du die MQTT-Zugangsdaten, die Seriennummer und Deine User-ID. Damit Du nicht alles selbst implementieren musst, findest Du hier mein Script aus dem Video. Danach hast Du alle möglichen Datenpunkte. Einige davon sind schreibbar und ändern die Konfiguration der Power Station (wie z.B. das Ladelimit).
const mqttInstance = 'mqtt.1';
const serialNumber = 'R6XXXXXXXXXXXXXXX';
const userId = '123123123123123123';
const prefix = '0_userdata.0.energy.powerstation';
const valueCache = {};
const changeableStates = [
'mppt.cfgChgWatts',
'mppt.chgPauseFlag',
'bms_emsStatus.maxChargeSoc',
'bms_emsStatus.minDsgSoc'
];
on({ id: `${mqttInstance}.app.device.property.${serialNumber}`, change: 'ne', ack: true }, async (obj) => {
try {
const msgObj = JSON.parse(obj.state.val);
for (let [key, value] of Object.entries(msgObj.params)) {
const vType = typeof value;
const isObject = vType === 'object';
if (!await existsObjectAsync(`${prefix}.${key}`)) {
await createStateAsync(`${prefix}.${key}`, isObject ? JSON.stringify(value) : value, {
name: key,
role: isObject ? 'json' : 'state',
type: isObject ? 'string' : vType,
read: true,
write: changeableStates.includes(key)
});
}
if (isObject) {
value = JSON.stringify(value);
}
if (!Object.prototype.hasOwnProperty.call(valueCache, key) || valueCache[key] !== value) {
await setStateAsync(`${prefix}.${key}`, { val: value, ack: true });
valueCache[key] = value;
}
}
} catch (err) {
console.error(JSON.stringify(err));
}
});
on({ id: [`${prefix}.mppt.cfgChgWatts`, `${prefix}.mppt.chgPauseFlag`], change: 'ne', ack: false }, async (obj) => {
const chargeWatts = (obj.id === `${prefix}.mppt.cfgChgWatts` ? obj.state.val : getState(`${prefix}.mppt.cfgChgWatts`).val);
const chargePaused = (obj.id === `${prefix}.mppt.chgPauseFlag` ? obj.state.val : getState(`${prefix}.mppt.chgPauseFlag`).val);
console.log(`Changing charge speed of power station to ${chargeWatts}W (paused: ${chargePaused})`);
if (chargeWatts >= 100 && chargeWatts <= 940) {
setChargeSpeed(chargeWatts, chargePaused);
await setStateAsync(obj.id, { val: obj.state.val, ack: true });
}
});
on({ id: `${prefix}.bms_emsStatus.maxChargeSoc`, change: 'ne', ack: false }, async (obj) => {
const newVal = obj.state.val;
if (newVal >= 50 && newVal <= 100) {
setChargeLimit(newVal);
await setStateAsync(obj.id, { val: obj.state.val, ack: true });
}
});
on({ id: `${prefix}.bms_emsStatus.minDsgSoc`, change: 'ne', ack: false }, async (obj) => {
const newVal = obj.state.val;
if (newVal >= 0 && newVal <= 30) {
setDischargeLimit(newVal);
await setStateAsync(obj.id, { val: obj.state.val, ack: true });
}
});
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min) + min);
}
function sendCommandToDevice(msgObj) {
const msgBaseObj = {
params: {},
from: 'iOS',
lang: 'en-us',
id: String(getRandomInt(100000000, 900000000)),
moduleSn: serialNumber,
version: '1.0'
};
sendTo(mqttInstance, 'sendMessage2Client', { topic: `/app/${userId}/${serialNumber}/thing/property/set`, message: JSON.stringify({...msgBaseObj, ...msgObj}) });
}
/** Enable or disable AC ouput (1 or 0) */
function setAcOutput(val) {
sendCommandToDevice({
params: {
enabled: val ? 1 : 0,
out_freq: 255,
out_voltage: 4294967295,
xboost: 255
},
moduleType: 5,
operateType: 'acOutCfg'
});
}
/** Enable or disable DC ouput (1 or 0) */
function setDcOutput(val) {
sendCommandToDevice({
params: {
enabled: val ? 1 : 0,
},
moduleType: 5,
operateType: 'mpptCar'
});
}
/** Set charge speed (100 to 940) */
function setChargeSpeed(val, pause) {
sendCommandToDevice({
params: {
chgWatts: val,
chgPauseFlag: pause ? 1 : 0
},
moduleType: 5,
operateType: 'acChgCfg'
});
}
/** Set charge limit (50 to 100) */
function setChargeLimit(val) {
sendCommandToDevice({
params: {
maxChgSoc: val
},
moduleType: 2,
operateType: 'upsConfig'
});
}
/** Set charge limit (0 to 30) */
function setDischargeLimit(val) {
sendCommandToDevice({
params: {
minDsgSoc: val
},
moduleType: 2,
operateType: 'dsgCfg'
});
}
Überschuss-Laden
Erstmal brauchen wir folgende Werte:
- Aktuelle Einspeisung ins Netz (in Watt)
- Aktueller Bezug aus dem Netz (in Watt)
Damit kann man nun die Ladung der EcoFlow steuern.
// v0.2
const prefix = '0_userdata.0.energy.powerstation';
const objIdMeterPower = 'alias.0.energy.electricity.meter.power'; // Power (negative is import)
const objIdPowerSwitch = '';
const objIdMaxChargeSoc = `${prefix}.bms_emsStatus.maxChargeSoc`;
const objIdCurrentSoc = `${prefix}.bms_bmsStatus.soc`;
const objIdChargeWatts = `${prefix}.mppt.cfgChgWatts`;
const objIdChargePause = `${prefix}.mppt.chgPauseFlag`;
// --------------------------------------------------------------------------------------
let chargingSpeed = 0; // Watt
let isCharging = false;
let allowedToCharge = false;
on({ id: objIdMeterPower, change: 'any' }, async (obj) => {
const power = obj.state.val;
const chargeWith = chargingSpeed + power - 50; // 50W Puffer
if (allowedToCharge && chargeWith >= 100) {
console.log(`Setting charging speed to ${chargeWith}W (power: ${power}W)`);
setCharging(chargeWith);
} else {
pauseCharging();
}
});
// --------------------------------------------------------------------------------------
// Bei Script-Start prüfen, ob direkt geladen werden darf (Soc < Max SoC)
async function initScript() {
pauseCharging(); // Pause bei Script-Start
const socState = await getStateAsync(objIdCurrentSoc);
if (socState) {
const soc = socState.val;
console.log(`Starting with SoC: ${soc}%`);
const maxChargeSocState = await getStateAsync(objIdMaxChargeSoc);
if (maxChargeSocState && maxChargeSocState.val > soc) {
allowedToCharge = true;
console.log(`Max. SoC ${maxChargeSocState.val}% > ${soc}% - allowedToCharge: ${allowedToCharge}`);
}
}
}
initScript();
// Aktuellen Ladestand überwachen
on({ id: objIdCurrentSoc, change: 'ne' }, async (obj) => {
const soc = obj.state.val;
console.log(`SoC: ${soc}%`);
// Zwischenstecker einschalten, wenn Akkustand kleiner gleich 50%
if (soc <= 50) {
allowedToCharge = true;
console.log(`SoC too low: ${soc}% - allowedToCharge: ${allowedToCharge} (switching on AC)`);
if (objIdPowerSwitch) {
await setStateAsync(objIdPowerSwitch, { val: true, ack: false }); // Switch AC on
pauseCharging();
}
}
// Akku vollständig geladen, laden stoppen
const maxChargeSocState = await getStateAsync(objIdMaxChargeSoc);
if (maxChargeSocState && maxChargeSocState.val == obj.state.val) {
allowedToCharge = false;
console.log(`SoC ${soc}% reached max. SoC ${maxChargeSocState.val}% - allowedToCharge: ${allowedToCharge}`);
}
});
// Nachts den Zwischenstecker ausschalten, um den Akku zu nutzen
schedule({ astro: 'night', shift: 60 }, async () => {
const socState = await getStateAsync(objIdCurrentSoc);
if (socState) {
const soc = socState.val;
// Zur Sicherheit nur abschalten, wenn aktueller Ladestand größer 55%
if (soc > 55) {
console.log(`SoC high enough to use battery: ${soc}% (switching off AC)`);
if (objIdPowerSwitch) {
await setStateAsync(objIdPowerSwitch, { val: false, ack: false }); // Switch AC off to use battery
}
}
}
});
// --------------------------------------------------------------------------------------
async function pauseCharging() {
if (isCharging) {
console.log('Charging paused');
}
chargingSpeed = 0;
isCharging = false;
await setStateAsync(objIdChargePause, { val: 1, ack: false });
}
async function setCharging(newSpeed) {
let newSpeedRounded = Math.floor(newSpeed / 100) * 100; // Auf nächste volle 100 runden
if (newSpeedRounded > 940) {
newSpeedRounded = 940;
} else if (newSpeedRounded < 100) {
newSpeedRounded = 100;
}
if (chargingSpeed !== newSpeedRounded || !isCharging) {
chargingSpeed = newSpeedRounded;
isCharging = true;
console.log(`Charging with ${chargingSpeed}W`);
await setStateAsync(objIdChargeWatts, { val: chargingSpeed, ack: false });
await setStateAsync(objIdChargePause, { val: 0, ack: false });
}
}
Weitere Quellen
- https://github.com/Philmod/node-pid-controller
- https://de.wikipedia.org/wiki/Regler
- https://forum.arduino.cc/t/wie-funktioniert-ein-pid-regler-eine-nicht-wissenschaftliche-erklarung/
Fazit
Ich würde mir wünschen, dass der Hersteller mit einem Firmware-Upgrade nachbessert, und eine lokale Schnittstelle anbietet. Gerne per HTTP oder MQTT (zu einem eigenen Broker). Damit würde sich das Gerät super in eine Smart Home Lösung integrieren lassen und man könnte die Akkukapazität auch sinnvoller nutzen! Aktuell kommt man zwar zum Ziel - aber das ist alles andere als offiziell.
Zumindest war es ein weiter Weg, welchen sicher nicht viele Nutzer gehen werden oder gehen können.
Generell finde ich die PowerStation sehr gut! Die Verarbeitung ist top - nur die Software und die verfügbaren Schnittstellen sind ausbaufähig.
Update Oktober 2024
Mittlerweile gibt es eine offizielle IoT-Platform für Entwickler. Darüber sollte man hoffentlich mehr Infos zu den Geräten bekommen. Dort meldet man sich mit seinem normalen Account an, welcher auch für die App genutzt wird. Ein Gerät sollte natürlich mit dem Konto verknüpft sein. Dann kann man sich als Entwickler bewerben.
Am Ende habe ich dort genau den gleichen API-Key gefunden, welcher mir damals schon vom Support per Mail angelegt wurde. Dieser Prozess geht nun also über diese Plattform und man kann sich selbst registieren. Die Freischaltung des Kontos dauert ein paar Tage!
Positiv
- Gute Verarbeitung
- LFP-Zellchemie
- Hohe Lebensdauer / viele Ladezyklen
- Viele Schnittstellen (USB-C, 230V, …)
Negativ
- Keine lokale Schnittstelle / Cloud-Produkt
- API nicht dokumentiert
Transparenz-Hinweis (Level 2)
Für diesen Beitrag wurden mir Produkte kostenfrei zur Verfügung gestellt! Es wurden keinerlei Bedingungen, Richtlinien oder Vorgaben bezüglich der Inhalte, welche ich in meiner Bewertung äußern darf, auferlegt.
Darüber hinaus habe ich keine zusätzliche Vergütung erhalten.