EcoFlow OpenHAB Integration (Binding)

Hi,
I just bought an EcoFlow Delta Max Powerstation and wonder if anyone managed to integrate an EcoFlow device into OpenHAB.

For Home Assistant they seemed to have success.

It would be great to use these devices as intelligent battery storage in combination with an photovoltaic system.

Regards

Tim

Just in reply to your request i managed to get a basic integration for my delta 2 here.
I used the mqtt interface, so its not that hard to implement.
Follow the basic instructions to get your credentials with the following guide Accessing-EF
You ll need to be registered in the ecoflow app and these credentials you need for the next steps. If your managed to run the linked script youll get allmost all the data you need.

ill post the code of the broker and the channels here so you should be able to adapt or enhance. Just create an mqtt target yourself, and tune it to your needs. Its very basic atm but works for me.

if they change the app or something you ll need to investigate the calls again, even the delta max might have slightly different json calls.

The mqtt broker (e00b275b89 PW and User must be changed)

UID: mqtt:broker:e00b275b89
label: MQTT Broker ecoflow
thingTypeUID: mqtt:broker
configuration:
  lwtQos: 0
  publickeypin: false
  keepAlive: 60
  clientID: openhab
  hostnameValidated: false
  secure: true
  birthRetain: false
  shutdownRetain: false
  certificatepin: false
  password: YOURPWFROMTHEAPP
  protocol: TCP
  qos: 0
  reconnectTime: 60000
  port: 8883
  mqttVersion: V3
  host: mqtt.ecoflow.com
  lwtRetain: true
  username: YOURUSERNAMEFROMTHEAPP
  enableDiscovery: false

The Channels (again change IDs SRN and clientID)

