Offizielle Ecoflow API mittels FHEM und MQTT nutzen

Seit ein paar Wochen habe ich die Ecoflow Delta2 Powerstation sowie den Mikrowechselrichter Powerstream bei mir im Einsatz und natürlich sollten die Geräte möglichst schnell in meine Haussteuerung integriert werden. In dem nachfolgenden Artikel habe ich daher einmal den Code dargestellt, den man benötigt, um an die offizielle API von Ecoflow zu gelangen. Weiterhin zeige ich euch, wie ich die beiden Geräte mittels MQTT in meine Haussteuerung integriert habe.

Im nächsten Artikel werde ich dann über mein erstes Test-Setup berichten, wie ich die Powerstation über das SMA Portal bzw. den SMA Homemanager mit Überschuß aus meiner PV-Dachanlage auflade und mittels Powerstream bei Bedarf den gespeicherten Strom ins Hausnetz einspeise.

Meine Ecoflow Delta 2

Ich habe mich entschlossen, den gesamten Code für den Zugriff auf die Ecoflow API im Blogtext darzustellen. Daher besteht der Artikel in erster Linie aus sehr viel Softwarecode. Ein Grund dafür ist auch die Tatsache, dass ich bisher nur inoffizielle Zugriffsroutinen gefunden habe, die die notwendigen Zugriffsdaten für die offizielle API von Ecoflow ermitteln. Ich habe mich entschieden, den Beispielcode auf der Developerseite zu nutzen. Allerdings hat man damit wohl nur Zugriff über die Cloud von Ecoflow und kann nicht lokal auf die Geräte zugreifen.

Developer Zugriff beantragen und Access Key erstellen

Um Zugriff auf die API von Ecoflow zu bekommen, geht man zunächst zur Developer Seite von Ecoflow und loggt sich dort mit seinem Account ein bzw. nutzt den Button „Become a Developer“. Nachdem man sich für das Developer Programm registriert hat, bekommt man nach ein paar Tagen den Zugriff auf die IOT Plattform von Ecoflow.

Ecoflow Developer Webseite

MIt dem Zugriff auf die Developer Plattform hat man dann die Möglichkeit, sich einen Access Key sowie einen Secret Key zu erstellen. Beides benötigt man, um beispielsweise die Zugangsdaten für die MQTT Schnittstelle zu bekommen.

Ecoflow API Access Key erzeugen

Zugriffsdaten für die Ecoflow MQTT Schnittstelle ermitteln

Hat man nun den Acces Key sowie den Security Key kann man die Daten ermitteln, die man für den Zugriff auf die MQTT Schnittstelle benötigt. Die gesamte Dokumentation dazu sowie die allgemeine Beschreibung, wie man an die Daten kommt bzw. wie man die jeweiligen HTTP sowie MQTT Schnittstellen benutzt, sind auf der Developer Seite zu finden.

Dort findet man auch Code Beispiele, die allerdings in Java erstellt wurden. Diese Beispiele habe ich nun genutzt, um damit die nachfolgenden Python Routinen zu erstellen. Da ich nicht mehr so tief drin stecke in der Entwicklung von Programmcode habe ich ein paar Online Tools genutzt, die Java Code in Python umwandeln. Das hat zwar nicht zu 100% funktioniert aber mit ein paar Anpassungen, auf Grund entsprechender Fehlermeldungen und der Nachinstallation einzelner Packages auf meinem Raspberry Pi habe ich den nachfolgenden Code erstellen können.

Der Code besteht aus der Hauptroutine „ecoflow_mqtt.py“ sowie einzelnen Hilfsdateien, die den Java Beispielen von Ecoflow entsprechen. In der Hauptroutine habe ich allerdings nur den Code genutzt, der mir die MQTT Daten liefert. Den Rest habe ich weggelassen bzw. nicht getestet und daher hier auch nicht veröffentlicht.

