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