UID: mqtt:topic:e00b275b89:2e180fbcbe
label: MQTT Thing ecoflow delta 2
thingTypeUID: mqtt:topic
configuration: {}
bridgeUID: mqtt:broker:e00b275b89
channels:
  - id: soc
    channelTypeUID: mqtt:string
    label: soc
    description: null
    configuration:
      stateTopic: /app/device/property/YOURSRN
      transformationPattern: REGEX:(.*pd.soc.*)∩JSONPATH:$.params.['pd.soc']
  - id: carwatts
    channelTypeUID: mqtt:string
    label: carwatts
    description: null
    configuration:
      stateTopic: /app/device/property/YOURSRN
      transformationPattern: REGEX:(.*pd.carWatts.*)∩JSONPATH:$.params.['pd.carWatts']
  - id: usb1Watts
    channelTypeUID: mqtt:string
    label: usb1Watts
    description: null
    configuration:
      stateTopic: /app/device/property/YOURSRN
      transformationPattern: REGEX:(.*pd.usb1Watts.*)∩JSONPATH:$.params.['pd.usb1Watts']
  - id: outputWatts
    channelTypeUID: mqtt:string
    label: outputWatts
    description: null
    configuration:
      stateTopic: /app/device/property/YOURSRN
      transformationPattern: REGEX:(.*pd.outWatts.*)∩JSONPATH:$.params.['pd.outWatts']
  - id: inputWatts
    channelTypeUID: mqtt:string
    label: inputWatts
    description: null
    configuration:
      stateTopic: /app/device/property/YOURSRN
      transformationPattern: REGEX:(.*pd.inputWatts.*)∩JSONPATH:$.params.['pd.inputWatts']
  - id: achargespeedswitch
    channelTypeUID: mqtt:switch
    label: achargespeedswitch
    description: null
    configuration:
      commandTopic: /app/YOURCLIENTID/YOURSRN/thing/property/set
      off: '{"from":"Android","id":"490361407","moduleType":5,"operateType":"acChgCfg","params":{"chgWatts":200,"chgPauseFlag":255},"version":"1.0"}'
      on: '{"from":"Android","id":"490361407","moduleType":5,"operateType":"acChgCfg","params":{"chgWatts":500,"chgPauseFlag":255},"version":"1.0"}'
  - id: dcswitch
    channelTypeUID: mqtt:switch
    label: dcswitch
    description: null
    configuration:
      commandTopic: /app/YOURCLIENTID/YOURSRN/thing/property/set
      off: '{"params":{"enabled":0},"from":"Android","lang":"en-us","id":"824661624","moduleSn":
        "YOURSRN","moduleType":5,"operateType":"mpptCar","version":"1.0"}'
      on: '{"params":{"enabled":1},"from":"Android","lang":"en-us","id":"824661624","moduleSn":
        "YOURSRN","moduleType":5,"operateType":"mpptCar","version":"1.0"}'
  - id: acswitch
    channelTypeUID: mqtt:switch
    label: acswitch
    description: null
    configuration:
      commandTopic: /app/YOURCLIENTID/YOURSRN/thing/property/set
      off: '{"from":"Android","id":"160291434","moduleType":5,"operateType":"acOutCfg","params":{"out_voltage":-1,"out_freq":255,"xboost":255,"enabled":0},"version":"1.0"}'
      on: '{"from":"Android","id":"160291434","moduleType":5,"operateType":"acOutCfg","params":{"out_voltage":-1,"out_freq":255,"xboost":255,"enabled":1},"version":"1.0"}'
  - id: pvchargeswitch
    channelTypeUID: mqtt:switch
    label: pvchargeswitch
    description: null
    configuration:
      commandTopic: /app/YOURCLIENTID/YOURSRN/thing/property/set
      off: '{"from":"Android","id":"458351940","moduleType":1,"operateType":"pvChangePrio","params":{"pvChangeSet":0},"version":"1.0"}'
      on: '{"from":"Android","id":"458351940","moduleType":1,"operateType":"pvChangePrio","params":{"pvChangeSet":1},"version":"1.0"}'
  - id: typec1Watts
    channelTypeUID: mqtt:string
    label: typec1Watts
    description: null
    configuration:
      stateTopic: /app/device/property/YOURSRN
      transformationPattern: REGEX:(.*pd.typec1Watts.*)∩JSONPATH:$.params.['pd.typec1Watts']
  - id: usbswitch
    channelTypeUID: mqtt:switch
    label: usbswitch
    description: null
    configuration:
      commandTopic: /app/YOURCLIENTID/YOURSRN/thing/property/set
      off: '{"params":{"enabled":0},"from":"Android","lang":"en-us","id":"824661624","moduleSn":
        "YOURSRN","moduleType":1,"operateType":"dcOutCfg","version":"1.0"}'
      on: '{"params":{"enabled":1},"from":"Android","lang":"en-us","id":"824661624","moduleSn":
        "YOURSRN","moduleType":1,"operateType":"dcOutCfg","version":"1.0"}'
  - id: remainTime
    channelTypeUID: mqtt:string
    label: remainTime
    description: null
    configuration:
      stateTopic: /app/device/property/YOURSRN
      transformationPattern: REGEX:(.*pd.remainTime.*)∩JSONPATH:$.params.['pd.remainTime']

ecoflow closed mqtt access, ill keep you updated if i can get it working again

MQTT acces still works. However you have to make sure, that you use the same client id that the smartphone application uses.

Where do you get the client id?

Little Update, with the release of an official developer api, the access has been way more easier.

First you need developer access from here EcoFlow Developer Platform

Then you will be able to get an AccessKey and a SecretKey for accessing the http api - which is documented under the link.

Refering to the original script from svenerbe for HomeAssitant EcoFlow Device API Integration for PowerStream dynamic power adjustment i did some little dirty coding and changed the python script for querring all ecoflow devices like smartplugs, batteries and powerstream (the ones i´ve tested), you can even go for setting the power value for powerstreams from there.

#!/usr/local/bin/python3.11

import os
import sys
import json
import requests
import hmac
import hashlib
import random
import time
import binascii
from urllib.parse import urlencode

def hmac_sha256(data, key):
    hashed = hmac.new(key.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).digest()
    return binascii.hexlify(hashed).decode('utf-8')

def get_flattened_map(json_obj, prefix=""):
    def flatten(obj, pre=""):
        result = {}
        if isinstance(obj, dict):
            for k, v in obj.items():
                result.update(flatten(v, f"{pre}.{k}" if pre else k))
        elif isinstance(obj, list):
            for i, item in enumerate(obj):
                result.update(flatten(item, f"{pre}[{i}]"))
        else:
            result[pre] = obj
        return result
    return flatten(json_obj, prefix)