Die Dateien habe ich in ein Verzeichnis auf meinem Rasperry Pi abgelegt und dann mit dem Befehl „python3 ecoflow_mqtt.py“ auf Betriebssystemebene gestartet. Neben ein paar Zwischenmeldungen, die ich eingebaut habe, bekommt man dann die Authentifizierungsdaten sowie die Adresse für den Zugriff auf die MQTT Schnittstelle. Die Werte sollte man sich dann notieren. Die Routinen selbst benötigt man danach nicht mehr.

getMQTTCertification|{"code":"0","message":"Success","data":{"certificateAccount":"open-e051e2XXXXXXX",
 "certificatePassword":"c726cfXXXXXXXXX",
 "url":"mqtt-e.ecoflow.com","port":"8883","protocol":"mqtts"}

Hauptroutine (Datei ecoflow_mqtt.py)

Die Hauptroutine nutzt die Funktion get_mqtt_certification, um damit den HTTP Aufruf zur Ermittlung der notwendigen Daten zu erstellen sowei auszuführen.

import json
import requests
import httputil

ACCESS_KEY = "DEIN ACCESKEY"
SECRET_KEY = "DEIN SECRETKEY"
HOST = "https://api.ecoflow.com"
GET_MQTT_CERTIFICATION_URL = f"{HOST}/iot-open/sign/certification"
DEVICE_LIST_URL = f"{HOST}/iot-open/sign/device/list"
SET_QUOTA_URL = f"{HOST}/iot-open/sign/device/quota"
GET_QUOTA_URL = f"{HOST}/iot-open/sign/device/quota"
GET_ALL_QUOTA_URL = f"{HOST}/iot-open/sign/device/quota/all"

def get_mqtt_certification():
    response = httputil.execute('GET', GET_MQTT_CERTIFICATION_URL, None, ACCESS_KEY, SECRET_KEY)
    print(f"response: getMQTTCertification|{response}")

def main():
    get_mqtt_certification()

if __name__ == "__main__":
    main()

Datei httputil.py

Die Datei httputil.py nutzt die übrigen Dateien bzw. Funktionen, um die Infos zusammen zu stellen, die man für den HTTP-Aufruf benötigt.

import requests
import json
import random
import string
import time
from urllib.parse import urlencode
import urllib
import hashlib
import hmac
import MapUtil
import encrypt

ACCESS_KEY = "accessKey"
NONCE = "nonce"
TIMESTAMP = "timestamp"
SIGN = "sign"

def execute(http_method, url, req, access_key, secret_key):
    try:
        (url,headers) = get_http_uri_request(http_method, url, req, access_key, secret_key)
        print(url)
        print(headers)
        
        response = requests.get(url, headers=headers)
        if response.status_code != 200:
            print(f"response status is failed|url={url},statusCode={response.status_code}")
            raise RuntimeException("response status is failed")
        return response.text
    except requests.exceptions.RequestException as e:
        raise RuntimeException(e)

def build_headers(req, access_key, secret_key):
    headers = {}
    sorted_req = {}
    sorted_req[NONCE] = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
    sorted_req[TIMESTAMP] = str(int(time.time()))
    sorted_req[ACCESS_KEY] = access_key
    sorted_req[SIGN] = sign(sorted_req, secret_key)
    headers["Content-Type"] = "application/json"
    headers["Authorization"] = urlencode(sorted_req)
    return headers

def get_http_uri_request(http_method, url, req, access_key, secret_key):
    nonce = str(random.randint(10000, 1000000))
    timestamp = str(int(time.time() * 1000))
    sort_key_value_map = {}
    
    if req != None:
       sort_key_value_map = MapUtil.getMapFromObject(req);
    query_string = MapUtil.get_key_value_string(sort_key_value_map);
    key_value_string = MapUtil.append_access_key(query_string, access_key, nonce, timestamp);        
    print(f"KeyValue:{key_value_string}")    
    
    sign = encrypt.encrypt_hmac_sha256(key_value_string, secret_key)
    print(f"Sign:{sign}")

    query_string = urllib.parse.quote(query_string, safe='')

    if http_method == 'GET':
        print(f"{url}?{query_string}")
        
        headers = {
            'accessKey': access_key,
            'nonce': nonce,
            'timestamp': timestamp,
            'sign': sign
        }        
        return (f"{url}?{query_string}", headers)
    elif http_method == 'PUT':
        headers = {
            'Content-Type': 'application/json;charset=UTF-8',
            'accessKey': access_key,
            'nonce': nonce,
            'timestamp': timestamp,
            'sign': sign
        }
        return (url, headers, req)
    elif http_method == 'POST':
        headers = {
            'Content-Type': 'application/json;charset=UTF-8',
            'accessKey': access_key,
            'nonce': nonce,
            'timestamp': timestamp,
            'sign': sign
        }
        return (url, headers, req)
    elif http_method == 'DELETE':
        return f"{url}?{query_string}"
    else:
        raise ValueError(f"HTTP method {http_method} not supported")

def sign(params, secret_key):
    sorted_params = '&'.join([f"{k}={v}" for k, v in sorted(params.items())])
    return hashlib.sha256((sorted_params + secret_key).encode()).hexdigest()

Die Hilfsdateien MapUtil.py und encrypt.py

Die beiden Dateien MapUtil und ecnrypt enthalten Funktionen, mit denen der String zusammen gestellt und verschlüsselt wird, damit der HTTP-Aufruf zur Ermittlung der gewünschten MQTT-Daten funktioniert.

Datei MapUtil.py

import json
from collections import OrderedDict

MERGE_CHAR = "&"
EQUAL_CHAR = "="
POINT_CHAR = "."
ACCESS_KEY = "accessKey"
NONCE = "nonce"
TIMESTAMP = "timestamp"
SIGN = "sign"

def append_access_key(key_value_string, access_key, nonce, timestamp):
    builder = []
    if key_value_string:
        builder.append(key_value_string)
        builder.append(MERGE_CHAR)
    builder.append(ACCESS_KEY)
    builder.append(EQUAL_CHAR)
    builder.append(access_key)
    builder.append(MERGE_CHAR)
    builder.append(NONCE)
    builder.append(EQUAL_CHAR)
    builder.append(nonce)
    builder.append(MERGE_CHAR)
    builder.append(TIMESTAMP)
    builder.append(EQUAL_CHAR)
    builder.append(timestamp)
    return "".join(builder)

def get_key_value_string(sort_key_value_map):
    if not sort_key_value_map:
        return ""
    builder = []
    for key, value in sort_key_value_map.items():
        builder.append(key)
        builder.append(EQUAL_CHAR)
        builder.append(str(value))
        builder.append(MERGE_CHAR)
    if builder:
        return builder[:-1]
    return ""

def get_map_from_object(json_object):
    if not json_object or not json_object.keys():
        raise RuntimeError("parameter invalid")
    sort_key_value_map = OrderedDict()
    for key, value in json_object.items():
        sort_key_value_map.update(get_by_object(key, value))
    return sort_key_value_map

def get_by_object(key, value):
    result = {}
    if isinstance(value, dict):
        for sub_key, sub_value in value.items():
            result[f"{key}{POINT_CHAR}{sub_key}"] = sub_value
    else:
        result[key] = value
    return result

Datei encrypt.py

import hashlib
import hmac

def encrypt_hmac_sha256(message, secret):
    try:
        secret_key = secret.encode()
        hmac_sha256 = hmac.new(secret_key, message.encode(), hashlib.sha256)
        return hmac_sha256.hexdigest().lower()
    except Exception as e:
        raise RuntimeError(str(e))

MQTT Devices in FHEM erstellen

Nachdem man nun hoffentlich die notwendigen Daten für die MQTT Schnittstelle ermittelt hat, kann man damit die notwendigen Devices innerhalb FHEM erstellen. Für mich war dies der erste Versuch mit MQTT und wahrscheinlich kann man einige Dinge noch vereinfachen. So habe ich beispielsweise sowohl für die Powerstation als auch den Powerstream zwei Clients angelegt. Durch das Attribut „autocreate complex“ werden dann wohl auch automatisch die entsprechenden Devices angelegt, in denen dann für die Subscription die jeweiligen Readings angelegt werden.

Für den Zugriff auf den MQTT Server von Ecoflow definiert man in FHEM wie folgt einen Client. Im Code müsst ihr euren ermittelten Certificate Account sowie die Seriennummer des Gerätes, für die ihr die Daten erhalten wollt, an den jeweiligen Stellen einsetzen. Das Passwort wird dann mittels FHEM set-Befehl gesetzt.

Mit den von mir angegebenen Subriptions „status“, „quota“ und „set“ bekommt man quasi alle Infos des jeweiligen Geräts. Mit „quota“ werden alle verfügbaren Parameter ermittelt, so dass eine recht lange Readingsliste entsteht. Damit FHEM nicht zu sehr belastet wird, solltet ihr auf jeden Fall ein „event-on-change-reading: .*“ in dem generierten Device definieren. Die MQTT Schnittstelle ist nämlich sehr gesprächig und liefert quasi jede Sekunde neue Informationen.

defmod myMQTTClient MQTT2_CLIENT mqtt-e.ecoflow.com:8883
attr myMQTTClient SSL TLS
attr myMQTTClient autocreate complex
attr myMQTTClient room MQTT2_DEVICE,System
attr myMQTTClient subscriptions /open/open-e05CERTIFICATEACCOUNT/SERIENNUMMER/status /open/open-e05XXXXX/SERIENNUMMER/quota /open/open-e05CERTIFICATEACCOUNT/SERIENNUMMER/set
attr myMQTTClient username open-e05CERTIFICATEACCOUNT

MQTT Befehle an die Ecoflow Geräte senden

Mit zwei kleinen Beispielen zeige ich euch noch, wie ihr entsprechende Paramater bei den Geräten ändert.

Mit folgendem Befehl wird der Powerstream angesprochen, den ich als zweiten Client, wie gerade beschrieben erstellt habe. Der entsprechende Befehl wird immer beim Client ausgeführt und sollte dann im zugehörigen Device in der Veränderung der Parameter erkennbar sein.

Möchte man die Einspeiseleistung auf 200Wh setzen, dann geht das mit dem FHEM set-Befehl wie folgt:

set myPowerstream publish /open/open-e05CERTIFICATEACCOUNT/SERIENNUMMER/set 
   {"id": 123456789,"version": "1.0","cmdCode": "WN511_SET_PERMANENT_WATTS_PACK","params": 
       {"permanentWatts": 200}}

Die Ladeleistung über den AC-Eingang kann man bei der Delta2 Powerstream mit folgendem Befehl auf 500Wh setzen:

set myMQTTClient publish /open/open-e05CERTIFICATEACCOUNT/SERIENNUMMER/set 
	{ "id":123456789, "version":"1.0", "moduleType":5, "operateType":"acChgCfg", "params":
	{ "chgWatts":500, "chgPauseFlag":0 }

Die jeweiligen Befehle für die Geräte sind alle in der Doku auf der Developer Seite von Ecoflow zu finden und sollten mit diesen Beispielen recht einfach umsetzbar sein.

Im nächsten Beitrag zeige ich euch dann, wie ich mit einem DOIF die entsprechenden Parameter so verändere, dass der Akku immer mit der vorhandenen Sonnenergie geladen und bei Netzbezug entladen bzw. die gespeicherte Energie in der benötigten Höhe ins Haus eingespeist wird.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Why ask?

x

Check Also

Unser Photovoltaikertrag im März 2024

Ich bin zwar ein wenig spät dran aber der monatliche PV-Bericht darf natürlich nicht fehlen. ...