EcoFlow River 2 (Pro) als USV und Batteriespeicher

Mit ** gekennzeichnete Links auf dieser Seite sind Affiliatelinks.

EcoFlow River 2 (Pro) als USV und Batteriespeicher
EcoFlow River 2 (Pro) als USV und Batteriespeicher
  • Matthias Kleine
  • 13.02.2023
  • Hardware
  • 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

Hausbau-Kurs

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.

Wichtig: Das Umschalten auf den Akku dauert laut EcoFlow-Webseite ca. 30 Millisekunden. Das könnte für einige Systeme schon zu lang sein. Zum Vergleich: Meine CyberPower USV ist mit 4 Millisekunden angegeben.

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, für den Fall eine “richtige” USV zu verwenden. Aber eventuell möchtest Du ja trotzdem die Powerstation mit PV-Überschuss laden?

EF ECOFLOW Tragbare Powerstation RIVER 2 Pro, 768 Wh Solargenerator mit LiFeP04, Schnellladung in 70 Minuten, 3x 800 W AC-Steckdosen, Balkonkraft für Notstrom/Camping/Wohnmobile/zu Hause **

EF ECOFLOW Tragbare Powerstation RIVER 2 MAX, 512 Wh Solargenerator mit LiFeP04, Schnellladung in 1 Stunde, bis zu 1000 W Leistung, Balkonkraft für Notstrom/Camping/Wohnmobile/zu Hause **

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 und secretKey-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

Wichtig: Das hier gezeigte Vorgehen ist keine offizielle Schnittstelle zu EcoFlow und auch nicht dokumentiert. Befehle oder Pfade können sich in Zukunft ändern! Bitte mit Vorsicht genießen und im Zweifel nicht benutzen.
Wichtig: Leider scheint EcoFlow eher daran zu arbeiten, die hier gezeigten Wege aktiv zu verhindern, anstatt offener zu werden und Schnittstellen anzubieten. Jedenfalls wird meine Verbindung seit dem 15.03.2023 abgelehnt, wenn ich nicht die richtige Client-ID angegebe. Bitte die Hinweise dazu beachten.

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: Inverter (Wechselrichter)
  • 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 bis 940)
  • mppt.chgPauseFlag - Laden pausiert
  • bms_emsStatus.maxChargeSoc - Maximaler Ladestand (50 bis 100)
  • bms_emsStatus.minDsgSoc - Minimaler Ladestand (0 bis 30)
  • bms_bmsStatus.cycles - Zyklen des Akkus
  • bms_bmsStatus.temp - Temperatur des Akkus (?)
  • bms_emsStatus.fanLevel - Lüfter (abhängig von Temperatur)
  • pd.soc oder bms_bmsStatus.soc - Aktueller Ladestand in Prozent (0 bis 100)
  • pd.remainTime - Restliche Zeit in Minuten bis oberes Ladelimit erreicht ist
  • pd.soc - Aktueller Ladestand in Prozent (0 bis 100)
  • pd.wattsInSum - Aktuelle Eingangsleistung
  • pd.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
Wichtig: Diese Payloads habe ich mit der iOS App in Version v4.2.0.12 und der River 2 Pro mit Firmware-Version 1.0.0.1 herausgefunden. Da im Forum komplett andere Payloads geteilt wurden, scheinen sich diese von Gerät zu Gerät zu unterscheiden. Bitte mit Vorsicht genießen!

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

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.

Positiv

  • Gute Verarbeitung
  • LFP-Zellchemie
  • Hohe Lebensdauer / viele Ladezyklen
  • Viele Schnittstellen (USB-C, 230V, ...)

Negativ

  • Keine lokale Schnittstelle / Cloud-Produkt
  • API nicht dokumentiert
Du willst mehr?

Smart-Home-Trainings von A-Z

Steig noch tiefer in die Themen ein und meistere Deine Projekte! Über 13.000 Teilnehmer konnten sich schon von der Qualität der Online-Kurse überzeugen.

ioBroker-Master-Kurs

ioBroker-Master-Kurs

Mehr Infos
Hausbau-Kurs

Hausbau mit KNX

Mehr Infos
Lox-Kurs

Lox-Kurs

Mehr Infos
Node-RED-Master-Kurs

Node-RED-Master-Kurs

Mehr Infos