def get_qstr(params):
    return '&'.join(f"{key}={params[key]}" for key in sorted(params.keys()))

def get_api(url, key, secret, params=None):
    nonce = str(random.randint(100000, 999999))
    timestamp = str(int(time.time() * 1000))
    headers = {'accessKey': key, 'nonce': nonce, 'timestamp': timestamp}
    sign_str = (get_qstr(get_flattened_map(params)) + '&' if params else '') + get_qstr(headers)
    headers['sign'] = hmac_sha256(sign_str, secret)
    response = requests.get(url, json=params, headers=headers)
    if response.status_code == 200:
        return response.json()
    else:
        sys.exit(f"Error fetching API data: {response.text}")

def check_if_device_is_online(serial_number, devices_data):
    for device in devices_data.get('data', []):
        if device.get('sn') == serial_number:
            return "online" if device.get('online', 0) == 1 else "offline"
    return "device not found"

def get_ef_data(serial_number):
    if serial_number is None:
        sys.exit("Serial number is not provided. Exiting function.")

    api_url_base = "https://api.ecoflow.com/iot-open/sign"
    key = 'APIKEY'
    secret = 'SECRETKEY'

    url_device = f'{api_url_base}/device/list'
    url_quota = f'{api_url_base}/device/quota/all?sn={serial_number}'

    # Cache the device list to avoid repeated API calls
    device_list = get_api(url_device, key, secret)
    online_status = check_if_device_is_online(serial_number, device_list)

    if online_status == "online":
        ef_data = get_api(url_quota, key, secret, {"sn": serial_number})
        print(ef_data)
    else:
        print(f"The device with SN '{serial_number}' is {online_status}.")

if __name__ == '__main__':
    serial_number = sys.argv[1] if len(sys.argv) > 1 else None
    get_ef_data(serial_number)

You can call the script with adding a valid SN from your devices and you´ll get json data as a response.

./get_ef_data.py HW52ZDH4SF******

from this point you can go an with parsing the json and extract the values you need and update the items from the rest api or you can run this script with your exec binding and parse the result with a rule and extract your values.

Hi there,
thank you for posting all these hints. I was able prior to composing an addin to get this all working by DSL-code. Maybe once a day I or somebody else wants to create an ecoflow integration.

I have a special startup logic and startup order. If you don’t need it just delete the code and set startupComplete to true, then everything should work. Add your secret and your key by subscribing to the API and developer acces on the ecoflow website, as described above. Once the reply email has arrived you can start the integration.

Please install the JSON transofrmation addin prior to executing the code!

Change the following code lines to the keys and secrets you have created on the ecoflow developer site:

 
    var String key = 'Mz9oNcXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    var String secret = '2fUsXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

I am using the heartbeat by my bitbucket smart reader MQTT device:

rule "ECOFLOW heartbeat set power receive"
when Item SMRPowerIn received update
then
try
{
    if(start) ....

In this rule just put your own trigger or use the prepared trigger “EF_SetPowerHeartbeat” to trigger the update process.

Every time the smart reader gets an update I update the output of my ecoflow microinverter to give the grid as less power I can possibly give.
THids is done by using an I-Controller with I-factor 100% below zero and adjustable gain above zero difference. This is neccessary to react an on negative grid values faster than on positive.

Calling subfunctions in DSL is a mess, but works pretty well by using semi object oriented HashMap workarounds. I hope that the code is readable.

The code runs smoothly without any errors since May.

I hope you have fun with it!

Regards
E.

Create an ecoflow.items file and add this:

Switch EF_SetPowerHeartbeat
Number EF_SetOutputPower "Ausgangsleistung: [%d W]" <energy> (gRestore)
Number EF_CorrectionIFactor "Regler I-Faktor [%.1f]" (gRestore)
Number EF_MaxOutputValue "Maximale Ausgangsleistung [%d W]" <energy> (gRestore)
Switch EF_Online "Status [MAP(offon.map):%s]" <network> (gRestore)
Number EF_PVToInvWatts "PV Leistung: [%d W]" <sun> (gRestore)
Number EF_BatInputWatts "Batterie Leistung: [%d W]" <battery> (gRestore)
Number EF_BatChargeLevel "Batterie Ladezustand: [%d %%]" <battery> (gRestore)
Number EF_InvOutputWatts "Inverter Leistung: [%d W]" <energy> (gRestore)

Put this in your sitemap file:

Frame label="Balkonkraftwerk"
            { 
                Text item=EF_Online
                Default item=EF_SetOutputPower
                Setpoint item=EF_CorrectionIFactor minValue=0.1 step=0.1 maxValue=3
                Setpoint item=EF_MaxOutputValue minValue=0 step=10 maxValue=800
                Text item=EF_PVToInvWatts
                Text item=EF_InvOutputWatts
                Text item=EF_BatInputWatts
                Text item=EF_BatChargeLevel
            }

Create an ecoflow.rules file:


import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;


import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Random
import java.util.HashMap
import java.util.ArrayList


var HashMap<String, Object> functions=newHashMap
var Integer STARTUP_ID = 41


var Boolean startupComplete=false



val Functions$Function4<HashMap<String, Object>,String,String,String,HashMap<String,String>> getHeaders=

[HashMap<String, Object> map,String key, String secret,String params|

    val Functions$Function3<HashMap<String, Object>,String, String, String> encryptHmacSHA256 =map.get("encryptHmacSHA256") as Functions$Function3<HashMap<String, Object>,String, String, String> 

    val Random rand=new Random()    
    var String nonce = (rand.nextInt(1000000-100000)+100000).toString + ""
   
   
    var String timestamp = now.toInstant.toEpochMilli.toString + ""
     var paramsInt=params
     if(paramsInt==null|| paramsInt=="")
        paramsInt=""
    else
        paramsInt=paramsInt+"&"
    var String signString=paramsInt+"accessKey="+key+"&"+"nonce="+nonce+"&"+"timestamp="+timestamp

  //  logInfo("ECOFLOW","signString: {}",signString)
    signString=encryptHmacSHA256.apply(map,signString,secret)
  //  logInfo("ECOFLOW","signStringCoded: {}",signString)

    var HashMap<String,String> headers = newHashMap("accessKey"->key,"nonce"->nonce,"timestamp"->timestamp,"sign"->signString)

    headers

]

val Functions$Function1<SortedMap<String, Object>,String> createParameters=
[SortedMap<String, Object> params|

    if(params==null)
        return ""
    val String MERGE_CHAR = "&"
    val String EQUAL_CHAR = "="
    val String POINT_CHAR = "."

        val StringBuilder builder = new StringBuilder();
        var keyset=params.keySet()
        keyset.forEach[ key| 
                            
            builder.append(key).append(EQUAL_CHAR).append(params.get(key)).append(MERGE_CHAR)
        ]
        if (builder.toString=="") {
            return ""
        }
       //builder.substring(0, builder.getLength - 1)
       var String res=builder.toString
       res=res.substring(0,res.length-1)
       res

]





val Functions$Function3<HashMap<String, Object>,HashMap<String, Object>, Integer,Boolean> setOutputPower=[
    HashMap<String, Object> map, HashMap<String, Object> device, Integer watts|

    try
    {

        val Functions$Function3<HashMap<String, Object>,String, String, String> encryptHmacSHA256 =map.get("encryptHmacSHA256") as Functions$Function3<HashMap<String, Object>,String, String, String> 
        val Functions$Function4<HashMap<String, Object>,String,String,String,HashMap<String,String>> getHeaders=map.get("getHeaders") as Functions$Function4<HashMap<String, Object>,String,String,String,HashMap<String,String>>
        val Functions$Function1<SortedMap<String, Object>,String> createParameters=map.get("createParameters") as  Functions$Function1<SortedMap<String, Object>,String>

        var String key=map.get("key") as String
        var String secret=map.get("secret") as String
        var funcHeaders=getHeaders.apply(map,key,secret,null)
    
        var String urlBase=map.get("urlbase") as String
        var String urlDevice=map.get("urldevice") as String
        urlDevice=urlBase+urlDevice 
        var String urlDeviceQuota=map.get("urlquota") as String
        urlDeviceQuota=urlBase+urlDeviceQuota

        var String sn=device.get("sn") as String
        var Boolean online=device.get("online") as Boolean
        if(online)
        {
            var Integer wattsInt=watts*10
            var SortedMap<String,Object> paramMap=new TreeMap<String,Object>
            paramMap.put("cmdCode","WN511_SET_PERMANENT_WATTS_PACK")
            paramMap.put("params.permanentWatts",wattsInt.toString)
            paramMap.put("sn",sn)

            var String params=createParameters.apply(paramMap)
            var funcHeaders2=getHeaders.apply(map,key,secret,params)

            funcHeaders2.put("Content-Type","application/json;charset=UTF-8")
            var String setJson=String::format("{\"sn\": \"%s\",\"cmdCode\": \"WN511_SET_PERMANENT_WATTS_PACK\",\"params\": {\"permanentWatts\": %d}}",sn,wattsInt)
            var sendRes=sendHttpPutRequest( urlDeviceQuota,"application/json",setJson,  funcHeaders2, 10000)
            
            var Integer code=Integer::parseInt(transform("JSONPATH","$.code",sendRes))
            if(code!=0)
                logWarn("ECOFLOW","sendRes: {}",sendRes) 

            return true
        }
        else
        {
            logInfo("ECOFLOW","device {} offline or shut down",sn)            
        }
    }
    catch(Exception ex)
    {
          logError("ECOFLOW","Exception in function enumDevices: {}",ex.message)  
    }
    false
]





val Functions$Function2<HashMap<String, Object>,String,Boolean> getDeviceStatus=[HashMap<String, Object> map,String sn|

try
{

    val Functions$Function3<HashMap<String, Object>,String, String, String> encryptHmacSHA256 =map.get("encryptHmacSHA256") as Functions$Function3<HashMap<String, Object>,String, String, String> 
    val Functions$Function4<HashMap<String, Object>,String,String,String,HashMap<String,String>> getHeaders=map.get("getHeaders") as Functions$Function4<HashMap<String, Object>,String,String,String,HashMap<String,String>>
    val Functions$Function1<SortedMap<String, Object>,String> createParameters=map.get("createParameters") as  Functions$Function1<SortedMap<String, Object>,String>

    var String key=map.get("key") as String
    var String secret=map.get("secret") as String
    var SortedMap<String,Object> paramMap=new TreeMap<String,Object>

    paramMap.put("sn",sn)

    var String params=createParameters.apply(paramMap)

    var funcHeaders=getHeaders.apply(map,key,secret,params)
 
    var String urlBase=map.get("urlbase") as String
    var String urlDevice=map.get("urldevice") as String
    urlDevice=urlBase+urlDevice
    var String urlDeviceQuota=map.get("urlquota") as String
    urlDeviceQuota=urlBase+urlDeviceQuota+"/all?sn="+sn

    logInfo("ECOFLOW","urlDeviceQuota: {}",urlDeviceQuota)  
    logInfo("ECOFLOW","funcHeaders: {}",funcHeaders)  

   // logInfo("ECOFLOW","api: {}",urlDevice) 

    var res=sendHttpGetRequest(urlDeviceQuota,funcHeaders,10000)
    var Integer code=Integer::parseInt(transform("JSONPATH","$.code",res))
    if(code!=0)
        logWarn("ECOFLOW","res: {}",res) 
    logInfo("ECOFLOW","res:{}",res)
    res=res.replace("20_1.","20_1_")
    var NumberItem outputWatts=map.get("invOutputWatts") as NumberItem
    outputWatts.sendCommand(Integer::parseInt(transform("JSONPATH","$.data.20_1_invOutputWatts",res))/10.0)
    var NumberItem pvtoInvWatts=map.get("pvToInvWatts") as NumberItem
    pvtoInvWatts.sendCommand(Integer::parseInt(transform("JSONPATH","$.data.20_1_pvToInvWatts",res))/10.0)
    var NumberItem batInputWatts=map.get("batInputWatts") as NumberItem
    batInputWatts.sendCommand(Integer::parseInt(transform("JSONPATH","$.data.20_1_batInputWatts",res))/10.0)
    var NumberItem batChargeLevel=map.get("batChargeLevel") as NumberItem
    batChargeLevel.sendCommand(Integer::parseInt(transform("JSONPATH","$.data.20_1_batSoc",res)))

    return true
 

}catch(Exception e)
{
    logError("ECOFLOW","Exception: {}",e.message)
}

    false  

]






val Functions$Function1<HashMap<String, Object>,ArrayList<HashMap<String, Object>>> enumDevices=[HashMap<String, Object> map|

try
{

    val Functions$Function3<HashMap<String, Object>,String, String, String> encryptHmacSHA256 =map.get("encryptHmacSHA256") as Functions$Function3<HashMap<String, Object>,String, String, String> 
    val Functions$Function4<HashMap<String, Object>,String,String,String,HashMap<String,String>> getHeaders=map.get("getHeaders") as Functions$Function4<HashMap<String, Object>,String,String,String,HashMap<String,String>>
    val Functions$Function1<SortedMap<String, Object>,String> createParameters=map.get("createParameters") as  Functions$Function1<SortedMap<String, Object>,String>

    var String key=map.get("key") as String
    var String secret=map.get("secret") as String
    var funcHeaders=getHeaders.apply(map,key,secret,null)
 
    var String urlBase=map.get("urlbase") as String
    var String urlDevice=map.get("urldevice") as String
    urlDevice=urlBase+urlDevice
    var String urlDeviceQuota=map.get("urlquota") as String
    urlDeviceQuota=urlBase+urlDeviceQuota

   // logInfo("ECOFLOW","headers1: {}",headers)  
   // logInfo("ECOFLOW","funcHeaders: {}",funcHeaders)  

   // logInfo("ECOFLOW","api: {}",urlDevice) 

    var res=sendHttpGetRequest(urlDevice,funcHeaders,10000)
    var Integer code=Integer::parseInt(transform("JSONPATH","$.code",res))
    if(code!=0)
        logWarn("ECOFLOW","res: {}",res) 


    var ArrayList<HashMap<String, Object>> resultList=newArrayList

    var Integer arrayLength=Integer::parseInt(transform("JSONPATH","$.data.length()",res))

    for(var int i=0;i<arrayLength;i++)
    {
        var String item=String::format("$.data[%d]",i)
        var String  sn=transform("JSONPATH",item+".sn",res)
        var Boolean online=Integer::parseInt(transform("JSONPATH",item+".online",res))==1
        logInfo("ECOFLOW","sn: {} - online:{}",sn,online) 
        var HashMap<String, Object> device=newHashMap

        device.put("sn",sn)
        device.put("online",online)
        resultList.add(device)

    }
    return resultList
  
}catch(Exception e)
{
    logError("ECOFLOW","Exception: {}",e.message)
}

    null  

]
/**
 * @author kevin.liu
 * @date 2022/12/16 14:55
 * @description
 */
val Functions$Function3<HashMap<String, Object>,String, String, String> encryptHmacSHA256 =
 [HashMap<String, Object> map,String message,String secret|


        try {
            val Functions$Function1<byte[],  String> byteArrayToHexString=map.get("byteArrayToHexString") as Functions$Function1<byte[],  String> 
            var Mac sha256HMAC = Mac.getInstance("HmacSHA256");
            var SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
            sha256HMAC.init(secret_key);
           
            return byteArrayToHexString.apply(sha256HMAC.doFinal(message.getBytes()));
        } 
        catch (Exception e) {
            throw new RuntimeException(e);}
     
 ]






val Functions$Function1<byte[],  String> byteArrayToHexString=[
    byte[] ba|
  
       
        var StringBuilder hs = new StringBuilder()
        var String stmp
        for (var int n = 0; ba != null && n < ba.length; n++) {
            var byte hex=ba.get(n).bitwiseAnd(0xFF).byteValue
            stmp = Integer.toHexString(hex)
            stmp=String.format("%02X", ba.get(n).bitwiseAnd(0xFF).byteValue)
            if (stmp.length() == 1)
                hs.append('0')
            hs.append(stmp)
        }
        return hs.toString().toLowerCase();
    
]

rule "ECOFLOW start"
when Item Startup_EcoflowControl received update or Item Startup_Complete received update or Item Startup_Order changed to 41
then

    if(Startup_EcoflowControl.state !== ON|| Startup_Complete.state!==ON )
        return;
    if(Startup_Order.state ==NULL)
        return;
    if((Startup_Order.state as Number).intValue < STARTUP_ID) 
        return;
 
    if((Startup_Order.state as Number).intValue == STARTUP_ID)
        Startup_Order.sendCommand((Startup_Order.state as Number)+1)
    if(startupComplete==true)
    return;         

    functions.put("byteArrayToHexString",byteArrayToHexString)
    functions.put("encryptHmacSHA256",encryptHmacSHA256)
    functions.put("createParameters",createParameters)
    functions.put("getHeaders",getHeaders)
    functions.put("getDeviceStatus",getDeviceStatus)


    var String api_url_base = "https://api.ecoflow.com/iot-open/sign"
 
    var String key = 'Mz9oNcXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    var String secret = '2fUsXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

    var String url_device = "/device/list"
    var String url_quota = "/device/quota"

    functions.put("key",key)
    functions.put("secret",secret)
    functions.put("urlbase",api_url_base)
    functions.put("urldevice",url_device)
    functions.put("urlquota",url_quota)

    functions.put("invOutputWatts",EF_InvOutputWatts)
    functions.put("pvToInvWatts",EF_PVToInvWatts)
    functions.put("batInputWatts",EF_BatInputWatts)
    functions.put("batChargeLevel",EF_BatChargeLevel)
    

    EF_SetPowerHeartbeat.sendCommand(OFF)
    startupComplete=true
    logInfo("ECOFLOW","ECOFLOW START COMPLETE")
end 

rule "ECOFLOW heartbeat set power"
when Time cron "*/5 * * ? * *"
then
try
{
    if(startupComplete==false)    
        return;
    logInfo("ECOFLOW","Set heartbeat")
    EF_SetPowerHeartbeat.sendCommand(ON)

}catch(Exception e)
{
    logError("ECOFLOW","Exception: {}",e.message)
}
 
end

rule "ECOFLOW heartbeat set power receive"
when Item SMRPowerIn received update
then
try
{
    if(startupComplete==false)    
        return;
    logInfo("ECOFLOW","Set heartbeat receive")
    var Float power=(SMRPowerIn.state as Number).floatValue
    var Integer currentPower=0
    if(EF_SetOutputPower.state!==NULL)
       currentPower= (EF_SetOutputPower.state as Number).intValue
    var Double factor=0.3
    if(EF_CorrectionIFactor.state!=NULL)
        factor=(EF_CorrectionIFactor.state as Number).doubleValue
    if(power<0.0)
        factor=1.0
    currentPower=(currentPower+power*factor).intValue
    var Integer max=800
    if(EF_MaxOutputValue.state!==NULL)
        max=(EF_MaxOutputValue.state as Number).intValue
    if(currentPower>max)
        currentPower=max
    if(currentPower<0)
        currentPower=0
   
    if(EF_InvOutputWatts.state!==NULL)
    {
        if((EF_InvOutputWatts.state as Number).intValue==0 && currentPower>0)
        {
            if(currentPower>20)
                currentPower=20
        }

    }

    var ArrayList<HashMap<String, Object>> resultList=enumDevices.apply(functions)

    for(var int i=0;i<resultList.size;i++)
    {
        
        var Boolean online=resultList.get(i).get("online") as Boolean
        if(EF_Online.state!==OFF && !online)
             EF_Online.sendCommand(OFF)
        if(EF_Online.state!==ON && online)
             EF_Online.sendCommand(ON)
        if(!online)
            currentPower=0
        EF_SetOutputPower.sendCommand(currentPower)
        
        var String sn=resultList.get(i).get("sn") as String
        if(setOutputPower.apply(functions,resultList.get(i),currentPower)==true)
            logInfo("ECOFLOW","Output power for device {} set to {} watts",sn,currentPower)

        getDeviceStatus.apply(functions,sn)
    }

    EF_SetPowerHeartbeat.postUpdate(OFF) 

}catch(Exception e)
{
    logError("ECOFLOW","Exception: {}",e.message)
} 

end


rule "ECOFLOW heartbeat startup" 
when Item Startup_Heartbeat received command ON
then
try
{
    if(startupComplete==false)    
    {
        Startup_EcoflowControl.sendCommand(ON)
 
    }
} 
catch(Exception e)
{
      logError("ECOFLOW",e.toString)
} 
end